Grails 数据库迁移
在本指南中,我们将学习如何使用 Grails 数据库迁移插件
作者:普尼特·贝赫尔、尼拉夫·阿萨尔、塞尔吉奥·德尔·阿莫
Grails 版本:6.0.0-RC1
1 Grails 培训
Grails 培训 - 由创建并积极维护 Grails 框架的人员开发并提供!.
2 入门
在本指南中,您将学习如何使用 Grails 数据库迁移插件。我们将创建一个包含简单领域类的应用程序,并对其进行扩展以实现以下目标
-
建立数据库迁移基准
-
将列更改为可空
-
向现有表中添加列
-
重新设计表并迁移现有数据
2.1 所需内容
要完成本指南,您需要以下内容
-
一些空闲时间
-
出色的文本编辑器或 IDE
-
安装并已正确配置
JAVA_HOME
的 JDK 1.8 或更高版本
2.2 指南完成方式
要开始,请执行以下操作
-
下载并解压源代码
或
-
克隆 Git 资源库
git clone https://github.com/grails-guides/grails-database-migration.git
Grails 指南资源库包含两个文件夹
-
initial
初始项目。通常为一个简单的 Grails 应用程序,并包含一些额外代码,为您的入门提供帮助。 -
complete
已完成的示例。这是通过完成本指南提供的步骤,并将这些更改应用于initial
文件夹而产生的结果。
要完成指南,请转到 initial
文件夹
-
在
grails-guides/grails-database-migration/initial
中cd
并按照下一部分中的说明进行操作。
如果您 cd 到 grails-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.gradle
,application.yml
文件,导出环境变量 MYSQL_USER 和 MYSQL_PASSWORD。
dependencies {
...
runtimeOnly 'mysql:mysql-connector-java:5.1.36'
...
}
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 领域类
在适当的包中创建一个领域类
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
buildscript {
dependencies {
...
}
}
dependencies {
...
implementation 'org.grails.plugins:database-migration:4.2.0'
}
还要告诉 Gradle 迁移文件夹的位置。确保此配置在 dependencies
配置之前,以便文件夹声明生效
sourceSets {
main {
resources {
srcDir 'grails-app/migrations'
}
}
}
签出 Grails 数据库迁移插件 文档。
3.4 数据库迁移的作用
数据库迁移是指在保留现有数据的同时对数据库进行架构更改。如果没有用于管理数据库迁移的工具,则团队可能依赖手动 SQL、容易出错的通信流程以及代价高昂的风险管理来实施解决方案。数据库迁移插件可帮助您管理对数据库所做的结构更改。它可以自动执行增量更改,使这些更改可重复、可见且可跟踪。您可以在源代码管理中提交这些更改。
使用该插件所涉及的通用工作流如下
基线
-
定义域的当前状态
-
使用 Liquibase 根据变更日志创建数据库
-
在应用程序中设置配置选项以使用数据库迁移插件
开发工作流
-
更改域对象
-
使用该插件为数据库生成变更日志新增项
-
使用该插件更新数据库
3.5 数据库迁移的基线
使用 Grails 数据库迁移插件的数据库迁移工具 Liquibase 将更改数据库架构,而不使用 GORM 架构自动生成。
我们希望在启动时运行迁移,我们的迁移将位于 changelog.groovy
中
...
grails:
plugin:
databasemigration:
updateOnStart: true
updateOnStartFileName: changelog.groovy
...
该插件带有几个命令,其中一个命令 dbm-generate-gorm-changelog
使用一个 Groovy DSL 文件从当前 GORM 类生成一个初始变更日志
$ ./gradlew runCommand "-Pargs=dbm-generate-gorm-changelog 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 的内容
databaseChangeLog = {
include file: 'create-person-table.groovy'
}
应用迁移
$ ./gradlew runCommand "-Pargs=dbm-update"
创建数据库表
> SHOW TABLES;
dbmigration 中的表 |
DATABASECHANGELOG |
DATABASECHANGELOGLOCK |
person |
数据库迁移插件使用表 DATABASECHANGELOG
、DATABASECHANGELOGLOCK
来跟踪数据库迁移。
person
表对应于 Person
域类。
> DESCRIBE person;
字段 |
类型 |
空值 |
键 |
默认值 |
额外 |
id |
bigint(20) |
否 |
主键 |
<null> |
自增 |
version |
bigint(20) |
否 |
<null> |
||
age |
int(11) |
否 |
<null> |
||
name |
varchar(255) |
否 |
<null> |
3.6 使列可接受空值
在本节中,我们将进行一个简单的更改,以使某个列可接受空值。目前,age
列需要一个值。我们使其可接受空值,并继续迁移数据库以反映这一点。
在 Person
域对象中,使 age 属性可接受空值
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
中添加新包含语句
databaseChangeLog = {
include file: 'create-person-table.groovy'
include file: '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
类添加一些属性以表示我们的第二个迁移。
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
中添加新包含语句
databaseChangeLog = {
include file: 'create-person-table.groovy'
include file: 'change-age-constraint-to-nullable.groovy'
include file: '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"
新列 streetName
、city
、zipCode
在 person
表中创建;
> 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
。在执行此类型的域对象重新设计时,我们必须考虑以下几个方面
-
数据库表架构定义将发生改变
-
表中的现有数据必须在创建的新数据库表之间进行拆分
-
我们可以在 changelog 文件中编写自定义 SQL 以传输现有数据
下图描绘了重新设计
Person
和 Address
域对象可按如下方式编码
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]
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
中添加新包含语句
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'
}
也创建了个别更改集
我们即将手动添加一个更改集,以将现有数据从旧表移动到新表。
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 总结
为了总结本指南,我们了解了如何使用数据库迁移插件来更改列名称、添加列并有可能在迁移现有数据的同时重新设计表。有必要指出的是,数据库迁移包括典型的工作流
-
对域对象进行更改。
-
生成 changelog,它将识别现有数据库和已编辑域对象之间的数据库结构差异。
-
考虑要迁移的任何现有数据。
-
执行数据库迁移脚本。