显示导航

Grails 数据库迁移

在本指南中,我们将学习如何使用 Grails 数据库迁移插件

作者:普尼特·贝赫尔、尼拉夫·阿萨尔、塞尔吉奥·德尔·阿莫

Grails 版本:6.0.0-RC1

1 Grails 培训

Grails 培训 - 由创建并积极维护 Grails 框架的人员开发并提供!.

2 入门

在本指南中,您将学习如何使用 Grails 数据库迁移插件。我们将创建一个包含简单领域类的应用程序,并对其进行扩展以实现以下目标

  • 建立数据库迁移基准

  • 将列更改为可空

  • 向现有表中添加列

  • 重新设计表并迁移现有数据

2.1 所需内容

要完成本指南,您需要以下内容

  • 一些空闲时间

  • 出色的文本编辑器或 IDE

  • 安装并已正确配置 JAVA_HOME 的 JDK 1.8 或更高版本

2.2 指南完成方式

要开始,请执行以下操作

Grails 指南资源库包含两个文件夹

  • initial 初始项目。通常为一个简单的 Grails 应用程序,并包含一些额外代码,为您的入门提供帮助。

  • complete 已完成的示例。这是通过完成本指南提供的步骤,并将这些更改应用于 initial 文件夹而产生的结果。

要完成指南,请转到 initial 文件夹

  • grails-guides/grails-database-migration/initialcd

并按照下一部分中的说明进行操作。

如果您 cdgrails-guides/grails-database-migration/complete,则可以直接进入已完成示例

3 编写应用程序

我们将编写一个涉及 Person 类 的简单应用程序。Person 类最初将有自己的属性,其中还包含地址信息。随着我们进化领域,我们将属性拆分为自己的 Address 领域类。我们将使用 Grails 数据库迁移插件 来管理这些过渡。

3.1 安装数据库

创建一个 MySQL 数据库

让我们使用 MySql 设置一个物理数据库,而不是依赖于内存数据库中的默认 H2。

  • 转到 MySql 安装数据库

  • 使用 root 的 ID 和 root 的密码创建您的管理员访问权限

  • 打开 MySql 命令行客户端

在命令行客户端中运行这些命令以创建和使用数据库。show tables 命令应返回一个空集。

信息:请通读 MySQL 文档,以获取针对最新说明配置数据库用户的信息。

设置 Grails 以使用 MySQL 数据库

> CREATE USER 'devel'@'%' IDENTIFIED BY 's3cur3';
> CREATE DATABASE devDb character set utf8 collate utf8_general_ci;
> GRANT ALL ON data.* TO 'devel'@'%';
> USE devDb;
> SHOW DATABASES;
> SHOW TABLES;

现在我们需要配置 Grails 应用程序以指向新的 devDb 数据库。我们将编辑 build.gradleapplication.yml 文件,导出环境变量 MYSQL_USER 和 MYSQL_PASSWORD。

build.gradle
dependencies {
...
    runtimeOnly 'mysql:mysql-connector-java:5.1.36'
...
}
grails-app/conf/application.yml
dataSource:
    driverClassName: com.mysql.jdbc.Driver
    dialect: org.hibernate.dialect.MySQL5InnoDBDialect
    username: '${MYSQL_USER}'
    password: '${MYSQL_PASSWORD}'
    pooled: true
    jmxExport: true
environments:
    development:
        dataSource:
            dbCreate: none (1)
            url: jdbc:mysql://localhost:3306/devDb?useUnicode=yes&characterEncoding=UTF-8
$ export MYSQL_USER='devel'
$ export MYSQL_PASSWORD='s3cur3'
1 dbCreate 定义了我们是否希望从领域模型自动生成数据库。我们将其设置为 none,因为我们将使用 Grails 数据库迁移插件来管理数据库架构的管理。

3.2 领域类

在适当的包中创建一个领域类

grails-app/domain/grails/dbmigration/Person.groovy
Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=package]

Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=import]

Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=class]

Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=properties]

Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=closeClass]

3.3 安装数据库迁移插件

要安装 Grails 数据库迁移插件,我们需要添加到 build.gradle

build.gradle
buildscript {
   dependencies {
      ...
   }
}

dependencies {
    ...
    implementation 'org.grails.plugins:database-migration:4.2.0'
}

还要告诉 Gradle 迁移文件夹的位置。确保此配置在 dependencies 配置之前,以便文件夹声明生效

build.gradle
sourceSets {
    main {
        resources {
            srcDir 'grails-app/migrations'
        }
    }
}

签出 Grails 数据库迁移插件 文档。

3.4 数据库迁移的作用

数据库迁移是指在保留现有数据的同时对数据库进行架构更改。如果没有用于管理数据库迁移的工具,则团队可能依赖手动 SQL、容易出错的通信流程以及代价高昂的风险管理来实施解决方案。数据库迁移插件可帮助您管理对数据库所做的结构更改。它可以自动执行增量更改,使这些更改可重复、可见且可跟踪。您可以在源代码管理中提交这些更改。

使用该插件所涉及的通用工作流如下

基线

  1. 定义域的当前状态

  2. 使用 Liquibase 根据变更日志创建数据库

  3. 在应用程序中设置配置选项以使用数据库迁移插件

开发工作流

  1. 更改域对象

  2. 使用该插件为数据库生成变更日志新增项

  3. 使用该插件更新数据库

3.5 数据库迁移的基线

使用 Grails 数据库迁移插件的数据库迁移工具 Liquibase 将更改数据库架构,而不使用 GORM 架构自动生成。

我们希望在启动时运行迁移,我们的迁移将位于 changelog.groovy

grails-app/conf/application.yml
...
grails:
    plugin:
        databasemigration:
            updateOnStart: true
            updateOnStartFileName: changelog.groovy
...

该插件带有几个命令,其中一个命令 dbm-generate-gorm-changelog 使用一个 Groovy DSL 文件从当前 GORM 类生成一个初始变更日志

$ ./gradlew runCommand "-Pargs=dbm-generate-gorm-changelog changelog.groovy"

这将生成类似这样的变更日志

grails-app/migrations/changelog.groovy
databaseChangeLog = {

    changeSet(author: "behl (generated)", id: "1687423705657-1") {
        createTable(tableName: "person") {
            column(autoIncrement: "true", name: "id", type: "BIGINT") {
                constraints(nullable: "false", primaryKey: "true", primaryKeyName: "personPK")
            }

            column(name: "version", type: "BIGINT") {
                constraints(nullable: "false")
            }

            column(name: "age", type: "INT") {
                constraints(nullable: "false")
            }

            column(name: "name", type: "VARCHAR(255)") {
                constraints(nullable: "false")
            }
        }
    }
}

您可能会看到以下 INFO 日志语句。这不是一个错误

INFO 7/24/17 11:29 AM: liquibase: Can not use class org.grails.plugins.databasemigration.liquibase.GormDatabase as a
Liquibase service because it does not have a no-argument constructor

将初始变更日志移至其自身文件并从主变更日志文件引用它。

$ cp grails-app/migrations/changelog.groovy grails-app/migrations/create-person-table.groovy

使用以下内容替换 changelog.groovy 的内容

grails-app/migrations/changelog.groovy
databaseChangeLog = {
    include file: 'create-person-table.groovy'
}

应用迁移

$ ./gradlew runCommand "-Pargs=dbm-update"

创建数据库表

> SHOW TABLES;

dbmigration 中的表

DATABASECHANGELOG

DATABASECHANGELOGLOCK

person

数据库迁移插件使用表 DATABASECHANGELOGDATABASECHANGELOGLOCK 来跟踪数据库迁移。

person 表对应于 Person 域类。

> DESCRIBE person;

字段

类型

空值

默认值

额外

id

bigint(20)

主键

<null>

自增

version

bigint(20)

<null>

age

int(11)

<null>

name

varchar(255)

<null>

3.6 使列可接受空值

在本节中,我们将进行一个简单的更改,以使某个列可接受空值。目前,age 列需要一个值。我们使其可接受空值,并继续迁移数据库以反映这一点。

400

Person 域对象中,使 age 属性可接受空值

grails-app/domain/grails/dbmigration/Person.groovy
Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=package]

Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=import]

Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=class]

Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=properties]

Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=openConstraints]
Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=ageConstraints]
Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=closeConstraints]

Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=closeClass]

请注意,在域对象中进行更改并不会影响数据库。我们必须为 changelog.groovy 生成新增项,以使更改生效。运行以下命令

$ ./gradlew runCommand "-Pargs=dbm-gorm-diff change-age-constraint-to-nullable.groovy --add"

已在 changelog.groovy 中添加新包含语句

grails-app/migrations/changelog.groovy
databaseChangeLog = {
    include file: 'create-person-table.groovy'
    include file: 'change-age-constraint-to-nullable.groovy'
}

也创建了个别更改集

grails-app/migrations/change-age-constraint-to-nullable.groovy
databaseChangeLog = {

    changeSet(author: "behl (generated)", id: "1687423992173-1") {
        dropNotNullConstraint(columnDataType: "int", columnName: "age", tableName: "person")
    }
}

如果运行迁移

$ ./gradlew runCommand "-Pargs=dbm-update"

person 表中的 age 列是可空的,如约束中所示

字段

类型

空值

默认值

额外

id

bigint(20)

主键

<null>

自增

version

bigint(20)

<null>

age

int(11)

是的

<null>

name

varchar(255)

<null>

3.7 添加属性

我们将为 Person 类添加一些属性以表示我们的第二个迁移。

400
grails-app/domain/grails/dbmigration/Person.groovy
Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=package]

Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=import]

Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=class]

Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=properties]
    String streetName
    String city
    String zipCode

Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=openConstraints]
Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=ageConstraints]
        streetName nullable: true
        city nullable: true
        zipCode nullable: true
Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=closeConstraints]


Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=closeClass]

如果为这些更改生成一个更改集

$ ./gradlew runCommand "-Pargs=dbm-gorm-diff add-address-fields-to-person.groovy --add"

该命令在 changelog.groovy 中添加新包含语句

grails-app/migrations/changelog.groovy
databaseChangeLog = {
    include file: 'create-person-table.groovy'
    include file: 'change-age-constraint-to-nullable.groovy'
    include file: 'add-address-fields-to-person.groovy'
}

也创建了个别更改集

grails-app/migrations/add-address-fields-to-person.groovy
databaseChangeLog = {

    changeSet(author: "behl (generated)", id: "1687424134696-1") {
        addColumn(tableName: "person") {
            column(name: "city", type: "varchar(255)")
        }
    }

    changeSet(author: "behl (generated)", id: "1687424134696-2") {
        addColumn(tableName: "person") {
            column(name: "street_name", type: "varchar(255)")
        }
    }

    changeSet(author: "behl (generated)", id: "1687424134696-3") {
        addColumn(tableName: "person") {
            column(name: "zip_code", type: "varchar(255)")
        }
    }
}

如果运行迁移

$ ./gradlew runCommand "-Pargs=dbm-update"

新列 streetNamecityzipCodeperson 表中创建;

> describe person

字段

类型

空值

默认值

额外

id

bigint(20)

主键

<null>

自增

version

bigint(20)

<null>

age

int(11)

是的

<null>

name

varchar(255)

<null>

城市

varchar(255)

是的

<null>

街道名称

varchar(255)

是的

<null>

邮政编码

varchar(255)

是的

<null>

3.8 重新设计表

比方说我们希望通过将地址字段拆分为自己的域对象来重新设计 Person。此背后的想法是现在一个 Person 可以拥有多个 Address。在执行此类型的域对象重新设计时,我们必须考虑以下几个方面

  1. 数据库表架构定义将发生改变

  2. 表中的现有数据必须在创建的新数据库表之间进行拆分

  3. 我们可以在 changelog 文件中编写自定义 SQL 以传输现有数据

下图描绘了重新设计

400

PersonAddress 域对象可按如下方式编码

grails-app/domain/grails/dbmigration/Person.groovy
Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=package]

Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=import]

Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=class]

Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=properties]
Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=hasMany]

Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=openConstraints]
Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=ageConstraints]
Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=closeConstraints]


Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Person.groovy[tag=closeClass]
grails-app/domain/grails/dbmigration/Address.groovy
Unresolved directive in <stdin> - include::/home/runner/work/grails-database-migration/grails-database-migration/complete/grails-app/domain/grails/dbmigration/Address.groovy[indent=0]

运行数据库迁移命令,该命令将比较新域对象和现有数据库,并生成用于迁移架构的 Liquibase 声明

$ ./gradlew runCommand "-Pargs=dbm-gorm-diff create-address-table.groovy –add"

该命令在 changelog.groovy 中添加新包含语句

grails-app/migrations/changelog.groovy
databaseChangeLog = {
    include file: 'create-person-table.groovy'
    include file: 'change-age-constraint-to-nullable.groovy'
    include file: 'add-address-fields-to-person.groovy'
    include file: 'create-address-table.groovy'
}

也创建了个别更改集

grails-app/migrations/create-address-table.groovy

我们即将手动添加一个更改集,以将现有数据从旧表移动到新表。

create-address-table.groovy 的最终版本如下

grails-app/migrations/create-address-table.groovy
1 我们添加的更改集在创建 Address 表之后但在从 Person 表删除列之前执行。

运行迁移

$ ./gradlew runCommand "-Pargs=dbm-update"

人员表如下所示

> DESCRIBE person

字段

类型

空值

默认值

额外

id

bigint(20)

主键

<null>

自增

version

bigint(20)

<null>

age

int(11)

是的

<null>

name

varchar(255)

<null>

地址表如下所示

> DESCRIBE address

字段

类型

空值

默认值

额外

id

bigint(20)

主键

<null>

自增

version

bigint(20)

<null>

person_id

bigint(20)

MUL

<null>

城市

varchar(255)

是的

<null>

街道名称

varchar(255)

是的

<null>

邮政编码

varchar(255)

是的

<null>

4 总结

为了总结本指南,我们了解了如何使用数据库迁移插件来更改列名称、添加列并有可能在迁移现有数据的同时重新设计表。有必要指出的是,数据库迁移包括典型的工作流

  1. 对域对象进行更改。

  2. 生成 changelog,它将识别现有数据库和已编辑域对象之间的数据库结构差异。

  3. 考虑要迁移的任何现有数据。

  4. 执行数据库迁移脚本。

5 你需要关于 Grails 的帮助吗?

对象计算公司 (OCI) 赞助了此指南的创建。各种咨询和支持服务均已可用。

OCI 是 Grails 的总部

认识团队