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-personadd-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の中の実装が変わると再度実行されます。