显示导航

Grails 数据库迁移

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

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

Grails 版本 3.3.1

1 Grails 训练

Grails 训练 - 由创建和积极维护 Grails 框架的成员开发和提交!

2 入门

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

  • 为数据库迁移设定基线

  • 将某一列变为可空的

  • 将列添加到现有表

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

2.1 你需要什么

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

  • 一些空闲时间

  • 一个合适的文本编辑器或 IDE

  • JDK 1.8 或更高版本,并配置好了 JAVA_HOME

2.2 如何完成指南

若要开始,请执行以下操作

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.gradleapplication.yml 文件。

build.gradle
dependencies {
...
    runtime 'mysql:mysql-connector-java:5.1.36'
...
}
grails-app/conf/application.yml
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 域类

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

grails-app/domain/demo/Person.groovy
package demo

import grails.compiler.GrailsCompileStatic

@GrailsCompileStatic
class Person {

    String name
    Integer age

}

3.3 安装数据库迁移插件

要安装 Grails Database Migration 插件,我们需要添加到 build.gradle

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 有关迁移文件夹位置的信息

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

3.4 数据库迁移实战

数据库迁移是对数据库进行的架构更改,同时保留现有数据。如果没有用于管理数据库迁移的工具,团队可能依赖手动 SQL,容易出错的通信流程和成本高昂的风险管理来实施解决方案。数据库迁移插件允许你管理对数据库进行的结构更改。它自动执行增量更改,使其可重复、可见和可跟踪。你可以将这些更改提交到源控制中。

使用插件涉及的一般工作流如下

基准

  1. 定义域的当前状态

  2. 使用 liquibase 从 changelog 创建数据库

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

开发工作流

  1. 对领域对象做出更改

  2. 使用此插件为数据库生成变更日志附加项

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

3.5 数据库迁移基准

不再使用 GORM 模式自动生成,而是将数据库模式更改为 Liquibase;Grails 数据库迁移插件使用的数据库迁移工具。

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

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

此插件附带多个命令,其中一个命令为 dbm-generate-gorm-changelog,它会从当前 GORM 类生成包含 Groovy DSL 文件的初始变更日志

> grails dbm-generate-gorm-changelog changelog.groovy

这将生成类似于以下内容的变更日志

grails-app/migrations/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 的内容替换为

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

应用迁移

$ grails dbm-update

数据库表已创建

> show tables;

dbmigration 中的表格

DATABASECHANGELOG

DATABASECHANGELOGLOCK

person

DATABASECHANGELOGDATABASECHANGELOGLOCK 由数据库迁移插件用于追踪数据库迁移。

person 对应于 Person 领域类。

> describe person;

字段

类型

Null

主键

默认值

附加信息

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/demo/Person.groovy
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 语句

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: "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 类中添加一些用于地址的属性,以演示我们的第二个迁移。

400
grails-app/domain/demo/Person.groovy
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 语句

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: "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 表中创建了新列 streetNamecityzipCode

$ 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。在执行此类型域对象重新设计时,我们必须考虑几个方面

  1. 将更改数据库表模式定义

  2. 表中的现有数据将必须拆分到创建的新数据库表中

  3. 我们可以在更改日志文件中编写自定义 SQL 来传输现有数据

下图描绘了重新设计

400

PersonAddress域对象可以如下编码

grails-app/domain/demo/Person.groovy
package demo

import grails.compiler.GrailsCompileStatic

@GrailsCompileStatic
class Person {

    String name
    Integer age
    static hasMany = [addresses: Address]

    static constraints = {
        age nullable: true
    }


}
grails-app/domain/demo/Address.groovy
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 语句

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/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")
    }
    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的最终版本如下所示

grails-app/migrations/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 摘要

总结本指南,我们学习了如何使用数据库迁移插件来更改列名称、添加列以及在迁移现有数据时重新设计表。请务必注意,数据库迁移包括典型的工作流

  1. 对域对象进行更改。

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

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

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

5 你需要 Grails 的帮助吗?

Object Computing, Inc. (OCI) 赞助了本指南的创建。提供各种咨询和支持服务。

OCI 是 Grails 的所在地

认识团队