GrailsのGORMとID管理とPostgreSQLのシーケンス

●GORM〜Grailsにおけるドメインモデル

GrailsのドメインモデルはGORM(Grails Object Relational Mapping)というDSL記法を使って記述します。

基本的に1つのドメインモデルは、1つのテーブルに対応します。
ドメインモデルは、エンティティと呼んでもかまいません。ここでは同じモノを指します。

●GORMを元にしたDBスキーマの自動生成

Grailsでは$APP_HOME/grails-app/conf/DataSource.groovyファイルに、接続先のDataSourceを定義します。
GORMでドメインモデルを定義しておき、Grailsをそのまま起動すると、なんと対象DataSourceに対して自動的にGORMの定義情報を元に決定されたDDLが発行されて、DBスキーマが生成されます。
開発初期には結構便利です。

DBスキーマを勝手にいじって欲しくない場合は、DataSource.groovyの中に書いてあるdbCreateという設定エントリをコメントアウトまたは削除すればOKです。

dbCreate 説明
createDrop Grails起動時にDBスキーマを生成し、Grails停止時に破棄する。既存レコードは毎回全て消える
create Grails起動時にDBスキーマを生成する。現在のスキーマとGORMの定義に差分があってもスキーマを更新しない(※1)。既存レコードは毎回全て消える
update Grails起動時に存在しなければDBスキーマを生成する。現在のスキーマとGORMの定義に差分がある場合はスキーマをアップデートする(※2)。既存レコードはそのまま残る(※3)
エントリ自体存在しない GrailsからはDBスキーマを一切変更しない。当然データも変更されない
  • ※1 といいつつ、Grails1.1.1で確認するとスキーマ変更もされてるっぽい。『Grails停止時にDROPはせずに、次回の起動時にDROP&CREATEをする』という挙動を示している。→別途確認します
  • ※2 ※1に関連して、NOT NULL制約は追加されない。
  • ※3 既存レコードに対するカラム時は、そのカラムの値がNULLとなる。

Grailsと"環境"

Grailsでは、起動するときに「開発(development)」「テスト(test)」「本番運用(production)」の3つの環境を選択できます。
開発とテストではHsqldbを使うけど、本番はPostgreSQLで、みたいな設定もできます。
毎回設定ファイルを書き換えるのではなく、3種類の設定を書いておいて、実行時に選択することになります。

起動時に、環境を指定する場合は以下のようにします。

    grails run-app      →デフォルト=development
    grails dev run-app  →明示指定。短縮名を使う
    grails test run-app
         →テストは"grails test-app"で使われる環境のため、このように起動することは実際にはない
    grails prod run-app →明示指定。短縮名を使う

前項のdbCreateですが、本番用のproductionではコメントアウトまたは削除をしておくことをお勧めします。
GORMを書き換えて再起動したら、DB上の本番データが消えてました!という大惨事が起こらないようにご注意ください。

●GORMにおけるプライマリキーとしてのidカラム

さて、GORMを使った場合、自分で宣言しなくても、プライマリキー(以下、PK)として自動的にidカラムが生成されます。
(ちなみに楽観的排他制御のためにversionカラムも作られますが、とりあえずそれはおいておきます。)

GORMの仕様としては複合主キーも指定可能っぽいです。じつはこれは試したことがありませんのでよくわかりません。

DBアーキテクトな方には怒られてしまうかもしれませんが、PKは専用の通番カラムであるidにしておいて、他の制約は別途ユニーク制約などでつければよい、と思っています。

どちらにせよGrailsを使う場合は、素直にidカラムを受け入れましょう。

●idの通番管理

idカラムはPKなので、ユニークな値にしなければなりません。
GrailsではデフォルトでHibernateの通番管理の仕組みを利用します。

Hibernateのデフォルトの通番管理方式では、RDBMSがシーケンスに対応している場合、RDBMS上にhibernate_sequenceというBIGINTなシーケンスを生成して、以降ではそれを使って通番を払い出します。

注意事項として、デフォルトではこのシーケンスはRDBMS上に一つだけしかありませんので、Hibernateにまかせると、複数のテーブルをまたいで通番が払い出されまくることになります。

    (イメージ)
    テーブルA: {1, 2, 8}
    テーブルB: {3, 4, 6, 7}
    テーブルC: {5, 9}

実際にこれで運用すると精神衛生上あまりよろしくないと思います。
テーブルごとに1:1で個別のシーケンスを生成して、それで通番管理させることをお勧めします。


さて、Grailsに「このドメインモデルのテーブルはこのシーケンスを使ってね」と指定するには以下の様にドメインモデルに書けばOKです。

class Hoge {
    // ...省略....
    static mapping = {
        id generator:'sequence', params:[sequence:'hoge_id_seq']
    }
}

●PostgreSQLのserialとシーケンス

唐突ですが、PostgreSQLにはserial型という特別なデータ型があります。

現在の実装では、

CREATE TABLE tablename (
colname SERIAL
);

は以下を指定することと同じです。

CREATE SEQUENCE tablename_colname_seq;
CREATE TABLE tablename (
colname integer NOT NULL DEFAULT nextval('tablename_colname_seq')
);
ALTER SEQUENCE tablename_colname_seq OWNED BY tablename.colname;

これ、非常に便利です。


上記のSQLを見てもわかるように、データ型としてserial型を指定したカラムにはシーケンスが自動的に生成されて、その名前は

    <テーブル名> + "_" + <カラム名> + "_seq"

となります。

さらに、INSERTでidを省略したときに自動的にシーケンスから自動補填してくれるようにデフォルト設定まで自動生成してくれます。まさに至れり尽くせりです。

PostgreSQLであれば、CREATE TABLE後にpsql上で"\d テーブル名"というコマンドを実行すると、どのようなスキーマになったのかが表示されます。必ず確認しておきましょう。

grails_temp=# create table bar (id serial, value text);
NOTICE:  CREATE TABLE will create implicit sequence "bar_id_seq" for serial column "bar.id"
CREATE TABLE
grails_temp=# \d
             List of relations
 Schema |    Name    |   Type   |  Owner   
--------+------------+----------+----------
 public | bar        | table    | grails
 public | bar_id_seq | sequence | grails
(2 rows)

grails_temp=# \d bar
                         Table "public.bar"
 Column |  Type   |                    Modifiers                     
--------+---------+--------------------------------------------------
 id     | integer | not null default nextval('bar_id_seq'::regclass)
 value  | text    | 

grails_temp=# \d bar_id_seq
Sequence "public.bar_id_seq"
    Column     |  Type   
---------------+---------
 sequence_name | name
 last_value    | bigint
 increment_by  | bigint
 max_value     | bigint
 min_value     | bigint
 cache_value   | bigint
 log_cnt       | bigint
 is_cycled     | boolean
 is_called     | boolean

なお、serialのサイズはinteger(4byte)なので、これでは足りないという場合はbigserial (biginteger相当。8byte)を使いましょう。

●GrailsでPostgreSQLのシーケンスを使う

というわけで、GrailsでPostgreSQLのシーケンスを使う方法ですが、簡単には以下の様になります。

▼GrailsにDBスキーマの生成をまかせる場合
  1. 対象ドメインモデルのmappingとして以下のid設定を追加する。
    static mapping = {
        id generator:'sequence', params:[sequence:'任意のシーケンス名']
    }
  1. DataSource.groovyでは、dbCreate=createDrop(デフォルト)にしておく
  2. Grailsを起動する
    • ドメインモデルのテーブルと、mappingで指定したシーケンスが自動生成される!!!!!!!
▼DBスキーマは自前で用意してGrailsからはいじって欲しくない場合
  1. DBスキーマ生成のためのDDLで、ドメインモデル用のテーブルのidのデータ型をserial(or bigserial)などにする
  2. DDLを発行し、DBスキーマを生成する
  3. 対象ドメインモデルのmappingとして以下のid設定を追加する
    static mapping = {
        id generator:'sequence', params:[sequence:'PostgreSQLが自動生成したシーケンス名']
    }
  1. DataSource.groovyでは、dbCreateをコメントアウトまたは削除しておく
    • updateでも良いが、こちらの方針でやるのであればDBスキーマを一切いじらないようにした方が良い
  2. Grailsを起動する
    • 既存のドメインモデルのテーブルとシーケンスがそのまま使われる!!!!!

Grailsによる自動生成についてのサンプル

▼デフォルトの通番管理のまま、GrailsにDBスキーマを生成させた場合
class Foo {
    static constraints = {
    }
}
↓↓↓
grails_temp=# \d 
           List of relations
 Schema |  Name              |   Type   |  Owner   
--------+--------------------+----------+----------
 public | foo                | table    | postgres
 public | hibernate_sequence | sequence | postgres
(2 rows)

grails_temp=# \d foo
      Table "public.foo"
 Column  |  Type  | Modifiers 
---------+--------+-----------
 id      | bigint | not null
 version | bigint | not null
Indexes:
    "foo_pkey" PRIMARY KEY, btree (id)
▼個別のシーケンスを指定して、GrailsにDBスキーマを生成させた場合
class Foo {
    static constraints = {
    }
    static mapping = {
        id generator:'sequence', params:[sequence:'foo_id_seq']
    }
}
↓↓↓
grails_temp=# \d
             List of relations
 Schema |    Name    |   Type   |  Owner   
--------+------------+----------+----------
 public | foo        | table    | postgres
 public | foo_id_seq | sequence | postgres
(2 rows)

grails_temp=# \d foo
      Table "public.foo"
 Column  |  Type  | Modifiers 
---------+--------+-----------
 id      | bigint | not null
 version | bigint | not null
Indexes:
    "foo_pkey" PRIMARY KEY, btree (id)

●まとめ

というわけで実験の結果、Grailsが素敵にシーケンスを生成してくれることがわかります。

しかし、serialの様に、INSERTでidを省略したときに自動的にシーケンスから自動補填してくれるデフォルト設定まではさすがに生成してくれません。

なので、直接SQL操作をすることも視野に入れれば、DBスキーマはid=serial型でCREATE文を直接発行して生成しておき、GrailsではdbCreateをコメントアウトして勝手にスキーマをいじらないようにしつつ、GORMのmappingでserialが生成するシーケンスを指定する、というのが無難なところかと思います。

直接SQLでINSERTなんて実行しないし、もし実行するとしてもcurrvar関数などをつかって自力でシーケンスを使いこなすから、普段はGrailsに全部お任せしたいのさ!という向きには、dbCreate=createDropまたはupdateをお勧めしておきます。はい。
あ、もちろん運用開始にはdbCreateは削除してくださいね。


というところで。