什么是数据库迁移(Database Migrations)

引言

数据库模式(Database schemas)定义了关系型数据库所管理数据的结构与相互关系。尽管在项目初期制定一个经过深思熟虑的schema至关重要,但随着需求的不断演进,对初始schema进行更改往往难以甚至无法避免。由于schema决定了数据的形态与边界,任何变更都必须谨慎实施,以符合使用该schema的应用程序的预期,并避免丢失数据库系统当前已存储的数据。

在本文中,我们将介绍什么是数据库模式迁移、它们解决了哪些问题以及不同的实现策略。我们还将探讨它们所解决的各种痛点以及可能带来的一些潜在陷阱。

什么是数据库迁移(Database Migrations)

数据库迁移(Database migrations),又称 schema 迁移、数据库 schema 迁移,或简称迁移,是一组受控的变更,用于修改关系型数据库中对象的结构。迁移帮助将数据库 schema 从当前状态过渡到新的期望状态,无论是添加表和列、删除元素、拆分字段,还是更改类型和约束。

迁移以编程管理的方式对数据结构做增量的、通常是可逆的变更。数据库迁移工具的目标是使数据库变更可重复、可共享且可测试,同时避免数据丢失。通常,迁移工具会生成描述将数据库从已知状态转换到新状态所需的确切操作集的产物。这些产物可以被纳入常规的版本控制系统中,以便跟踪变更并在团队成员之间共享。

尽管防止数据丢失通常是迁移工具的目标之一,但删除或破坏性修改当前包含数据的结构可能会导致数据被删除。为应对这种情况,迁移通常是一个受监督的过程,涉及检查生成的变更脚本,并进行必要的修改以保留重要信息。

迁移工具有哪些优势

迁移之所以有用,是因为它们允许 database schema 随需求变化而演进。它们帮助开发者规划、验证并安全地将 schema 变更应用到各自的环境。这些被切分的变更在粒度级别上定义,描述了在不同“版本”数据库之间迁移所需完成的转换。

一般而言,迁移工具会生成可共享、可作用于多套数据库系统并能纳入版本控制的产物或文件。这有助于构建一份与客户端应用程序代码变更紧密关联的数据库修改历史,使 database schema 与应用程序对该结构的假设能够同步演进。

其他好处还包括:允许(有时是必须)手动微调流程,也就是说可以将流程列表的生成与执行分离开来。每一次变更都可被审计、测试和修改,以确保获得正确结果,同时仍让自动化承担大部分工作。

迁移工具有哪些劣势

迁移工具并非没有缺陷,但幸运的是,大多数缺陷都可以通过流程和监督加以缓解。

由于迁移会修改现有的数据库结构,因此必须小心避免数据丢失。这可能是由工具的错误假设引起的,也可能是某些转换需要对数据结构背后的含义有深入理解所致。尽管人们可能倾向于认为生成的迁移文件是正确的,但由于迁移文件主要关注的是数据结构,因此你有责任确保数据也能被正确保留或转换。

如果迁移被应用到一个与假设状态不同的数据库,也可能出现问题。例如,当没有使用迁移工具对数据库结构进行更改,或者迁移以错误的顺序应用时,就可能发生这种情况。所有迁移工具都依赖于对数据库当前状态的理解,以便正确修改现有结构。如果实际状态与假设状态不一致,迁移可能会失败,或者可能以不希望的方式更改数据库。

迁移的另一个潜在问题是,它们通常非常依赖于特定工具。迁移工具会生成代表数据库状态或所需变更的产物。尽管有些工具会生成纯 SQL 结果,但它们有时会在注释中编码额外的细节内容(如特定于某个工具的指令)供工具解析,或者使用特殊的文件名格式来表示顺序。在没有对正在使用的迁移工具进行充分理解的情况下,切换到不同的工具可能会很困难。

基于状态(state)与基于变更(change)的迁移

迁移工具主要采用两种 schema 转换策略:基于状态(state-based)的迁移和基于变更(change-based)的迁移。两者都可有效管理迁移流程,有时结合使用效果更佳。然而,各自的优势往往决定了哪种方式更适合某个项目,关键在于它与团队开发风格的契合程度。

基于状态(state-based)的迁移

基于状态(state-based)的迁移工具会生成描述如何“从零开始”重建目标数据库状态的产物。这些产物可被应用到一个空的关联数据库系统,使其一次性达到最新版本。

在描述目标状态的产物生成后,实际的迁移过程会将这些文件与数据库当前状态进行对比。工具会借此分析两者差异,并生成新的文件(或一组文件),将当前数据库 schema 调整至产物所描述的 schema。随后,这些变更操作被应用到数据库,以达成最终目标状态。

使用基于状态的迁移时需要注意什么

与几乎所有迁移一样,基于状态的迁移文件必须由经验丰富的开发者仔细审查,以监督整个过程。既要检查描述目标最终状态的文件,也要审查将当前数据库调整至该状态所需操作的文件,确保这些转换不会导致数据丢失。例如,如果生成的操作试图通过删除现有表再重建同名新表的方式来重命名表,那么就必须有人工识别并干预,以防止数据丢失。

如果数据库 schema 频繁发生需要此类人工干预的重大变更,基于状态的迁移就会显得笨重。由于这种额外开销,该技术更适合那些 schema 事先经过充分设计、且基本变更不频繁的场景。

然而,基于状态的迁移也有其优势:它能产出一份在单一上下文中完整描述数据库状态的文件。这有助于新成员快速上手,并且与版本控制系统的工作流契合良好,因为代码分支引入的冲突可以很容易地解决。

基于变更(change-based)的迁移

与基于状态的迁移相对的主要方案是“基于变更”的迁移工具。基于变更的迁移同样会生成用于修改数据库现有结构以达到期望状态的文件。不同的是,它并非去“发现”目标数据库状态与当前状态的差异,而是以某个已知数据库状态为起点,明确定义一系列操作,将其过渡到新状态。随后不断生成新的迁移文件,对数据库做进一步修改,形成一串可按顺序依次执行的变更文件,最终重现出目标数据库状态。

由于基于变更的迁移通过“从已知状态到目标状态所需操作”来工作,因此必须从初始起点开始,保持一条不间断的迁移文件链。该系统需要一个初始状态(可以是一个空数据库,也可以是描述起始结构的文件)、描述每一次模式转换操作的文件,以及一个定义迁移文件必须按何种顺序执行的明确次序。

使用基于变更的迁移时需要注意什么

基于变更的迁移通过其生成的一系列转换脚本,将数据库模式设计的血缘一直追溯到最初结构。这有助于展示数据库结构的演进过程,但难以让人在任意时刻一眼看清数据库的完整状态,因为每个文件中的变更都是在上一个迁移文件产出的结构之上再做修改。

由于前一状态对基于变更的系统至关重要,这类系统往往会在数据库内部再建一张(元数据)表,记录哪些迁移文件已被执行。借此,工具无需分析当前结构并与“期望状态”比对,就能知道系统身处何态;而期望状态只能通过依次编译全部迁移文件才能得知。

该方法的劣势在于:除初始点外,代码仓库里再也找不到对数据库当前状态的完整描述。每个迁移文件都依赖前一个文件,因此虽然变更被很好地模块化,但在任意时刻想推理出整个数据库全貌却变得困难。此外,操作顺序至关重要,当不同开发者做出冲突变更时,也更难解决冲突。

然而,基于变更的系统也具备优势:它允许对数据库结构进行快速、迭代的调整。省掉了分析当前状态→对比期望状态→生成操作文件→再应用这一耗时流程,系统直接依据之前已执行的变更来假定当前状态。总体而言,这让变更更轻量,但也使得绕过迁移直接改库的行为格外危险,因为迁移可能让目标系统陷入未定义状态。

如何使用数据库迁移

在介绍了两种主要的迁移工具类别之后,我们来看看在应用开发、部署与协作过程中,如何实际使用这些工具。

开发期间

从你对初始数据模型做出第一次修改的那一刻起,数据库迁移就可能变得相关。事实上,大多数迁移工具都会生成迁移文件,将关系型数据库从完全空白的状态带到初始模型。

在开发应用程序的过程中,你希望存储的数据形态、数据库系统的要求,以及你想保留的具体细节常常会变化。这可能是因为你对问题域的理解更加深入,也可能是因为新增功能需要额外的数据或对现有信息采用不同的布局。重要的是,在开发过程中一旦发生这些模型变更,就要及时定义它们,以确保迁移产物能与相关代码同步并一起部署。

将迁移文件与对应代码库一起存储和管理,可以让数据库模型变更像代码变更一样接受评审。如前所述,基于状态的迁移能让这一过程更简单,因为通过文件差异就能直观看到期望状态之间的冲突;而基于变更的系统在出现冲突时需要更谨慎地手动解决,因为顺序至关重要,且任何一份迭代迁移文件都无法看到完整的数据结构。

总体而言,在开发阶段定义数据库变更时应保持保守,并记住这些变更最终可能会在真实生产数据上执行。这一点对破坏性变更尤其重要。应考虑更安全的替代删除方案,例如用重命名列代替删除列、用软删除代替物理删除,或在转换数据结构时先复制再修改,而非直接覆盖。

换句话说,数据库模型迁移是你随代码变更一起维护的变更之一。模型迁移应与代码变更同步考虑,以确保所有变更都有记录,并且适用于当前的数据库设计。

测试和部署期间

迁移文件创建完成后,通常需要依次部署到越来越重要的环境中,以验证它们是否按预期工作,并确保所有代码需求都得到满足。虽然你很可能在开发环境中已经编写并应用了这些迁移,但就像对待代码变更一样,你还需在数据更真实、集成更完整、负载更接近实际的额外环境中进行测试。

此时,你必须确认迁移文件能够干净地应用到目标数据库,目标数据库的初始结构与你在开发迁移文件时所假设的结构一致,并且数据库中的数据不会因所做的变更而受到负面影响。可以通过手动与自动化检查的组合来确保这些要求得到满足。

一旦变更在预发布环境中被验证为安全且功能正常,就可以准备将其应用到生产数据库系统。在将迁移应用于生产系统之前,如果近期没有可用的数据副本,务必考虑对当前数据执行一次完整备份。若迁移产生不可预见的后果,或预发布环境与生产环境之间的差异导致数据丢失,备份将是最后的救命稻草。

如何选择数据库迁移工具

综合上述内容,你究竟该如何为项目挑选最合适的迁移方案?可以从以下几个维度来缩小选择范围。

有时,代码语言、开发框架或所用的数据库后端会强烈暗示你该用哪款迁移工具。例如,许多 Web 框架自带迁移工具来管理底层数据库的 schema,虽然通常不是硬性要求,但它们与框架其他工具链集成良好,能减少把数据库变更当作独立流程的摩擦。

另一个考量是工具的成熟度与支持力度。如前所述,不同工具生成的迁移文件往往互不兼容,尽管它们可能都能以纯 SQL 直接执行。选一个稳定、持续维护的工具,可避免中途因工具弃用而被迫更换迁移流程的尴尬。

迁移产物的格式也值得留意。如果生成的是排版清晰、结构良好的 SQL 文件,你基本能看懂并验证其效果;若迁移脚本用项目本身的编程语言编写,也更容易理解,无需频繁切换上下文。尽量避免那些产出难以阅读或手工修改的迁移文件。

此外,迁移工具提供的附加功能也会影响决策。有些工具与其他生态深度集成,或支持把基于变更的迁移文件“压缩”成一份 checkpoint 状态文件;它们在排查失败、回滚变更等方面的能力也各不相同。你的实际需求与开发理念,将决定哪些功能才是刚需。

总结

在本文中,我们探讨了数据库模式迁移的概念,并讨论了用于管理数据结构变更的一些策略。我们谈到了迁移为何有用、不同类别的迁移工具以及每种方法所带来的好处。最后,我们介绍了如何实际将迁移纳入开发工作流,以及如何找到最符合自身风格与需求的工具。

总体而言,尽管模式迁移工具并非必需,但它们所提供的组织性、功能性与可控性,使其成为开发中最常用的数据库工具之一。能够规划并执行数据库结构的变更,并将这些修改与代码一同记录,具有极高的价值。虽然不同迁移工具在功能或分析差异与应用变更的策略上可能各异,但它们所扮演的角色,仍是实现可靠且可预测开发的重要组成部分。