Grails 数据库迁移
在本指南中,我们将学习如何使用 Grails 数据库迁移插件
作者:尼拉夫·阿萨、塞尔吉奥·德尔·阿莫
Grails 版本 3.3.1
1 Grails 训练
Grails 训练 - 由创建和积极维护 Grails 框架的成员开发和提交!
2 入门
在本指南中,你将学习如何使用 Grails 数据库迁移插件。我们将创建一个包含简单域类的应用程序,并扩展它们以实现以下目标
-
为数据库迁移设定基线
-
将某一列变为可空的
-
将列添加到现有表
-
重新设计表并迁移现有数据
2.1 你需要什么
若要完成本指南,你需要以下内容
-
一些空闲时间
-
一个合适的文本编辑器或 IDE
-
JDK 1.8 或更高版本,并配置好了
JAVA_HOME
2.2 如何完成指南
若要开始,请执行以下操作
-
下载并解压缩源代码
或
-
克隆 Git 存储库
git clone https://github.com/grails-guides/grails-database-migration.git
Grails 指南存储库包含两个文件夹
-
initial
初始项目。通常是带有少量附加代码的简单 Grails 应用,可让你抢先一步。 -
完成
已完成的示例。这是实施指南所演示的步骤并在initial
文件夹中应用这些更改的结果。
若要完成本指南,请转到 initial
文件夹
-
将
cd
转到
然后按照下一部分中的说明操作。
如果你将 cd 转到 grails-guides/grails-database-migration/complete ,则可以直接转到已完成的示例 |
3 编写应用程序
我们将编写一个涉及 Person
类的简单应用程序。Person
类最初有它自己的属性,其中还包含地址信息。在实现域的同时,我们将把属性分成它自己的 Address
域类。我们将使用 Grails Database Migration 插件来管理这些转换。
3.1 安装数据库
创建 MySQL 数据库
让我们设置一个带 MySQL 的物理数据库,而不是依赖默认的 H2 内存数据库。
-
转到 MySql 以安装数据库
-
使用
root
的 ID 和root
的密码创建你的管理员访问 -
打开 MySQL 命令行客户端
在命令行客户端中运行这些命令以创建和使用数据库。show tables
命令应返回一个空集合。
设置 Grails 以使用 MySQL 数据库
> create database dbmigration character set utf8 collate utf8_general_ci;
> use dbmigration;
> show databases;
> show tables;
现在,我们需要配置 Grails应用程序以指向新的 dbmigration
数据库。我们将编辑 build.gradle
和 application.yml
文件。
dependencies {
...
runtime 'mysql:mysql-connector-java:5.1.36'
...
}
dataSource:
pooled: true
jmxExport: true
driverClassName: com.mysql.jdbc.Driver
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
username: root
password: root
environments:
development:
dataSource:
dbCreate: none (1)
url: jdbc:mysql://localhost:3306/dbmigration?useUnicode=yes&characterEncoding=UTF-8
1 | dbCreate 定义了我们是否希望从域模型中自动生成数据库。我们将其设置为 none ,因为我们将使用 Grails Database Migration 插件管理数据库架构。 |
3.2 域类
在适当的包中创建一个域类
package demo
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
class Person {
String name
Integer age
}
3.3 安装数据库迁移插件
要安装 Grails Database Migration 插件,我们需要添加到 build.gradle
buildscript {
dependencies {
...
classpath 'org.grails.plugins:database-migration:3.0.3'
}
}
dependencies {
...
compile 'org.grails.plugins:database-migration:3.0.3'
compile 'org.liquibase:liquibase-core:3.5.3'
}
也要告诉 Gradle 有关迁移文件夹位置的信息
sourceSets {
main {
resources {
srcDir 'grails-app/migrations'
}
}
}
查看 Grails Database Migration 插件 文档。
3.4 数据库迁移实战
数据库迁移是对数据库进行的架构更改,同时保留现有数据。如果没有用于管理数据库迁移的工具,团队可能依赖手动 SQL,容易出错的通信流程和成本高昂的风险管理来实施解决方案。数据库迁移插件允许你管理对数据库进行的结构更改。它自动执行增量更改,使其可重复、可见和可跟踪。你可以将这些更改提交到源控制中。
使用插件涉及的一般工作流如下
基准
-
定义域的当前状态
-
使用 liquibase 从 changelog 创建数据库
-
在应用程序中设置配置选项以使用数据库迁移插件
开发工作流
-
对领域对象做出更改
-
使用此插件为数据库生成变更日志附加项
-
使用此插件更新数据库
3.5 数据库迁移基准
不再使用 GORM 模式自动生成,而是将数据库模式更改为 Liquibase;Grails 数据库迁移插件使用的数据库迁移工具。
我们希望在启动时运行迁移,而我们的迁移将位于 changelog.groovy
中
...
grails:
plugin:
databasemigration:
updateOnStart: true
updateOnStartFileName: changelog.groovy
...
此插件附带多个命令,其中一个命令为 dbm-generate-gorm-changelog
,它会从当前 GORM 类生成包含 Groovy DSL 文件的初始变更日志
> grails dbm-generate-gorm-changelog changelog.groovy
这将生成类似于以下内容的变更日志
databaseChangeLog = {
changeSet(author: "Nirav Assar (generated)", id: "1497549057046-1") {
createTable(tableName: "person") {
column(autoIncrement: "true", name: "id", type: "BIGINT") {
constraints(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'
}
应用迁移
$ grails dbm-update
数据库表已创建
> show tables;
dbmigration 中的表格 |
DATABASECHANGELOG |
DATABASECHANGELOGLOCK |
person |
表 DATABASECHANGELOG
和 DATABASECHANGELOGLOCK
由数据库迁移插件用于追踪数据库迁移。
表 person
对应于 Person
领域类。
> describe person;
字段 |
类型 |
Null |
主键 |
默认值 |
附加信息 |
id |
bigint(20) |
否 |
主键 |
<null> |
自动增量 |
version |
bigint(20) |
否 |
<null> |
||
age |
int(11) |
否 |
<null> |
||
name |
varchar(255) |
否 |
<null> |
3.6 将列设置为可为空
本节中,我们将进行简单的更改,将一个列设置为可为空。age
列当前需要一个值。我们将使其可为空,并继续迁移数据库以反映这一点。
在 Person
领域对象中,将 age 属性设置为可为空
package demo
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
class Person {
String name
Integer age
static constraints = {
age nullable: true
}
}
请注意,在领域对象中进行的更改不会影响数据库。我们必须生成 changelog.groovy
的附加项才能使更改生效。运行以下命令
> grails dbm-gorm-diff change-age-constraint-to-nullable.groovy --add
已向 changelog.groovy
中添加了新的 include 语句
databaseChangeLog = {
include file: 'create-person-table.groovy'
include file: 'change-age-constraint-to-nullable.groovy'
}
同时还创建了一个单独的变更集
databaseChangeLog = {
changeSet(author: "Nirav Assar (generated)", id: "1497551594095-1") {
dropNotNullConstraint(columnDataType: "int", columnName: "age", tableName: "person")
}
}
如果我们运行迁移
$ grails dbm-update
表 person
中的列 age
在约束中表示为可为空
字段 |
类型 |
Null |
主键 |
默认值 |
附加信息 |
id |
bigint(20) |
否 |
主键 |
<null> |
自动增量 |
version |
bigint(20) |
否 |
<null> |
||
age |
int(11) |
是 |
<null> |
||
name |
varchar(255) |
否 |
<null> |
3.7 添加属性
我们将在 Person
类中添加一些用于地址的属性,以演示我们的第二个迁移。
package demo
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
class Person {
String name
Integer age
String streetName
String city
String zipCode
static constraints = {
age nullable: true
streetName nullable: true
city nullable: true
zipCode nullable: true
}
}
如果我们针对这些更改生成一个变更集
> grails dbm-gorm-diff add-address-fields-to-person.groovy --add
该命令向 changelog.groovy
添加了一个新的 include 语句
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: "Nirav Assar (generated)", id: "1497552798178-1") {
addColumn(tableName: "person") {
column(name: "city", type: "varchar(255)")
}
}
changeSet(author: "Nirav Assar (generated)", id: "1497552798178-2") {
addColumn(tableName: "person") {
column(name: "street_name", type: "varchar(255)")
}
}
changeSet(author: "Nirav Assar (generated)", id: "1497552798178-3") {
addColumn(tableName: "person") {
column(name: "zip_code", type: "varchar(255)")
}
}
}
如果我们运行迁移
$ grails dbm-update
在 person
表中创建了新列 streetName
、city
和 zipCode
;
$ describe person
字段 |
类型 |
Null |
主键 |
默认值 |
附加信息 |
id |
bigint(20) |
否 |
主键 |
<null> |
自动增量 |
version |
bigint(20) |
否 |
<null> |
||
age |
int(11) |
是 |
<null> |
||
name |
varchar(255) |
否 |
<null> |
||
city |
varchar(255) |
是 |
<null> |
||
street_name |
varchar(255) |
是 |
<null> |
||
zip_code |
varchar(255) |
是 |
<null> |
3.8 重新设计表格
假设我们想重新设计Person
并将地址域拆分到其自己的域对象中。这样做的目的是,现在一个Person
可以拥有多个Address
。在执行此类型域对象重新设计时,我们必须考虑几个方面
-
将更改数据库表模式定义
-
表中的现有数据将必须拆分到创建的新数据库表中
-
我们可以在更改日志文件中编写自定义 SQL 来传输现有数据
下图描绘了重新设计
Person
和Address
域对象可以如下编码
package demo
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
class Person {
String name
Integer age
static hasMany = [addresses: Address]
static constraints = {
age nullable: true
}
}
package demo
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
class Address {
Person person
String streetName
String city
String zipCode
static belongsTo = [person: Person]
static constraints = {
streetName nullable: true
city nullable: true
zipCode nullable: true
}
}
运行数据库迁移命令,该命令将比较新的域对象与现有数据库并生成 liquibase 声明来迁移模式
> grails dbm-gorm-diff create-address-table.groovy –add
该命令向 changelog.groovy
添加了一个新的 include 语句
databaseChangeLog = {
include file: 'create-person-table.groovy'
include file: 'change-age-constraint-to-nullable.groovy'
include file: 'add-address-fields-to-person.groovy'
}
同时还创建了一个单独的变更集
changeSet(author: "Nirav Assar (generated)", id: "1497553930799-1") {
createTable(tableName: "address") {
column(autoIncrement: "true", name: "id", type: "BIGINT") {
constraints(primaryKey: "true", primaryKeyName: "addressPK")
}
column(name: "version", type: "BIGINT") {
constraints(nullable: "false")
}
column(name: "city", type: "VARCHAR(255)")
column(name: "person_id", type: "BIGINT") {
constraints(nullable: "false")
}
column(name: "street_name", type: "VARCHAR(255)")
column(name: "zip_code", type: "VARCHAR(255)")
}
}
changeSet(author: "Nirav Assar (generated)", id: "1497553930799-2") {
addForeignKeyConstraint(baseColumnNames: "person_id", baseTableName: "address", constraintName: "FK81ihijcn1kdfwffke0c0sjqeb", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "person")
}
changeSet(author: "Nirav Assar (generated)", id: "1497553930799-3") {
dropColumn(columnName: "city", tableName: "person")
}
changeSet(author: "Nirav Assar (generated)", id: "1497553930799-4") {
dropColumn(columnName: "street_name", tableName: "person")
}
changeSet(author: "Nirav Assar (generated)", id: "1497553930799-5") {
dropColumn(columnName: "zip_code", tableName: "person")
}
我们将手动添加一个变更集,以将现有数据从旧表移至新表。
create-address-table.groovy.groovy
的最终版本如下所示
changeSet(author: "Nirav Assar (generated)", id: "1497553930799-1") {
createTable(tableName: "address") {
column(autoIncrement: "true", name: "id", type: "BIGINT") {
constraints(primaryKey: "true", primaryKeyName: "addressPK")
}
column(name: "version", type: "BIGINT") {
constraints(nullable: "false")
}
column(name: "city", type: "VARCHAR(255)")
column(name: "person_id", type: "BIGINT") {
constraints(nullable: "false")
}
column(name: "street_name", type: "VARCHAR(255)")
column(name: "zip_code", type: "VARCHAR(255)")
}
}
changeSet(author: "Nirav Assar (generated)", id: "1497553930799-2") {
addForeignKeyConstraint(baseColumnNames: "person_id", baseTableName: "address", constraintName: "FK81ihijcn1kdfwffke0c0sjqeb", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "person")
}
(1)
changeSet(author: "a488338 (generated)", id: "migrate-person-data") {
sql("""insert into address (version, person_id, street_name, city, zip_code)
select 0, id, street_name, city, zip_code from person""")
}
changeSet(author: "Nirav Assar (generated)", id: "1497553930799-3") {
dropColumn(columnName: "city", tableName: "person")
}
changeSet(author: "Nirav Assar (generated)", id: "1497553930799-4") {
dropColumn(columnName: "street_name", tableName: "person")
}
changeSet(author: "Nirav Assar (generated)", id: "1497553930799-5") {
dropColumn(columnName: "zip_code", tableName: "person")
}
1 | 我们添加的变更集在创建Address 表后但在删除Person 表中的列之前执行。 |
运行迁移
$ grails dbm-update
person 表如下所示
$ describe person
字段 |
类型 |
Null |
主键 |
默认值 |
附加信息 |
id |
bigint(20) |
否 |
主键 |
<null> |
自动增量 |
version |
bigint(20) |
否 |
<null> |
||
age |
int(11) |
是 |
<null> |
||
name |
varchar(255) |
否 |
<null> |
address 表如下所示
$ describe address
字段 |
类型 |
Null |
主键 |
默认值 |
附加信息 |
id |
bigint(20) |
否 |
主键 |
<null> |
自动增量 |
version |
bigint(20) |
否 |
<null> |
||
person_id |
bigint(20) |
否 |
MUL |
<null> |
|
city |
varchar(255) |
是 |
<null> |
||
street_name |
varchar(255) |
是 |
<null> |
||
zip_code |
varchar(255) |
是 |
<null> |
4 摘要
总结本指南,我们学习了如何使用数据库迁移插件来更改列名称、添加列以及在迁移现有数据时重新设计表。请务必注意,数据库迁移包括典型的工作流
-
对域对象进行更改。
-
生成变更日志,这将识别现有数据库和已编辑域对象之间的数据库结构差异。
-
考虑要迁移的任何现有数据。
-
执行数据库迁移脚本。