Database Migration Plugin のチェックサム
データベースへの反映履歴を管理するdatabasechangelog
テーブルにはmd5sum
というchangeset
のチェックサムを格納するカラムがあります。
liquibaseのリファレンスには以下のように記述されています。
LiquiBase が変更セットに到達すると、MD5Sum を計算して、”databasechangelog” に MD5Sum を保存します。MD5Sum を保存する意味は、LiquiBase が、ほかの誰かが実行されて以来変更セットを変更していないかどうかを知ることができるためです。変更セットが実行されたときから変更されていた場合、LiquiBase はエラーとともに移行を終了します。というのも、何が変更されたか知ることができず、データベースが変更ログが期待しているのと異なった状態にあるかもしれないからです。もし、適切な理由によって変更セットが変更されていた場合やこのエラーを無視したい場合は、databasechangelog テーブルを更新して、その行の id/author/ファイルのパス名に対応する MD5Sum を null に更新します。次回 LiquiBase が実行されると、MD5Sum の値を適切な値に更新してくれます。
MD5Sum は、”runOnChange” 変更セット属性と一緒に使用されます。普通はただ現在のバージョンが知りたいだけで新しい変更セットを追加したくないのに、更新されたときはいつでも適用したいときがなんどもあるでしょう。このよい例はストアドプロシージャに関するものです。ストアドプロシージャの全体をコピーして新しい変更セットを作るたび、とても長い変更ログが無駄に終わるだけでなく、ソースコード管理システムのマージや差分の機能を失うことになるのです。代わりに、runOnChange = “true” 属性を変更セットにあるストアドプロシージャのテキストにつけましょう。そのストアドプロシージャは、内容が変更されたときだけ再作成されるようになります。
簡単に言うと、changesetを変更しチェックサムが変わった場合に、前者のデフォルトの場合(runOnChange=false
)はエラー、runOnChange=true
の場合は再度実行という動作の違いがあるようです。
書いてある通りなのですが実際に動作させながら検証してみます。
デフォルトの場合
以下のPersonドメインがあるとします。
class Person { String name }
このドメインに対するchangelogを以下のように作成します。
databaseChangeLog = { changeSet(author: "yamkazu (generated)", id: "create-person") { createTable(tableName: "person") { column(name: "id", type: "int8") { constraints(nullable: "false", primaryKey: "true", primaryKeyName: "personPK") } column(name: "version", type: "int8") { constraints(nullable: "false") } column(name: "name", type: "varchar(255)") { constraints(nullable: "false") } } } }
この状態で一度dbm-update
コマンドを使用してデータベースと同期しておきます。同期が成功すると上のchangesetを反映した履歴がdatabasechangelogが書き込まれます。
devDb=# select id, md5sum from databasechangelog; id | md5sum ---------------+------------------------------------ create-person | 3:c2a46c4edd51cd911c9dced0a8fcabe3
changeSetの内容を元にmd5sum
が書き込まれています。この状態でほど作成したchangeSetのname
カラムの型をvarchar(255)
からtext
に変更します。
databaseChangeLog = { changeSet(author: "yamkazu (generated)", id: "create-person") { createTable(tableName: "person") { ... column(name: "name", type: "text") { constraints(nullable: "false") } } } }
同じIDにもかかわらずchangeSetの中身が書き換えられた状態です。この状態でdbm-validate
コマンドを実行します。
grails> dbm-validate | Starting dbm-validate for database test @ jdbc:postgresql://localhost:5432/devDb Validation Error: 1 change sets have changed since they were ran against the database changelog-0.1.groovy::create-person::yamkazu (generated)
このようにchangeSetの中身が変わりチェックサムがことなるためエラーとなります。dbm-update
などの同期処理においても必ず最初にこのvalidateが行われるため、同様にエラーになります(1.3.2までこのエラーがdbm-validate
コマンド以外では握りつぶされていましたが1.3.3で修正される予定です)。
text
への変更をvarchar(255)
に戻して、次はドメインに変更を加えてみます。
class Person { String name Integer age }
age
のプロパティを追加しました。このカラムを追加するchangesetを以下のように作成します。
databaseChangeLog = { ... changeSet(author: "yamkazu (generated)", id: "add-age") { addColumn(tableName: "person") { column(name: "age", type: "int4") { constraints(nullable: "false") } } } }
再度dbm-update
コマンドを使用してデータベースと同期してします。databasechangelogは以下のようになります。
devDb=# select id, md5sum from databasechangelog; id | md5sum ---------------+------------------------------------ create-person | 3:c2a46c4edd51cd911c9dced0a8fcabe3 add-age | 3:2e304646a134a175a22769177941c06a
すでにデータベースに反映が完了して、changesetのcreate-person
とadd-age
を正規化して一つにまとめたいと考えたとします。
databaseChangeLog = { changeSet(author: "yamkazu (generated)", id: "create-person") { createTable(tableName: "person") { column(name: "id", type: "int8") { constraints(nullable: "false", primaryKey: "true", primaryKeyName: "personPK") } column(name: "version", type: "int8") { constraints(nullable: "false") } column(name: "name", type: "varchar(255)") { constraints(nullable: "false") } column(name: "age", type: "int4") { constraints(nullable: "false") } } } }
create-person
のchangesetにage
を追加し、add-age
は削除しました。この状態でdbm-update
を実施すると当然エラーになります。チャックサムが異なるためです。
Validation Error:
1 change sets have changed since they were ran against the database
changelog-0.1.groovy::create-person::yamkazu (generated)
特にデータベースへ反映を行うわけではないがchangesetだけ正規化するとこのような状態になります。この状態を解消するには一度dbm-clear-checksum
を実行します。
grails> dbm-clear-checksums
databasechangelogは以下のようになります。
devDb=# select id, md5sum from databasechangelog; id | md5sum ---------------+-------- create-person | add-age |
このようにmd5sum
の値がクリアされます。この状態で再度dbm-update
を実施するとdatabasechangelogは以下のようになります。
devDb=# select id, md5sum from databasechangelog order by orderexecuted ; id | md5sum ---------------+------------------------------------ create-person | 3:393ba7bc4288b805a68f49b8c3f3f3ee add-age |
今度はエラーとならずにcreate-person
に新たなハッシュ値が書き込まれました。
ちなみにこの時にdbm-rollback-count-sql 1
を実行すると以下になります。
grails> dbm-rollback-count-sql 1 DROP TABLE person; DELETE FROM databasechangelog WHERE ID='create-person' AND AUTHOR='yamkazu (generated)' AND FILENAME='changelog-0.1.groovy';
どうも実行のsqlを見ているとmd5sum
がnullのレコードを対象として動作するようです。少し注意が必要です。
このsqlでロールバックするとadd-age
が残ってしまいますが、特に残っていても害はなさそうですが気になります。dbm-update
のオプションなどで再度チェックサム書いたあとに浮いたレコード削除してくれてもいいようなきもしますが、そんな機能は今のところないようです。
runOnChange=true の場合
runOnChange=true
はchangeSetの属性に設定します。注意点としては当たり前ですがrunOnChange=true
で再実行された際に問題なく動作するクエリを書くことです。
changeSet(author: "yamkazu (generated)", id: "create-one-function", runOnChange: true) { sql(""" | CREATE OR REPLACE FUNCTION one() RETURNS integer | AS 'SELECT 1 AS RESULT;' | LANGUAGE SQL; """.stripMargin()) }
CREATE OR REPLACE
というふうに宣言することで何度実行しても問題ありません。これでfunctionの中の実装が変わると再度実行されます。