Show Navigation

Grails 数据库迁移

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

作者: Nirav Assar、Sergio del Amo

Grails 版本 4.0.1

1 Grails 培训

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

2 入门

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

  • 对数据库迁移进行数据库基准测试

  • 将列更改为可为空

  • 向现有表格添加列

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

2.1 您需要了解的内容

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

  • 留出一些时间亲自动手

  • 一款像样的文本编辑器或 IDE

  • 已安装 JDK 1.8 或更高版本,且相应地配置了 JAVA_HOME

2.2 如何完成指南

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

Grails 指南存储库包含两个文件夹

  • initial 初始项目。通常是一个简单的 Grails 应用程序,其中包含一些附加代码,可以帮助您快速入门。

  • 已完成已完成的示例。这是按照指南呈现的步骤操作并且将这些更改应用到initial文件夹的结果。

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

  • cd转换为grails-guides/grails-database-migration/initial

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

如果你将cd转换为grails-guides/grails-database-migration/complete,则可以直接转到已完成的示例

3 编写应用程序

我们要编写一个涉及 Person 类的简单应用程序。Person 类最初将拥有其自身属性,这些属性还包含地址信息。随着域的发展,我们将会将其属性分成其自身的 Address 域类。我们将使用 Grails Database Migration Plugin管理这些转换。

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 Plugin 来管理数据库架构的管理。

3.2 域类

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

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

import grails.compiler.GrailsCompileStatic

@GrailsCompileStatic
class Person {

    String name
    Integer age

}

3.3 安装数据库迁移插件

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

build.gradle
buildscript {
   dependencies {
      ...
        classpath 'org.grails.plugins:database-migration:3.1.0.RC1'
   }
}

dependencies {
    ...
    compile 'org.grails.plugins:database-migration:3.1.0.RC1'
    compile 'org.liquibase:liquibase-core:3.6.1'
}

也要告诉 Gradle 有关迁移文件夹位置的信息。确保此配置早于 dependencies 配置,以使文件夹声明生效

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

查看 Grails 数据库迁移插件 文档。

3.4 数据库迁移付诸实践

数据库迁移是对数据库执行架构变动,同时保留现有数据。如果没有管理数据库迁移的工具,团队可能依赖于手动 SQL、极易出错的通信流程以及昂贵的风险管理,来实施解决方案。使用此数据库迁移插件,你可以管理对数据库进行的结构化更改。它可以自动执行渐进式更改,使其可重复、不可见和可跟踪。你可以将这些更改提交到源代码管理中。

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

基准

  1. 定义域的当前状态

  2. 使用 liquibase 从更改日志创建数据库

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

开发工作流

  1. 修改域对象

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

  3. 使用插件更新数据库

3.5 数据库迁移基准

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

我们要在启动时运行迁移,而迁移将位于 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

Database Migration 插件使用表为 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/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 列在约束中表示为可空的

字段

类型

空值

默认值

额外

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

该命令将一条新的 include 语句添加到 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: "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

字段

类型

空值

默认值

额外

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. 可以在变更日志文件中编写自定义 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

该命令将一条新的 include 语句添加到 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
    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 的最终版本如下所示

grails-app/migrations/create-address-table.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

字段

类型

空值

默认值

额外

id

bigint(20)

<null>

自动增加

version

bigint(20)

<null>

age

int(11)

<null>

name

varchar(255)

<null>

address 表如下所示

$ 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. 生成变更日志,它将识别现有数据库和已编辑域对象之间的数据库结构差异。

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

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

5 您需要 Grails 的帮助吗?

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

OCI 是 Grails 的家园

见见团队