[Grails]データベースマイグレーション

  • 2
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

概要

下書きが溜まってしまったのでとりあえず投稿。そのうち文章とか修正予定。

Grailsでは開発段階では基本的にDBのテーブルはGrails自信が起動時/終了時にcreate/dropします。
しかし、いざ本番環境で稼働し始めた後や、すでにテストに必要なテストデータが大量に格納されている場合、開発環境の起動時に毎回dropされたりcreateされたりするのは非常に不味いです。

そこで、Grailsに標準で添付されているマイグレーションツールを利用します。
マイグレーションツールを利用することで、データベースのテーブル構造に関す変更の差分のみ(ドメインに完する変項)を、Grailsで管理、適用することが出来るようになります。

なお、今回はPostgreSQLを使って試しています。
実際にコードを実行して試される場合は、GrailsからPostgreSQLにアクセスできるようにしておいてください。

初回の準備

Grailsのマイグレーションツールは、全てのドメイン/テーブルに関する変更をchangelogという名前で管理します。
さらに、そのchagnelogの中はchangeSetという単位で変更の塊を管理しています。
changelogXMLベースで管理するのかGroovyベースで管理するのか決めましょう。
と言っても、変更履歴を管理するファイル名の拡張子を.groovyにするか.xmlにするかの違いだけ。
今回は全てGroovyベースで進めます。

あと、DataSource.groovyの各環境のdataSourceはコメントアウトしておいてください。

ただし、初回実行時の状況によって以下の3パターンが有ります。

変更を管理するファイルchangelogの作成

パターン1:ドメインも実際のデータベースもまだ何もない状態。

とても簡単。以下のコマンドのどっちでもいいから実行。

テーブルの情報を元にする。
grails dbm-generate-changelog changelog.groovy
ドメイン(GORM)の情報を元にする。
grails dbm-generate-gorm-changelog changelog.groovy

パターン2:すでに既存のGORMがある場合

ドメイン(GORM)の情報を元にする。
grails dbm-generate-gorm-changelog changelog.groovy

パターン3:すでに既存のテーブルがある場合。

テーブルの情報を元にする。
grails dbm-generate-changelog changelog.groovy

実行結果

今回は完全に空っぽなパターン1の状態で、とりあえずどっちでもいいのでコマンドを実行します。

changelogの作成
grails dbm-generate-changelog changelog.groovy

すると、grails-app/migrations/changelog.groovyというファイルが生成されます。
パターン1の場合、中身は空っぽです。(以下の通り)

grails-app/migrations/changelog.groovy
databaseChangeLog = {
}

マイグレーション用テーブルの生成

dbm-changelog-syncというコマンドを実行します。

インタラクティブモードで実行
grails> dbm-changelog-sync
| Finished dbm-changelog-sync
grails> 

このコマンドによって、PostgreSQLの該当データベースに2つのテーブルが作成されました。

dbmigration=# \d
               List of relations
 Schema |         Name          | Type  | Owner 
--------+-----------------------+-------+-------
 public | databasechangelog     | table | koji
 public | databasechangeloglock | table | koji
(2 rows)

このコマンドは本来、changelogで管理されている変更を、全て適用済みだよ、と一括処理するためのコマンドです。詳細は最後の方で別途記述します。
この生成された2つのテーブルが、このマイグレーションツールの情報を管理してくれるテーブルです。
今回のパターン1の場合、databasechangelogテーブルの中身は今の所空っぽです。
databasechangeloglockテーブルの方は1件レコードが有ります。

dbmigration=# select * from databasechangeloglock ;
 id | locked | lockgranted | lockedby 
----+--------+-------------+----------
  1 | f      |             | 
(1 row)

が、まぁ今の所無視しちゃいましょう。

準備完了

これで、データベースの変更が安全安心に管理できるようになりました。
念の為確認しましょう。

  1. grails-app/migrations/changelog.groovyというファイルは有りますか?
  2. databasechangelogというテーブルがありますか?
  3. databasechangeloglockというテーブルが有りますか?

この3点がOKなら大丈夫です。
では早速使ってみましょう。

でもその前に...注意

Grailsのインタラクティブモードでマイグレーション用コマンドを実行すると更新されなかったりして混乱します。
これは、Grailsのインタラクティブモードでドメインのキャッシュなどを保持しているためだと思われます。
以降、dbm-*コマンドは全て通常のLinux/Macのコンソールから行うようにしてください。

実際に試してみる

とりあえずPersonというドメインを生成してみます。

インタラクティブモードでドメインの作成
grails> create-domain-class Person
| Created file grails-app/domain/dbmigration/Person.groovy
| Created file test/unit/dbmigration/PersonSpec.groovy
grails> 

Person.groovyの中身を適当に以下のような感じに編集

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

class Person {

    String name
    Integer age
    static constraints = {
    }
}

差分の記録(dbm-gorm-diff)

現在、当然のことながらドメイン/GORMの内容と実際のテーブルには差分が有りますね。(そもそもテーブルがまだない)
この状態で、マイグレーションツールにこの差分情報を記録してもらいます。
そのために、dbm-gorm-diffというコマンドを使います。
以下のような感じです。

grails dbm-gorm-diff 2014-11-14-person-added.groovy --add

すると、引数で指定したファイル名が、grails-app/migrations/2014-11-14-person-added.groovyとして保存されます。
そのファイルの中に、実際のGORMとデータベースの差分が書き込まれます。
また、初回に作成したgrails-app/migrations/changelog.groovyに、このファイルのinclude文が追記されます。

差分の適用(dbm-update)

それでは引き続きgrails dbm-updateというコマンドを事項してみてください。

grails dbm-update

エラーなくコマンドが終了したら、実際にデータベースを確認してみましょう。
なんと!ちゃんとpersonテーブルが生成されていますね!

すでにこの変更(2014-11-14-person-added.groovy)は適用されたとちゃんと記憶されているので、今後何度grails dbm-updateを実行しても再実行されることはありません。
この情報の管理先が、最初に生成したdatabasechangelogテーブルです。

デプロイ

本番環境にアプリケーションをデプロイする場合、データベースの変更はどうなるんでしょうか?
通常のWEBアプリケーションではプログラムのデプロイと、テーブルに対する変更を別々に行う必要が有ります。
しかしGrailsのdbmigrationプラグインはそれらを自動化してくれます。

まず、grails-app/conf/Config.groovyの末尾に

grails-app/conf/Config.groovy
grails.plugin.databasemigration. updateOnStart = true
grails.plugin.databasemigration. updateOnStartFileNames = ['changelog.groovy']

を追記します。
そしてwarを生成します。

普通にwarを生成
grails war

これでTomcatなどのサーブレットコンテナにwarを適切にデプロイすれば自動的に本番データベースのテーブルの変更までdbmigrationが自動で行ってくれます!便利!

軽いまとめ

今後、実際に運用を進めていく場合は、

  1. ドメイン/GORMの修正
  2. db-gorm-diffコマンドで差分の記録
  3. db-updateコマンドで差分をデータベースに適用
  4. しっかりテストする。
  5. 1から4を必要な分繰り返した後、本番にデプロイする

の繰り返しです。
とても簡単ですね!

dbm-gorm-diffで--addを忘れちゃった!

dbm-gorm-diffコマンドで、--addオプションを付けずに実行すると、単純に生成されたファイル名がデフォルトのchangelog.groovyの中でincludeされません。
そんな状態でdbm-updateを実行しても、マイグレーションツールはchangelog.groovyの中身を元に必要はファイルを検索しているので、当然何も変化しません。
そんなどうすればいいんだ。。。。
大丈夫。指定した名前(差分のGroovyファイル)は普通に作成されているはずなので、単純にchangelog.groovyの、include文が追記されていっている部分の末尾に、そのファイルのinclude文を追記してあげましょう。
その後、再度grails dbm-upateを実行すればちゃんと動きます。

開発環境のDBから、テスト用のデータベースを作成する。

超簡単。
まずは当然Datasource.groovyから、テストDB用のサーバ、データベースに接続できるようにしておく。
完了したら後はgrails test dbm-updateと実行するだけ。
データベースマイグレーションで管理されている情報(つまりgrails-app/migrations/changelog.groovy)を元に、まだテスト用データベースに適用されていない全ての変更が実行されます。
後はテスト用のデータをテスト用に登録するだけ。

テーブルから変更する。

似て非なるもの?な感じのするdbm-diffというコマンドがあります。
コレはもうドメイン(GORM)は関係ありません。
単純に、ある実行環境と、ある別の実行環境の間でテーブルに差分があるかどうかを確認/記録するためのものです。
例えば開発(デフォルトのdev)とテスト(test)のテーブル間で差分があるかどうか確認できます。

ではまず、開発環境のperosnテーブルにSQLでカラムを追加してみる。

dbmigration=# ALTER TABLE person ADD add_from_pgsql varchar(255);
ALTER TABLE
dbmigration=# 
dbmigration=# select * from person;
 id | version | age | name | add_from_pgsql 
----+---------+-----+------+----------------
(0 rows)

追加できました。
すでにテスト環境のデータベースを上で作成していますので、それとこの開発環境のデータベースをdbm-diffを使って比べてみます。

grails dev dbm-diff testとういコマンドを時刻した場合の実行結果は以下のとおりです。(見やすいように余分な部分を削除しています。)
余談ですが、dbm-diffでもdbm-gorm-diffでも、出力するファイル名を指定しない場合、XMLとしてコンソールに表示されます。どういった内容の差分があるのか確認したいだけの時に便利です。

grails dev dbm-diff testの実行結果
<changeSet author="k-kuwana (generated)" id="1415977568511-1">
    <addColumn tableName="person">
        <column name="add_from_pgsql" type="VARCHAR(255)"/>
    </addColumn>
</changeSet>

今度は試しに実行環境の指定を反対にしてgrails test dbm-diff devとしてみます。

grails test dbm-diff devの実行結果
<changeSet author="k-kuwana (generated)" id="1415977601826-1">
    <dropColumn columnName="add_from_pgsql" tableName="person"/>
</changeSet>

それぞれ差分がちゃんと表示されていますね。
このことから、最初に指定する環境から見て、2個めに指定する環境のテーブルがどうなっているか、という条件になっていることがわかります。

では、開発環境が正のものとして進めましょう。
当然diffの結果を保存する必要が有ります。

diffの結果を保存(--addオプションを忘れずに)
grails dev dbm-diff test sqlbaseadded.groovy --add

と実行して、

変更を適用
grails test dbm-update

と実行します。

テストデータベースのperseonテーブルを確認してみます。

dbmigrationtest=# select * from person;
 id | version | age | name | add_from_pgsql 
----+---------+-----+------+----------------
(0 rows)

ちゃんと開発用のデータベースにSQLベースで追加したカラムがテスト環境の方にも適用されましたね。

ここで注意!!!

今後作業を続けて行く前に注意です。
テスト環境に対してはマイグレーションを実行したのですでに最新のものになっていますが、開発環境はどうでしょう?
テーブルが新しくなっていて、マイグレーションツールはそのための差分(ADD COLUMNSのSQL)を保持しています。
しかし開発環境用のマイグレーション用テーブルにはその事実が記録されていません。
だってそもそも開発環境向けにはまだdbm-updateを実行していないので。
じゃあ実行してみましょう。

dbm-upateの実行をすると。。。?
kkuwana% grails dbm-update

| Starting dbm-update for database koji @ jdbc:postgresql://127.0.0.1:5432/dbmigration
| Error 2014-11-14 16:32:10,991 [main] ERROR liquibase  - Change Set sqlbaseadded.groovy::1415978760219-1::k-kuwana (generated) failed.  Error: Error executing SQL ALTER TABLE person ADD add_from_pgsql VARCHAR(255): ERROR: column "add_from_pgsql" of relation "person" already exists
Message: Error executing SQL ALTER TABLE person ADD add_from_pgsql VARCHAR(255): ERROR: column "add_from_pgsql" of relation "person" already exists
以下省略

なんかエラーが出てしまいます。
というのは、まだマイグレーション用テーブルに、今回生成したsqlbaseadded.groovyが実行されたよ、というログが存在していないため、dbm-updateは当然このファイルを実行しようとします。
でもそもそも開発環境用のpersonテーブルに対してSQLでカラムを追加しているので、当然すでに同名のカラムがあるよ、というエラーが表示されているわけですね。

このままだと、今後どんなに変更をしていったとしてもこのエラーのせいでdbm-updateを実行することが出来ません。
じゃどうするんだ!とう言うことで、初回に1回だけ実行したdbm-changelog-syncを実行します。
すると、changelog.groovyでincludeされているファイルの中で、まだ実行されていない物がある場合には、実際には実行せずに、実行済みだよ、とマークしてくれます。

実際に実行してみましょう。
grails dev dbm-changelog-sync
で、再度dbm-updateを試してみます。
grails dev dbm-update
エラーが出ずに終了しましたね。

*混乱を避けるため、grailsコマンドの後に明示的に環境(dev)を指定していますが、省略すればデフォルトでdev環境になるので、今回の場合であれば省略してもOKです。

dbm-changelog-syncの正体

さて、マイグレーションツールは、全ての変更をchangelog.groovyで管理しています。
そして、その変更(changelog.groovyの中身)が、データベースに対して実行されたかどうかはdatabasechangelogテーブルが管理しています。

dbm-changelog-syncコマンドは、changelog.groovyで管理されている変更のうち、まだ実行されていない変更を、単純にdatabasechangelogテーブルに実行済み、としてINSERTします。
やっていることは本当にコレだけです。
このテーブルに実行済みとしてレコードが格納されているので、次回以降dbm-updateを実行しても、すでに適用された差分が再度適用されることが防げているわけですね。

このあたりを意識する必要があるのは、GORMの変更ではなくて、さっき試したテーブルを直接SQLで修正する場合のみかなと思います。
でも、データベースの取り扱いに関する重要なこのツールの動作を理解しないまま使うのはやっぱり怖いので、軽くでもこのあたりの動作を把握しておいたほうが精神衛生上宜しいと思います。

もし誤ってdbm-changelog-syncを実行してしまった場合

grails dbm-gorm-diffgrails dbm-diffで、変更履歴を作成したあとに、dbm-updateを実行せずに間違ってdbm-changelog-syncを実行すると、まだデータベースに反映していないので、上述の通り、時刻済みとしてdatabasechangelogテーブルにレコードが格納されてしまいます。

もしそうしてしまった場合は、grails dbm-gorm-diffで追加したファイル名を元に、databasechangelogテーブルからそのレコードを削除して、再度dbm-updateを実行すればOKです。

実際に運用する場合

とりあえずは全てcreate-dropモードで開発していく。
で、テーブルの作成、変更が落ち着いた段階(初回リリース時など)以降、dbmigrationを使ってデータベースの構造に完する変項を管理、適用していく。
このタイミングで、DataSource.groovyのdev、test、prodの前環境で、dbCreateをコメントアウトしておく。
今後データベースに対する変更は全てdbmigration経由で行うためです。

ドメインあり、テーブルなし状態。

初回の準備の中で書いたパターン2です。
現在開発段階で、ずっとcreate-dropモードで開発してきており、テーブル自体はデータベース上に存在していなくて、GORM/ドメインのみが存在持している状態。

dbmigrationで管理を始めるには、初回に以下のコマンドを実行してchangelog.groovyを作成します。

grails dbm-generate-gorm-changelog changelog.groovy

とすると、changelog.groovyの中にそのままベタでchangeSetが記述されます。
すでにデータベースとドメインの間に差分があるためですね。
この状態で

dbm-update

すれば当然通常通り変更が適用できます。

あれ?上の方で初回の作業時にdbm-changelog-syncって実行してなかったっけ?と思った人は記憶力と理解力のずば抜けている人です!

dbm-generate-gorm-changelog changelog.grooyを実行した後に、dbm-changelog-syncを実行すると、当然changelog.groovyの中に記述された差分が適用済みとして、databasechangelogの中に登録されてしまうのです。
ドメインもテーブルも空っぽの状況ならコレでなんの問題もありませんでしたが、今回はドメインは完成しているものがすでにあるけど、テーブルが空っぽだから、この差分が適用されなければ困りますね。

なので、今回のようなドメイン有り、テーブルなしな場合は

  1. grails dbm-generate-gorm-changelog changelog.groovy
  2. grails dbm-update

と素直に実行してあげる必要が有ります。

ちなみに、dbm-updateコマンドも、データベース上にdatabasechangelogdatabasechangeloglockというマイグレーション用テーブルがない場合は自動的に生成してくれます。

なんかchangelog.groovyの中にいきなり差分が書かれるのは気持ち悪い。。。差分はあくまでchangesetフィル(dbm-gorm-diffで作成するファイル)の中でのみ管理したい。。。という場合、現在ドメイン有り、テーブルなしなので、テーブルの情報からchangelogを作成して、dbm-gorm-diffで差分ファイルを作成->dbm-updateという流れで実現できます。

  1. grails dbm-generate-changelog changelog.groovy
  2. grails dbm-gorm-diff initial_create_tables_from_gorm.groovy --add
  3. grails dbm-update

1を実行すればからのchangelog.groovyが作成されて、2を実行すればinitial_create_tables_from_gorm.groovyに差分が記述されて、さらにこのファイルのinclude文がchangelog.groovyに自動的に追記されます。
最後の3番の実行で、差分がデータベースに適用されます。

ドメインなし、テーブルあり状態。

今度は逆にドメインはまだないけど、テーブル自体はすでに作成されているという状態です。
どんな時にこんな状態になるかというと、例えばすでに稼働しているシステムがあって、データベースはそのままで、Grailsで新しいガワを作りたい、といった感じでしょうか。

このパターンはちょっと分かり辛いですが、基本的には

  1. grails dbm-generate-changelog changelog.groovy
  2. grails dbm-changelog-sync

と実行します。
1でまずテーブル情報をそのままchangelog.groovyに差分として書き出しますが、そもそもGrailsアプリケーション側にはドメインが存在していないし、テーブル自体の情報を取得して差分としてchange.logに書き出しているので、ワザワザdbm-updateを実行する必要はありません。
というよりも、すでにテーブルが正として存在しているので、dbm-updateを実行してもエラーになります。

テーブル自体の変更をドメインに反映する方法はありませんので、手動でドメインを修正していきます。

その他整理のためのちょっとメモ

databasechangelogテーブルは、changesetの単位でレコードが増えていく。
そのため、同じfilenameが連続して格納されることもある。なのでそれは問題じゃない。

まとめ

だらだらと書いてしまいました。
Grailsの開発段階では、dbCreateをdrop-createにしてバリバリ開発していけばいいのですが、いざ本番リリース後には当然バージョンアップ、修正を重ねていく必要が有ります。
dbmigrationプラグインを使えば、プログラムの修正を本番に反映(warのデプロイ)、データベースの変更をSQLで反映といった作業を分けて行う必要はなくなりますし、変更の内容などは全てdbmigrationプラグインが管理してくれているので、作業漏れなどが減らせます。

留意する点としては、基本的にはGrails側からdbmigrationプラグインを通して全てのテーブルの変更を実行するようにしたほうがいいという点です。
もし別の環境(例えばいきなり本番DB)でテーブルの構造を変更した場合には、以下のような手順を踏む必要が有り、かなり煩雑な作業が必要になります。

  1. 何らかの方法で、開発環境のGrails経由で新しいテーブルの構造を持つデータベースに接続できるようにする。
  2. grails 例えばprod dbm-diff dev sqlbaseadded.groovy --addを実行。
  3. grails dbm-updateを実行。
  4. grails 例えばprod dbm-changelog-syncを実行。
  5. 環境によるが、最初にアクセスできるようにした環境をセキュリティの観点から開発環境からはアクセスできないように元に戻す。
  6. 終了

特に4番の作業を忘れると、いざデプロイしたらアプリケーションが起動しないなどの問題が発生します。(Grails起動時にdbmigrationを自動実行するようにしている場合)

いずれにせよ、データベースの変更というのは大変な作業ですし、開発ルール、運用ルールは会社、プロジェクト、人、環境によって様々です。
まずは実際にテストプロジェクトでdbmigrationプラグインを触ってみて、自分の環境に適した運用ルール、使い方を見つけていきましょう。

なお、今回は触れていませんが、データ自体の登録もdbmigrationツールで簡単かつ安心便利に行うことが出来ます。それについてはまた別途。

参考

公式ドキュメント
Database Migration Pluginがすごい ~入門編~
Database Migration Pluginがすごい ~ロールバック編~
dbm-gorm-diff
dbm-diff
dbm-changelog-sync
dbm-update