Grails 2.0 倒计时:数据库迁移

工程 | Peter Ledbrook | 2011年8月17日 | ...

Grails 的众多出色功能之一是它能够根据您的领域模型自动为您创建数据库 schema。诚然,这是 Grails 使用的 Hibernate 的一个功能,但它仍然可以帮助您非常快速地开始使用数据库驱动的 Web 应用程序,而无需担心数据库 schema。

一旦您的应用程序投入生产,会发生什么?在开发过程中,服务器运行之间丢失数据并不是一个大问题。但是您不能直接在生产环境中删除数据库。因此,这排除了dbCreate数据源设置。“更新”怎么样?它不会清除数据库中的数据,因此人们经常使用它。然而,它不适合生产环境,因为它有显著的局限性。例如,它无法处理简单的列重命名,当然也无法处理现有数据的修改,而这可能是升级的必要部分。尽管使用dbCreate = "update"来部署到生产环境很有诱惑力,但这通常是错误的解决方案。

那剩下什么选择呢?用于执行迁移的 SQL 脚本?这当然可行,但要为 GORM 领域模型中的给定更改创建适当的 SQL 并不容易。而且,如果您需要支持多种数据库类型,这也不适用,因为每种数据库的 SQL 可能都会有所不同。

幸运的是,有一个灵活的、与数据库无关的工具可以执行 schema 迁移:Liquibase。更好的是,有两个 Grails 插件可以使 Liquibase 的使用比平时更容易一些:Liquibase 插件Autobase

那么,如果这些插件已经存在很长时间了,Grails 2.0 有什么新功能呢?数据库迁移是任何严肃工作中使用 Grails 的重要组成部分,因此 Grails 团队决定应该有一种官方方式来处理它们。结果是一个新插件,它结合了 Liquibase 和 Autobase 插件的最佳部分:数据库迁移插件

简而言之

尽管核心数据库迁移支持是 Grails 2.0 路线图的一部分,但没有理由将其绑定到该版本。这意味着您也可以在 Grails 1.3 项目中使用该插件。该插件为您的应用程序带来了以下功能

  • 声明式数据库 schema 和数据迁移
  • Groovy 和 XML 迁移脚本
  • 手动或自动执行迁移
  • 自动跟踪已应用的迁移
  • 通过比较领域模型和数据库生成迁移

它提供了超过 20 个命令,为您提供了充分的控制。您可以从插件的用户指南中了解更多信息,但简而言之,它为管理 Grails 应用程序新版本的受控数据库升级提供了很大的帮助。如果您愿意,仍然可以使用其他插件,甚至完全不同的方法,但对于大多数用户来说,这绝对是首选。

本文的其余部分将向您展示该插件的常见用法模式。

入门

想象一下,您一直在努力开发一个 Grails 应用程序,现在您想将第一个版本部署到生产环境。是时候考虑如何管理数据库升级了。此时,生产数据库甚至还没有创建。因此,将数据库迁移插件声明为运行时依赖项,如下所示

grails.project.dependency.resolution = {
    inherits "global"
    ...
    plugins {
        runtime ":database-migration:1.0"

        compile ":hibernate:$grailsVersion"
        compile ":jquery:1.6.1.1"
        compile ":resources:1.0.2"

        build ":tomcat:$grailsVersion"
    }
    ...
}

并运行grails compile。在撰写本文时,1.0 是该插件的最新版本。请查看 Grails 插件门户,了解任何给定时间点的最新版本。

有了可用的插件,您就可以开始数据库迁移之旅了。正如我所说,生产数据库尚未创建。您可以使用dbCreate“update”值部署应用程序的初始版本,这会很好地工作。但出于我稍后将讨论的原因,我想鼓励您从数据库迁移变更日志(即迁移脚本)初始化数据库。别担心,这比您想象的要省力得多。

诀窍是使用插件的命令之一为我们生成迁移脚本。首先,您需要确保您的“prod”数据库为空,并删除该环境的任何dbCreate设置。然后运行

grails dbm-create-changelog
grails prod dbm-generate-gorm-changelog --add changelog-1.0.groovy

(现在是尝试新的 Grails 2 交互模式的好时机!)

上述操作将创建一个grails-app/migrations/changelog.groovy文件,它将成为您的父变更日志文件。第二个命令然后生成一个迁移脚本,grails-app/migrations/changelog-1.0.groovy,它将采用一个空数据库并为当前版本的应用程序创建适当的 schema。父变更日志也已更新,以包含这个新脚本。请注意,您的数据库将保持为空!

尽管 Liquibase 旨在实现数据库无关性,但最好在配置了适当类型数据源(通常是“production”)的 Grails 环境中运行各种生成和差异命令。这将确保您无需对生成的变更日志进行太多更改。

[标注标题=变更日志名称] 插件不会对变更日志文件名强制执行任何特定的命名约定。在本文中,我只是使用“changelog-”,因此每个变更日志都与应用程序的特定版本相关联。它运作良好。[/标注]

我们为什么要创建 1.0 变更日志,而不是使用“update”dbCreate设置?初始变更日志意味着您可以将应用程序部署到全新的空数据库。所有迁移都将按预期工作,因为它们将从已知、固定的 schema(即空 schema)按顺序运行。“update”设置的问题在于,它总是会创建一个与当前领域模型匹配的 schema。旧的变更日志根本不会运行,因为数据库不处于它们预期的状态!如果您愿意,仍然可以在应用程序的第一个版本中使用“update”,但上述方法意味着您有机会尽早测试您的初始变更日志。

此时,值得回顾一下changelog-1.0.groovy文件,看看迁移是什么样子,以及是否需要更改任何内容。您可能会看到添加了比您实际需要的更多的索引和约束,因此可能需要进行一些修剪。

好的,我们现在有一个迁移脚本了。但是当我们运行grails prod run-app应用程序将无法启动:数据库仍然为空。为什么?迁移脚本不会在启动时自动运行,这意味着您要么必须使用类似dbm-update的命令手动更新数据库,要么在启动时启用迁移执行。我更喜欢后者,特别是在许多情况下,您根本无法从 Grails 命令行访问生产数据库。所以,将这些设置添加到Config.groovy文件

grails.plugin.databasemigration.updateOnStart = true
grails.plugin.databasemigration.updateOnStartFileNames = ["changelog.groovy"]

现在,当您启动应用程序时,迁移脚本将运行,您的应用程序将正常工作!当您重新启动服务器时,插件将检测到迁移已运行,因此它们将被忽略。

后续更改

数据库迁移的全部意义在于,随着应用程序的发展,您可以更改数据库并控制该过程。现在想象一下,您已经对应用程序进行了一些工作,并希望发布一个新版本,也许是 1.0.1。要升级生产数据库,您需要为您的领域模型更改创建变更日志。您可以手动完成此操作,但通过使用数据库迁移插件的“diff”命令,您可以节省大量精力。

要执行差异,您需要一个数据库,插件可以将其与当前领域模型进行比较。同样,最好使用与生产环境中使用的相同类型的数据库。此外,数据库必须处于其原始状态,即在当前领域模型更改之前。换句话说,不要在启用了dbCreate = "update"的情况下运行您的应用程序!事实上,为应用程序的每个版本创建一个数据库转储是值得的,这样您就可以在不小心以这种方式更新数据库时进行回滚。

好了,警告就到此为止。让我们创建下一个变更日志

grails prod dbm-gorm-diff --add changelog-1.0.1.groovy

和以前一样,这将在迁移目录中创建一个changelog-1.0.1.groovy文件,并将其包含在父变更日志中。您还需要检查生成的变更日志并可能进行调整,但编辑现有文件比从头开始创建新文件要容易得多。就是这样!现在您可以将此变更日志与相应的领域类更改一起提交到版本控制。事实上,我建议您始终在同一个提交中包含领域类更改和相应的迁移脚本修改。

变更日志文件本身如下所示

databaseChangeLog = {
    changeSet(id: "UpdateDescriminatorForPluginTabs", author: "pledbrook") {
        update(tableName: "content") {
            column name: "class", value: "org.grails.plugin.PluginTab"
            where "title like 'description-%' and class = 'org.grails.wiki.WikiPage'"
        }
    }

    changeSet(id: "IssuesUrlForPlugins", author: "pledbrook") {
        addColumn(tableName: "plugin") {
            column name: "issues_url", type: "varchar(255)", {
                constraints nullable: true
            }
        }
    }
}

如您所见,它们只是变更集的集合,每个变更集都包含一些数据库重构。每个变更集都需要每个作者在每个变更日志中具有唯一的 ID,以便 Liquibase 可以跟踪它是否已应用(Liquibase 跟踪变更集,而不是变更日志!)。上面的示例演示了如何更新现有数据(id “UpdateDescriminatorForPluginTabs”)以及添加新列。其他支持的重构包括

  • 添加/重命名/修改列(schema 更改)
  • 添加索引
  • 添加/重命名/修改表
  • 添加/删除唯一约束

完整的重构范围在Liquibase 手册中有所描述,尽管所有示例都是 XML 格式。幸运的是,XML -> Groovy 的映射非常简单

  1. XML 元素 -> Groovy 方法
  2. 属性 -> 命名参数
  3. 嵌套元素 -> 闭包内的嵌套方法

最后,您可能想知道如何组织变更日志。您应该为每个重构设置一个变更集吗?还是为每个变更日志设置一个变更集?或者介于两者之间?这取决于您,但每个源代码控制提交一个变更集可能效果很好。换句话说,您为每个包含领域模型更改的提交创建一个变更集。或者,您可能希望每个数据库表一个变更集。做任何适合您的。

本文到此结束。正如您所看到的,适当的数据库迁移支持是任何生产数据库支持的 Web 应用程序的重要组成部分,因此我们现在拥有一个官方支持且功能强大的插件来满足这一需求,这是一个好消息。它只适用于关系型数据库(所以恐怕没有 Redis、MongoDB 等的迁移),但它仍然应该满足绝大多数 Grails 用户的需求。快去试试吧!

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看所有