Database Migration Pluginがすごい ~入門編~

ずっと放置していたけどDatabase Migration Pluginを触ってみた。

やばい。これは使わないと。

実際にアプリケーションを作りながら説明してく。とりあえずプロジェクトを作成。

$ grails create-app database-migration-test

作成したらBuildConfig.groovyを覗いてみる。

    plugins {
        ...
        runtime ":database-migration:1.1"
        ...
    }

database-migrationがデフォルトで入っているのがわかる。今回はDatabaseにPostgreSQLを使用するのでdependencyに依存関係を追加。

    dependencies {
        ....
        runtime 'postgresql:postgresql:9.1-901.jdbc4'
    }

次にDataSource.groovyを編集する。dbCreateはHibernateの自動DDL生成機能だがdatabase-migrationと競合する機能なため動かないようにする。

environments {
dataSource {
    pooled = true
    driverClassName = "org.postgresql.Driver"
    username = "test"
    password = ""
}
hibernate {
    cache.use_second_level_cache = false
    cache.use_query_cache = false
    cache.region.factory_class = 'net.sf.ehcache.hibernate.EhCacheRegionFactory'
}
// environment specific settings
environments {
    development {
        dataSource {
//            dbCreate = "create-drop" // one of 'create', 'create-drop', 'update', 'validate', ''
            url = "jdbc:postgresql://localhost:5432/devDb"
            pooled = true
        }
    }
    test {
        dataSource {
//            dbCreate = "update"
            url = "jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000"
        }
    }
    production {
        dataSource {
//            dbCreate = "update"
            url = "jdbc:postgresql://localhost:5432/prodDb"
            pooled = true
            properties {
               maxActive = -1
               minEvictableIdleTimeMillis=1800000
               timeBetweenEvictionRunsMillis=1800000
               numTestsPerEvictionRun=3
               testOnBorrow=true
               testWhileIdle=true
               testOnReturn=true
               validationQuery="SELECT 1"
            }
        }
    }
}

実際にやるときはDevとTestの扱いをどうするか戦略の余地がありますがとりあえず今回はこれで。またPostgreSQLを使用するので必要な箇所を書き換えた。

これで準備完了。実際に使っていく。インタラクティブモードで操作していくほうが効率が良いため、まずはインタラクティブモードを起動。

$ grails

はじめにデータベースの変更履歴を管理するchangelogを作成する。xmlかgroovyのDSLが使える。grailsで使うならgroovyを選択したくなるのが自然。changelogを作成する方法2種類。データベースの情報をもとに生成するか、GORMのドメインクラスを元に生成するか。

前者のコマンドが dbm-generate-changelog を使用し、後者は dbm-generate-gorm-changelog を使用する。ここではデータベースは空だし、ドメインもまだ何も作成していないので、どちらで実行しても変わりはないが、データベースの接続だけでも確認するという意味で dbm-generate-changelog を実行する(単に空ファイル作るだけなのdbm-create-changelogで本来は良い)。

grails> dev dbm-generate-changelog changelog.groovy

environmentとファイル名を指定して実行する(ここではどのDBも空なので何していしても一緒)。environmentは省略するとdevが使用される。ファイル名は拡張子が重要で、.groovyとつけるとGroovy DSLの定義ファイルになる。

うまくいくとgrails-app/migrations/changelog.groovyが生成されているはず。

databaseChangeLog = {
}

このようなファイルができてればOK。

changelogにまだ変更がない、データベースもまだ空。ということで変更履歴の同期がとれている。この同期がとれているという状態を作るのが dbm-changelog-sync というコマンド。

grails> dbm-changelog-sync

これを実行すると初回実行の場合はデータベースにdatabasechangelogに作られる。

$ psql -U test devDb                                                                                                                                                                                                          18:43:09
psql (9.0.7)
Type "help" for help.

devDb=> \d
               List of relations
 Schema |         Name          | Type  | Owner 
--------+-----------------------+-------+-------
 public | databasechangelog     | table | test
 public | databasechangeloglock | table | test
(2 rows)

devDb=> select * from databasechangelog;
 id | author | filename | dateexecuted | orderexecuted | exectype | md5sum | description | comments | tag | liquibase 
----+--------+----------+--------------+---------------+----------+--------+-------------+----------+-----+-----------
(0 rows)

devDb=> 

まだ中身は空だが、この中で変更履歴の反映が管理されていく。

そろそろ開発を進めていく。ドメインクラスを作る。

package org.yamkazu

class Book {

    String title

}

ドメインの開発が完了!これをデータベースに反映するchangelogを作成する。changelogを作成する方法はdbm-generate-changelog、dbm-generate-gorm-changelogを使用する方法が上で出てきたが、これらは初回のchangelogを作る際に既存のデータベースが存在していたり、既存のドメインをもとに一括生成する用途で、一度開発が始まった後は基本的にはdiffを使って前回との差分のchangelogを生成していく。

diffのコマンドは、dbm-diff、dbm-gorm-diffの2つがある。前者データベースの指定されたenviromentのDB同士の比較、後者はドメインとDBの比較が行える。今回はdbm-gorm-diffを使う。

grails> dbm-gorm-diff --add add-book.groovy

これの意味するところenviromentがdev(省略したので)のDBと、現在のドメインとの差分、それをadd-book.groovyに出力する。--addというオプションは出力されたファイルへの参照を起点となるchangelogファイルに追加してくれるというもの。

changelog.groovyを見てみる。

databaseChangeLog = {
    include file: 'add-book.groovy'
}

add-book.groovyが追加されていることがわかる。続いてadd-book.groovyを見てみる。

databaseChangeLog = {

    changeSet(author: "yamkazu (generated)", id: "1351331869483-1") {
        createTable(tableName: "book") {
            column(name: "id", type: "int8") {
                constraints(nullable: "false", primaryKey: "true", primaryKeyName: "bookPK")
            }

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

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

    changeSet(author: "yamkazu (generated)", id: "1351331869483-2") {
        createSequence(sequenceName: "hibernate_sequence")
    }

}

Bookドメインを反映するchangesetが定義されており、1351331869483-1と1351331869483-2のchangesetがある。

changesetが出来たので、これをデータベースに反映する。changesetを反映するにはdbm-updateを使用する。

grails> dbm-update

反映が終わったらデータベースを確認してみる。

devDb=> \d
                 List of relations
 Schema |         Name          |   Type   | Owner 
--------+-----------------------+----------+-------
 public | book                  | table    | test
 public | databasechangelog     | table    | test
 public | databasechangeloglock | table    | test
 public | hibernate_sequence    | sequence | test
(4 rows)

devDb=> select * from databasechangelog;
       id        |       author        |    filename     |         dateexecuted          | orderexecuted | exectype |               md5sum               |   description   | comments | tag | liquibase 
-----------------+---------------------+-----------------+-------------------------------+---------------+----------+------------------------------------+-----------------+----------+-----+-----------
 1351331869483-1 | yamkazu (generated) | add-book.groovy | 2012-10-27 19:07:44.890237+09 |             1 | EXECUTED | 3:378572087c807b0eae512c8e1bac00b7 | Create Table    |          |     | 2.0.5
 1351331869483-2 | yamkazu (generated) | add-book.groovy | 2012-10-27 19:07:44.907147+09 |             2 | EXECUTED | 3:1f8c97c0685b3c4f68b4ac2954ed9919 | Create Sequence |          |     | 2.0.5
(2 rows)

bookテーブルとシーケンスが生成されて、反映したchangesetがdatabasechangelogに記録されている。この様に適用したchangesetを記録するため、もう一度dbm-updateを実行しても、同じidのchangesetが実行されない。

開発を続ける。Authorドメインを作成する。

package org.yamkazu

class Author {

    String name

}

BookからもAuthorへの関連も追加。

class Book {

    Author author

    String title

}

先ほどと同じように dbm-gorm-diff を使う。

grails> dbm-gorm-diff --add add-author.groovy

add-author.groovyはこんなん。createTableやらbookへのaddColumnがあったりする。

databaseChangeLog = {

    changeSet(author: "yamkazu (generated)", id: "1351333217527-1") {
        createTable(tableName: "author") {
            column(name: "id", type: "int8") {
                constraints(nullable: "false", primaryKey: "true", primaryKeyName: "authorPK")
            }

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

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

    changeSet(author: "yamkazu (generated)", id: "1351333217527-2") {
        addColumn(tableName: "book") {
            column(name: "author_id", type: "int8") {
                constraints(nullable: "false")
            }
        }
    }

    changeSet(author: "yamkazu (generated)", id: "1351333217527-3") {
        addForeignKeyConstraint(baseColumnNames: "author_id", baseTableName: "book", constraintName: "FK2E3AE9B2F6003C", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "author", referencesUniqueColumn: "false")
    }

}

changesetを反映する。

grails> dbm-update

DBを確認

devDb=> \d
                 List of relations
 Schema |         Name          |   Type   | Owner 
--------+-----------------------+----------+-------
 public | author                | table    | test
 public | book                  | table    | test
 public | databasechangelog     | table    | test
 public | databasechangeloglock | table    | test
 public | hibernate_sequence    | sequence | test
(5 rows)

devDb=> select * from databasechangelog;
       id        |       author        |     filename      |         dateexecuted          | orderexecuted | exectype |               md5sum               |        description         | comments | tag | liquibase 
-----------------+---------------------+-------------------+-------------------------------+---------------+----------+------------------------------------+----------------------------+----------+-----+-----------
 1351331869483-1 | yamkazu (generated) | add-book.groovy   | 2012-10-27 19:07:44.890237+09 |             1 | EXECUTED | 3:378572087c807b0eae512c8e1bac00b7 | Create Table               |          |     | 2.0.5
 1351331869483-2 | yamkazu (generated) | add-book.groovy   | 2012-10-27 19:07:44.907147+09 |             2 | EXECUTED | 3:1f8c97c0685b3c4f68b4ac2954ed9919 | Create Sequence            |          |     | 2.0.5
 1351333217527-1 | yamkazu (generated) | add-author.groovy | 2012-10-27 19:27:06.174495+09 |             3 | EXECUTED | 3:500d2782c8ddcc7d7b89b0b29b2d7342 | Create Table               |          |     | 2.0.5
 1351333217527-2 | yamkazu (generated) | add-author.groovy | 2012-10-27 19:27:06.190441+09 |             4 | EXECUTED | 3:d4dcaaa9285f777d2a05f33e275488b7 | Add Column                 |          |     | 2.0.5
 1351333217527-3 | yamkazu (generated) | add-author.groovy | 2012-10-27 19:27:06.200821+09 |             5 | EXECUTED | 3:77e6c768bd70db135e30edfb2ef6788e | Add Foreign Key Constraint |          |     | 2.0.5
(5 rows)

dbm-updateを使うのがめんどくさかったり、自動でやりたい場合はConfig.groovyに以下の設定を追加することでアプリケーションの起動時に自動反映させることが出来る。

grails.plugin.databasemigration.updateOnStart = true
grails.plugin.databasemigration.updateOnStartFileNames = ["changelog.groovy"]

updateOnStartをtrueにすることでアプリケーションの自動的にupdateOnStartFileNamesに指定されたファイルを反映する。updateOnStartFileNamesはgrails-app/migrationsからの相対パスを指定する。warとして動作する場は空気読んで_Events.groovyでWEB-INF/classes/migrationに置換してくれているから動作環境を心配する必要はない。

そろそろ開発を終えてプロダクション環境にデプロイしてみる。せっかくなのでwarデプロイする。

grails> prod war

生成されたwarをtomcatにデプロイする。

Databaseを確認。さきほどのdevDbではなくprodDbのほう。

$ psql -U test prodDb                                                                                                                                                                                                         20:09:05
psql (9.0.7)
Type "help" for help.

prodDb=> \d
                 List of relations
 Schema |         Name          |   Type   | Owner 
--------+-----------------------+----------+-------
 public | author                | table    | test
 public | book                  | table    | test
 public | databasechangelog     | table    | test
 public | databasechangeloglock | table    | test
 public | hibernate_sequence    | sequence | test
(5 rows)

prodDb=> select * from databasechangelog;
       id        |       author        |     filename      |         dateexecuted          | orderexecuted | exectype |               md5sum               |        description         | comments | tag | liquibase 
-----------------+---------------------+-------------------+-------------------------------+---------------+----------+------------------------------------+----------------------------+----------+-----+-----------
 1351331869483-1 | yamkazu (generated) | add-book.groovy   | 2012-10-27 20:00:41.822337+09 |             1 | EXECUTED | 3:378572087c807b0eae512c8e1bac00b7 | Create Table               |          |     | 2.0.5
 1351331869483-2 | yamkazu (generated) | add-book.groovy   | 2012-10-27 20:00:41.854293+09 |             2 | EXECUTED | 3:1f8c97c0685b3c4f68b4ac2954ed9919 | Create Sequence            |          |     | 2.0.5
 1351333217527-1 | yamkazu (generated) | add-author.groovy | 2012-10-27 20:00:41.870867+09 |             3 | EXECUTED | 3:500d2782c8ddcc7d7b89b0b29b2d7342 | Create Table               |          |     | 2.0.5
 1351333217527-2 | yamkazu (generated) | add-author.groovy | 2012-10-27 20:00:41.884293+09 |             4 | EXECUTED | 3:d4dcaaa9285f777d2a05f33e275488b7 | Add Column                 |          |     | 2.0.5
 1351333217527-3 | yamkazu (generated) | add-author.groovy | 2012-10-27 20:00:41.900688+09 |             5 | EXECUTED | 3:77e6c768bd70db135e30edfb2ef6788e | Add Foreign Key Constraint |          |     | 2.0.5
(5 rows)

prodDb=> 

ちゃんと自動適用されている!
長くなってきたので今日はこのへんで!