作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
伊万·巴甫洛夫的头像

Ivan Pavlov

Ivan拥有后端和前端开发经验. 他曾为银行、医疗机构和城市管理部门开发软件.

Expertise

Years of Experience

17

Share

Users don’t care what’s inside the software they use; just that it works smoothly, safely, and unobtrusively. 开发人员努力做到这一点, 他们试图解决的问题之一是如何确保数据存储处于适合产品当前版本的状态. 软件在发展,它的数据模型也可能随着时间的推移而改变.g., to fix design mistakes. 使问题进一步复杂化, 您可能有许多测试环境或客户以不同的速度迁移到产品的新版本. 您不能仅仅从单一角度记录商店的结构以及使用闪亮的新版本需要哪些操作.

数据库迁移:把毛毛虫变成蝴蝶

我曾经加入过一个项目,其中有几个数据库的结构是按需更新的, directly by developers. 这意味着没有明显的方法来发现需要应用哪些更改来将结构迁移到最新版本,并且根本没有版本控制的概念! 这是在devops之前的时代,现在会被认为是一团糟. 我们决定开发一种工具,用于将每次更改应用到给定的数据库. 它具有迁移功能,并将记录模式更改. 这使我们确信不会有意外的变化,并且模式状态是可预测的.

In this article, 我们将了解如何应用关系数据库模式迁移,以及如何克服随之而来的问题.

首先,什么是数据库迁移? 在本文的上下文中,a migration 是否应该将一组更改应用于数据库. 创建或删除表、列或索引是迁移的常见示例. 随着时间的推移,你的图式的形状可能会发生巨大的变化, 特别是如果开发是在需求还很模糊的时候开始的. So, 在发布的过程中经历了几个里程碑, 您的数据模型将不断发展,并且可能与最初的数据模型完全不同. 迁移只是到达目标状态的步骤.

To get started, 让我们探索一下我们的工具箱里有什么,以避免重新发明已经做得很好的东西.

Tools

在每一种广泛使用的语言中,都有帮助简化数据库迁移的库. 例如,在Java的情况下,流行的选项是 Liquibase and Flyway. 我们会在例子中更多地使用liquebase, 但这些概念适用于其他解决方案,并不局限于Liquibase.

如果一些orm已经提供了自动升级模式并使其与映射类的结构相匹配的选项,那么为什么还要使用单独的模式迁移库呢? 在实践中,这种自动迁移只进行简单的模式更改,例如.g., 创建表和列, 并且不能做潜在的破坏性的事情,比如删除或重命名数据库对象. 因此,非自动化(但仍然是自动化的)解决方案通常是更好的选择,因为您必须自己描述迁移逻辑, 你知道你的数据库会发生什么.

混合使用自动和手动模式修改也是一个非常糟糕的主意,因为如果以错误的顺序应用手动更改或根本不应用手动更改,则可能生成唯一且不可预测的模式, even if they are required. 一旦选择了该工具,就可以使用它来应用所有模式迁移.

典型的数据库迁移

典型的迁移包括创建序列, tables, columns, primary and foreign keys, indexes, and other database objects. 对于大多数常见类型的更改, Liquibase提供了不同的声明性元素来描述应该做什么. 阅读Liquibase或其他类似工具支持的每一个细微变化都太无聊了. 以了解更改集的外观, 考虑下面的例子,我们创建了一个表(为了简洁,省略了XML名称空间声明):



    
        
            
                
            
            
                
            
        
    

如您所见,变更日志是一组变更集,而变更集由变更组成. Simple changes like createTable can be combined to implement more complex migrations; e.g.,假设您需要更新所有产品的产品代码. 这可以很容易地通过以下改变来实现:

UPDATE product SET code = 'new_' || code

如果你有无数的产品,性能就会受到影响. 为了加快迁移速度,我们可以将其重写为以下步骤:

  1. 为产品创建一个新表 createTable, just like we saw earlier. 在这个阶段,最好创建尽可能少的约束. Let’s name the new table PRODUCT_TMP.
  2. Populate PRODUCT_TMP with SQL in the form of INSERT INTO ... SELECT ... using sql change.
  3. 创建所有约束(addNotNullConstraint, addUniqueConstraint, addForeignKeyConstraint) and indexes (createIndex) you need.
  4. Rename the PRODUCT table to something like PRODUCT_BAK. Liquibase can do it with renameTable.
  5. Rename PRODUCT_TMP to PRODUCT (again, using renameTable).
  6. Optionally, remove PRODUCT_BAK with dropTable.

Of course, 最好避免这种迁移, 但最好知道如何实现它们,以防遇到需要它的罕见情况.

如果您认为XML、JSON或YAML对于描述更改的任务来说太奇怪,那么就 use plain SQL 并利用所有数据库供应商特有的特性. Also, you can implement any custom logic in plain Java.

Liquibase免除编写实际数据库特定SQL的方式可能会导致过度自信, but you should not forget about the quirks of your target database; e.g., 创建外键时, 索引可以创建,也可以不创建, 取决于所使用的特定数据库管理系统. 因此,你可能会发现自己处于尴尬的境地. Liquibase允许您指定仅针对特定类型的数据库运行更改集, e.g.、PostgreSQL、Oracle或MySQL. 对于不同的数据库,使用相同的与供应商无关的更改集可以实现这一点, and for other changesets, 使用特定于供应商的语法和特性. 下面的变更集只有在使用Oracle数据库时才会执行:


    ...

除了Oracle, Liquibase还支持一些 other databases out of the box.

Naming Database Objects

您创建的每个数据库对象都需要命名. 您不需要显式地为某些类型的对象提供名称,例如.g.,用于约束和索引. But it doesn’t mean that those objects won’t have names; their names will be generated by the database anyway. 当您需要引用该对象以删除或更改它时,问题就出现了. 所以最好给它们起个明确的名字. 但是给什么名字有什么规定吗? The answer is short: Be consistent; e.g.,如果您决定像这样命名索引: IDX_

_,然后是前面提到的索引 CODE column should be named IDX_PRODUCT_CODE.

命名约定是非常有争议的, 所以我们不会在这里给出全面的说明. 保持一致,尊重你的团队或项目惯例,或者在没有惯例的情况下创造它们.

Organizing Changesets

要决定的第一件事是在哪里存储更改集. 基本上有两种方法:

  1. 将变更集与应用程序代码一起保存. 这样做很方便,因为您可以同时提交和审查变更集和应用程序代码.
  2. 保持变更集和应用程序代码的分离, e.g.,在单独的VCS存储库中. 当数据模型在多个应用程序之间共享时,这种方法是合适的,并且更方便地将所有更改集存储在专用存储库中,而不是将它们分散到应用程序代码所在的多个存储库中.

存储更改集的位置, 一般来说,将它们分为以下几类是合理的:

  1. 不影响正在运行的系统的独立迁移. 创建新表通常是安全的, sequences, etc, 如果当前部署的应用程序还不知道它们.
  2. 改变存储结构的模式修改, e.g.,添加或删除列和索引. 当应用程序的旧版本仍在使用时,不应该应用这些更改,因为这样做可能会由于模式的更改而导致锁定或奇怪的行为.
  3. 插入或更新少量数据的快速迁移. 如果部署了多个应用程序, 此类别的变更集可以并发执行,而不会降低数据库性能.
  4. 插入或更新大量数据的迁移可能会减慢速度. 最好在没有执行其他类似迁移时应用这些更改.

四个类别的图形表示

在部署应用程序的新版本之前,应该连续运行这些迁移集. 如果系统由几个独立的应用程序组成,并且其中一些应用程序使用相同的数据库,则这种方法更加实用. Otherwise, 只分离那些可以应用而不会影响正在运行的应用程序的更改集是值得的, 其余的变更集可以一起应用.

对于更简单的应用程序,可以在应用程序启动时应用全套必要的迁移. In this case, 所有更改集都属于一个类别,并在初始化应用程序时运行.

选择在哪个阶段应用迁移, 值得一提的是,在应用迁移时,对多个应用程序使用相同的数据库可能会导致锁. Liquibase(像许多其他类似的解决方案一样)使用两个特殊的表来记录其元数据: DATABASECHANGELOG and DATABASECHANGELOGLOCK. 前者用于存储有关已应用的更改集的信息, 后者用于防止同一数据库模式内的并发迁移. So, 如果多个应用程序出于某种原因必须使用相同的数据库模式, 最好为元数据表使用非默认名称,以避免锁.

现在高层结构很清楚了, 您需要决定如何在每个类别中组织变更集.

样例变更集组织

这在很大程度上取决于具体的应用需求, 但以下几点通常是合理的:

  1. 将变更日志按产品的发布进行分组. 为每个版本创建一个新目录,并将相应的变更日志文件放入其中. Have a root changelog and include 与发布相对应的变更日志. 在发布变更日志中,包括包含此版本的其他变更日志.
  2. 对变更日志文件和变更集标识符有一个命名约定——当然要遵守它.
  3. 避免使用包含大量更改的更改集. 选择多个变更集而不是一个长变更集.
  4. 如果您使用存储过程并需要更新它们,请考虑使用 runOnChange="true" 在其中添加该存储过程的更改集的属性. Otherwise, each time it’s updated, 您需要使用存储过程的新版本创建一个新的更改集. 需求各不相同,但是不跟踪这样的历史记录通常是可以接受的.
  5. 考虑在合并特性分支之前压缩冗余的更改. Sometimes, 在一个特性分支中(尤其是在一个长期存在的分支中),较晚的变更集会完善较早的变更集中所做的变更. 例如,您可能创建了一个表,然后决定向其中添加更多列. 值得将这些列添加到初始值中 createTable 如果此特性分支尚未合并到主分支,则更改.
  6. 使用相同的变更日志创建测试数据库. If you try to do so, 您可能很快就会发现,并不是每个变更集都适用于测试环境, 或者特定的测试环境需要额外的变更集. 使用Liquibase,这个问题很容易解决 contexts. Just add the context="test" 属性设置为只需要使用测试执行的更改集, ,然后初始化Liquibase test context enabled.

Rolling Back

与其他类似的解决方案一样,Liquibase支持“向上”和“向下”迁移模式.但要注意:撤销迁移可能并不容易,而且并不总是值得这么做. 如果您决定为您的应用程序支持撤销迁移, 然后保持一致,并对每个需要撤消的更改集执行此操作. 使用Liquibase,通过添加一个 rollback 标记,该标记包含执行回滚所需的更改. 考虑下面的例子:


    
        
            
        
        
            
        
    
    
        
    

这里显式回滚是多余的,因为Liquibase将执行相同的回滚操作. Liquibase能够自动回滚其支持的大多数类型的更改,例如.g., createTable, addColumn, or createIndex.

Fixing the Past

人无完人,我们都会犯错. 其中一些可能发现得太晚了,因为已经应用了损坏的更改. 让我们来探讨一下我们能做些什么来挽救这一天.

手动更新数据库

It involves messing with DATABASECHANGELOG 和您的数据库在以下方面:

  1. 如果您想纠正错误的更改集并再次执行它们:
    • Remove rows from DATABASECHANGELOG 对应于变更集的.
    • Remove all side effects that were introduced by the changesets; e.g.,如果表被删除,则恢复表.
    • Fix the bad changesets.
    • Run migrations again.
  2. 如果你想纠正错误的更改集,但不想再次应用它们:
    • Update DATABASECHANGELOG by setting the MD5SUM field value to NULL 对于那些与坏的更改集相对应的行.
    • 手动修复数据库中的错误. 例如,如果添加了一个类型错误的列,那么发出一个查询来修改它的类型.
    • Fix the bad changesets.
    • Run migrations again. Liquibase将计算新的校验和并将其保存到 MD5SUM. 更正后的变更集不会再次运行.

Obviously, 在开发过程中很容易使用这些技巧, 但是,如果将更改应用于多个数据库,则会变得更加困难.

编写纠正性变更集

在实践中,这种方法通常更合适. 您可能会想,为什么不直接编辑原始变更集呢? 事实是,这取决于需要改变什么. Liquibase为每个更改集计算校验和,如果至少一个先前应用的更改集的校验和是新的,则拒绝应用新的更改. 此行为可以在每个更改集的基础上通过指定 runOnChange="true" attribute. 修改后不影响校验和 preconditions 或可选的变更集属性(context, runOnChange, etc.).

现在,您可能想知道,如何最终纠正带有错误的更改集?

  1. 如果您希望这些更改仍然应用于新模式,那么只需添加纠正性更改集. For example, 如果添加了类型错误的列, 然后在新的变更集中修改它的类型.
  2. 如果您想假装这些糟糕的变更集从未存在过,那么请执行以下操作:
    • 删除更改集或添加 context 属性,其值保证您再也不会尝试使用这种上下文应用迁移.g., 上下文= " graveyard-changesets-never-run ".
    • 添加新的更改集,这些更改集将恢复错误或修复错误. 只有在应用了不好的更改时,才应该应用这些更改. 它可以通过先决条件来实现,例如 changeSetExecuted. 别忘了加上评论,解释你为什么这么做.
    • 添加以正确方式修改模式的新更改集.

正如你所看到的,修复过去是可能的,尽管它可能并不总是直截了当的.

Mitigating Growing Pains

随着您的应用程序变老, its changelog also grows, 沿着路径积累每一个模式变化. 这是设计出来的,这本身并没有什么错. 通过定期压缩迁移,可以缩短长时间的变更日志.g.,在产品的每个版本发布后. 在某些情况下,它可以更快地初始化新模式.

变更日志被压缩的说明

压缩并不总是微不足道的,可能会导致回归,而不会带来很多好处. 另一个不错的选择是使用种子数据库来避免执行所有更改集. 如果您需要尽快准备好数据库,那么它非常适合测试环境, 也许还需要一些测试数据. 您可以将其视为压缩变更集的一种形式:在某些情况下(例如.g.(在发布另一个版本之后),对模式进行转储. 恢复转储之后,像往常一样应用迁移. Only new changes will be applied because older ones were already applied before making the dump; therefore, 它们是从垃圾场恢复过来的.

种子数据库的说明

Conclusion

我们有意避免深入探讨Liquibase的特性,以提供一篇简短而切中要害的文章, 主要关注发展中的模式. Hopefully, 数据库模式迁移的自动化应用程序所带来的好处和问题,以及它的适用性,都是显而易见的 DevOps culture. 重要的是不要把好的想法变成教条. Requirements vary, and as database engineers, 我们的决定应该促进产品的发展,而不是仅仅遵循互联网上某人的建议.

Understanding the basics

  • 数据库中模式的含义是什么?

    数据库模式描述了如何在数据库中组织数据.

  • 数据库和模式之间的区别是什么?

    模式是数据库的一部分. 数据库通常由一个或多个模式组成. 另外,将DBMS称为数据库也很常见. 从上下文中可以清楚地看出,我们讨论的是数据容器还是管理该容器的系统.

  • 图式的例子是什么?

    如果您正在构建一个旅游管理应用程序, 它的数据库模式将包含像airline这样的实体, flight, or city. 除了用户定义模式, DBMS通常有一个“信息模式”,可用于查询设置和元数据.

  • 有哪些不同类型的数据库?

    除了关系数据库之外,还有对象数据库、面向文档数据库和层次数据库.

  • 有没有办法使查询运行得更快?

    您可以通过重组查询或更改模式来优化慢速查询. DBMS通常可以为您提供执行计划,并帮助您猜测是什么减慢了您的查询速度.g.(不使用索引或在子查询中选择太多数据). 在PostgreSQL中,你可以使用EXPLAIN或EXPLAIN ANALYZE来了解哪里出了问题.

  • PostgreSQL的架构是什么?

    In PostgreSQL, you may have several databases within the same cluster; the schema is a structural element of the database. 它是表、视图、过程等的容器. 模式可以被视为数据库中的目录,但模式不能包含其他模式.

聘请Toptal这方面的专家.
Hire Now
伊万·巴甫洛夫的头像
Ivan Pavlov

Located in Hamburg, Germany

Member since March 7, 2016

About the author

Ivan拥有后端和前端开发经验. 他曾为银行、医疗机构和城市管理部门开发软件.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Expertise

Years of Experience

17

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Join the Toptal® community.