[Grails]データベースマイグレーション  こぼれ話編 with PostgreSQL

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

#dbm-gorm-diffが比較するもの
これはあくまで現在のデータベースの状態（databasechangelog含）とドメインの状態の差分です。
まだ適用されていないChangeSetがあったとしてもその内容は無視されます。




# PostgreSQLのsequenceについて

とりあえず以下のドメインを想定


```groovy:grails-app/domain/dbmigrationtest/Person.groovy
package dbmigrationtest

class Person {

    String name
    Integer age

    static constraints = {
    }

    static mapping = {
        id generator:'sequence', params: [sequence:'test_id_seq']
    }
}
```

ただし、この状態だと、確かにPostgreSQLのtest_id_seqというテーブルは存在しているけど、テストテーブル自体にはまだその情報は追加されていません。
そのため、手動でINSERTした時には毎回このsequence名を指定しなければなりません。

```:現在のテーブルの情報
dbmigrationtest=# \d
                 List of relations
 Schema |         Name          |   Type   | Owner
--------+-----------------------+----------+-------
 public | databasechangelog     | table    | koji
 public | databasechangeloglock | table    | koji
 public | hibernate_sequence    | sequence | koji
 public | hoge                  | table    | koji
 public | person                | table    | koji
 public | test_id_seq           | sequence | koji
(6 rows)

dbmigrationtest=# \d person
            Table "public.person"
 Column  |          Type          | Modifiers
---------+------------------------+-----------
 id      | bigint                 | not null
 version | bigint                 | not null
 age     | integer                | not null
 name    | character varying(255) | not null
Indexes:
    "personPK" PRIMARY KEY, btree (id)

dbmigrationtest=#
```

で、以下のようなファイルを作成します。

```groovy:grails-app/migrations/addSequence.groovy
databaseChangeLog = {
	changeSet(author: "koji (generated)", id: "add default") {
		grailsChange {
			change {
				sql.execute("ALTER TABLE person ALTER COLUMN id SET DEFAULT nextval('test_id_seq')")
			}
		}
	}
}

```

この作成したファイルを、changeog.gooryの、databaseChagneLogの中の末尾にincludeとして追加します。

```groovy:grails-app/migrations/changelog.groovy
databaseChangeLog = {

/*
 * 長いので省略
 */
        // これを追加。changeSetクロージャの外側に記述すること。
        // つまりdatabasechangeLogに突っ込むクロージャのトップレベルに定義する。
        include file: 'addSequence.groovy'
}

```




これでdbm-updateを実行します。

```:実際にデータベースに反映させる
grails dbm-update
```

でデータベースを確認すると。。。？

```
dbmigrationtest=# \d person
                                Table "public.person"
 Column  |          Type          |                     Modifiers
---------+------------------------+---------------------------------------------------
 id      | bigint                 | not null default nextval('test_id_seq'::regclass)
 version | bigint                 | not null
 age     | integer                | not null
 name    | character varying(255) | not null
Indexes:
    "personPK" PRIMARY KEY, btree (id)
```

反映されました！
これを手作業で開発環境、テスト環境、本番環境それぞれで実行しようとしているといつか作業漏れなどが発生しますよね。
こういう作業もdbmigrationに任せてしまえば安心！


#データの登録
BootStrap.groovyで、Grails起動時/終了時にテーブルのレコードを更新する事ができますが、起動時/終了時に毎回実行されるので、データの存在チェックなどのロジックを一緒に記述する必要があります。煩雑になりますね。
そこでもdbmigrationの登場です。
dbmigrationを使えば当然一回実行された更新は今後二度と実行されることはありません。この機能を利用しない手はありませんね。またデータに関わる作業をdbmigrationで一括管理することができるのでよりスマートですね。

なお、dbmigrationでは以下の3通りの方法でデータを操作することができます。
1. InsertStatementベース（Liquibase）
2. SQLベース
3. GORMベース


では、オリジナルのchangeSetを作成する。

```groovy:grails-app/migrations/add-records.groovy
import dbmigrationtest.Person
import liquibase.statement.core.InsertStatement


databaseChangeLog = {

    /**
     * liquibaseベース
     */
    changeSet(author: "koji", id: "liquibase-1") {
        grailsChange {
            change {
                def statements = []
                3.times {
                    def insertStatement = new InsertStatement('public', 'person')
                        .addColumnValue('name', "'liquibase-${it}'")
                        .addColumnValue('age', 30)
                        .addColumnValue('version', 1 as BigInteger)
                    statements.add(insertStatement)
                }
                // ここでまとめて実際に実行
                sqlStatements(statements)

                confirm 'liquireベースで実行'
            }
        }
    }

    /**
     * SQLベース
     */
    changeSet(author: "koji", id: "sql-1") {
        grailsChange {
            change {
                sql.execute("INSERT INTO person (name, age, version) values('sql-1', 30, 1)")
                sql.execute("INSERT INTO person (name, age, version) values('sql-2', 30, 1)")
                sql.execute("INSERT INTO person (name, age, version) values('sql-3', 30, 1)")
                confirm 'sqlベースで実行'
            }
        }
    }

    /**
     * GORMベース
     */
    changeSet(author: "koji", id: "gorm-1") {
        grailsChange {
            change {
                3.times {
                    new Person(name:'gorm-1', age:30, version: 1).save(flush:true)
                    new Person(name:'gorm-2', age:30, version: 1).save(flush:true)
                    new Person(name:'gorm-3', age:30, version: 1).save(flush:true)
                }
                confirm 'GORMベースで実行'
            }
        }
    }
}
```

そしてそのファイルをchangelog.groovyからincludeします。

```groovy:grails-app/migrations/changelog.groovy
databaseChangeLog = {

/*
 * 長いので省略
 */
        // これを追加。changeSetクロージャの外側に記述すること。
        // つまりdatabasechangeLogに突っ込むクロージャのトップレベルに定義する。
	include file: 'add-records.groovy'
}

```

で、例のごとく`dbm-upate`を実行します。

```:実際にデータベースに反映させる
grails dbm-update
```

ではレコードを見てみます。

```:INSERTされたレコード
dbmigrationtest=# select * from person;
 id | version | age |    name
----+---------+-----+-------------
  1 |       1 |  30 | liquibase-0
  2 |       1 |  30 | liquibase-1
  3 |       1 |  30 | liquibase-2
  4 |       1 |  30 | sql-1
  5 |       1 |  30 | sql-2
  6 |       1 |  30 | sql-3
  7 |       0 |  30 | gorm-1
  8 |       0 |  30 | gorm-2
  9 |       0 |  30 | gorm-3
 10 |       0 |  30 | gorm-1
 11 |       0 |  30 | gorm-2
 12 |       0 |  30 | gorm-3
 13 |       0 |  30 | gorm-1
 14 |       0 |  30 | gorm-2
 15 |       0 |  30 | gorm-3
(15 rows)
```

ちゃんとINSERTされました！
なんかGORM経由でいっぱい登録されているぞ、と思ったのですが、単純に自分のソースの記述ミスです。
これでdbmigrationを使ってデータベースにレコードを登録する方法がわかりました。
BootStrap.groovyと違って2回目以降は実行されないので重複してデータが登録されることはありません。
なお、liquibase使うメリットは正直全くないと思います...
今回は登録だけですが、GORMとSQLが使えるということは、メンテナンスにも大きな威力を発揮しそうですね！

#カラムの順番の入れ替え
GORM（というかHibrernate？）はカラムの追加の順番を制御できません。
さらに、PostgreSQLはカラムの追加は必ずテーブルの末尾になります。
動作自体に特に問題はありませんが、自分の意図した順番でカラムを並べ替えたいと思うのが人情。
この記事をここまで読んでくださった方ならなんとなく想像されている方もいらっしゃるかもしれませんが、dbmigrationはその管理すべき変更を全てただのGroovyファイルとして保存しています。
また、そのGroovyファイルの中で直接SQL文を記述することも可能です。

カラムの順番という情報自体はGORMの範疇にはない情報です。なので、dbm−*系のコマンドでは管理できない情報ということになります。
そこで、手動でChangeSetを作成して、その中でSQLを直接実行するようにすることでこの問題を解決することができます。

```:personテーブル
dbmigrationtest=# \d person
            Table "public.person"
 Column  |          Type          | Modifiers
---------+------------------------+-----------
 id      | bigint                 | not null
 version | bigint                 | not null
 age     | integer                | not null
 name    | character varying(255) | not null
Indexes:
    "personPK" PRIMARY KEY, btree (id)

dbmigrationtest=#
```

このテーブルのカラムの並びを変えたい、（具体的にはid, name, age, versionの順番にしたいとする。）

ちなみにPostgreSQLにはカラムを並び替える命令は存在しないので、テーブルを再作成する必要があります。
ということで、それをdbmigration経由でやってみます。
手動で以下のchangeSet用ファイルを作成しました。

```groovy:grails-app/migrations/reorder-columns.groovy
databaseChangeLog = {

    changeSet(author:"koji", id:"hogehoge-1") {
        grailsChange {
            change {

                // データのバックアップ
                sql.execute("CREATE TABLE person_table_backup AS SELECT * FROM person")

                // 対象テーブルの削除
                sql.execute("DROP TABLE person CASCADE")

            }
        }
    }
    changeSet(author:"koji", id:"hogehoge-2") {
        // 対象テーブルの再作成
        // dbmigrationでちゃんとテーブルを作成していれば、その時の定義が残っているはずなので、
        // それを探し出してきてカラムの順番を並び替えるだけでOK
        createTable(tableName: "person") {
            column(name: "id", type: "int8") {
                constraints(nullable: "false", primaryKey: "true", primaryKeyName: "personPK")
            }

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

            column(name: "age", type: "int4") {
                constraints(nullable: "false")
            }

            column(name: "version", type: "int8") {
                constraints(nullable: "false")
            }
        }
    }
    changeSet(author:"koji", id:"hogehoge-3") {
        grailsChange {
            change {
                // バックアップデータのリストア
                sql.execute("""
                    INSERT INTO person (
                        id,
                        name,
                        age,
                        version
                    )
                    (SELECT id, name, age, version FROM person_table_backup);
                """)

                // バックアップテーブルの削除
                sql.execute("DROP TABLE person_table_backup CASCADE")

                confirm '複雑な作業'
            }
        }
    }
}
```

なんかcreateTableを書く場合はchangeSetの中にcreateTable自信を一つしか描けないらしい（sqlとか別に実行しようとするとエラーになる）
まぁそいうなんだということでそこは放置。

これも当然changeset.groovyに追加します。

```groovy:grails-app/migrations/changelog.groovy
databaseChangeLog = {

/*
 * 長いので省略
 */
        // これを追加。changeSetクロージャの外側に記述すること。
        // つまりdatabasechangeLogに突っ込むクロージャのトップレベルに定義する。
	include file: 'reorder-columns.groovy'
}

```
で、例のごとく`dbm-upate`を実行します。

```:実際にデータベースに反映させる
grails dbm-update
```

実行結果を確認すると。。。？

```:実行結果
bmigrationtest=# \d person
            Table "public.person"
 Column  |          Type          | Modifiers
---------+------------------------+-----------
 id      | bigint                 | not null
 name    | character varying(255) | not null
 age     | integer                | not null
 version | bigint                 | not null
Indexes:
    "personPK" PRIMARY KEY, btree (id)

dbmigrationtest=# select * from person;
 id |    name     | age | version
----+-------------+-----+---------
  1 | liquibase-0 |  30 |       1
  2 | liquibase-1 |  30 |       1
  3 | liquibase-2 |  30 |       1
  4 | sql-1       |  30 |       1
  5 | sql-2       |  30 |       1
  6 | sql-3       |  30 |       1
  7 | gorm-1      |  30 |       0
  8 | gorm-2      |  30 |       0
  9 | gorm-3      |  30 |       0
 10 | gorm-1      |  30 |       0
 11 | gorm-2      |  30 |       0
 12 | gorm-3      |  30 |       0
 13 | gorm-1      |  30 |       0
 14 | gorm-2      |  30 |       0
 15 | gorm-3      |  30 |       0
(15 rows)
```

ちゃんと並べ替えられました！
こうやって最終的にdbmigration経由でちゃんとカラムを並べ替えられることはできましたが、まぁやはり最初にテーブルを生成する際に、生成されたchangeSetでカラムの並びをちゃんと入れ替えておいたほうが無難ですね。。。