Grails 2.0 倒计时:数据库迁移

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

Grails 的众多优秀特性之一就是它能够根据您的领域模型自动创建数据库模式。诚然,这是 Grails 使用的 Hibernate 的一个特性,但它仍然可以帮助您快速开始使用数据库驱动的 Web 应用程序,而无需担心数据库模式。

当您的应用程序迁移到生产环境时会发生什么?在开发过程中,服务器运行之间丢失数据并不是什么大问题。但是您不能在生产环境中简单地删除数据库。因此,这排除了dbCreate数据源设置的“create”和“create-drop”值。那么“update”呢?它不会清除数据库中的数据,因此经常被人们使用。然而,它不适用于生产环境,因为它存在明显的限制。例如,它无法处理简单的列重命名,当然也无法处理现有数据的修改,而这可能是升级的必要部分。虽然使用dbCreate = "update"部署到生产环境很诱人,但这通常是错误的解决方案。

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

幸运的是,存在一个灵活的、与数据库无关的工具用于执行模式迁移:Liquibase。更好的是,有两个 Grails 插件可以使使用 Liquibase 比其他情况更容易:Liquibase 插件Autobase

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

简而言之

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

  • 声明式数据库模式和数据迁移
  • 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,它将获取一个空数据库并为应用程序的当前版本创建相应的模式。父变更日志也会更新以包含这个新的变更日志。请注意,您的数据库将保持为空!

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

[边栏 标题=变更日志名称] 插件不会强迫您对变更日志文件名使用任何特定的命名约定。在本文中,我只是使用“changelog-”,因此每个变更日志都与应用程序的特定版本相关联。效果非常好。[/边栏]

为什么我们创建了一个 1.0 变更日志而不是使用“update”dbCreate设置?初始变更日志意味着您可以获取您的应用程序并将其部署到一个新的空数据库中。所有迁移都将按预期工作,因为它们将按顺序从已知的固定模式(即空模式)运行。“update”设置的问题在于它始终会创建一个与当前领域模型匹配的模式。旧的变更日志根本无法运行,因为数据库不在它们期望的状态!如果您愿意,您仍然可以使用“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”)以及如何添加新列。其他受支持的重构包括

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

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

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

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

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

获取 Spring 时事通讯

与 Spring 时事通讯保持联系

订阅

抢先一步

VMware 提供培训和认证,助您快速提升技能。

了解更多

获取支持

Tanzu Spring 通过一个简单的订阅提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

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

查看全部