GrailsでPostgreSQL用独自Dialectを使ってHibernateのデフォルトの挙動を変更する
今回も「ニッチだけど実用的なシリーズ」ということで、GrailsでPostgreSQLを使う場合に便利なTIPSについて紹介します。PostgreSQL用独自Dialectを使ってHibernateのデフォルトの挙動を変更する方法です。
中野靖治のGroovy活用術 第4回
- 2015年12月10日公開
GrailsでPostgreSQL用独自Dialectを使ってHibernateのデフォルトの挙動を変更する
はじめに
こんにちは。NTTソフトウェアの中野です。 今回も「ニッチだけど実用的なシリーズ」ということで、GrailsでPostgreSQLを使う場合に便利なTIPSについて紹介します。
デフォルトの挙動で困る点
GrailsのGORMでは、PostgreSQLに対して以下のようなデフォルトの挙動があります。
- (A) 自動ID採番のためのシーケンスがアプリケーション全体でひとつしかない
- (B) String型のプロパティに対してサイズ系の制約(size, maxSize 等)を指定しないと、実行時に例外がスローされる場合がある
それぞれ順番に説明します。
(A) 自動ID採番のためのシーケンスがアプリケーション全体でひとつしかない
どんな問題?
GORMの裏にいるHibernateのデフォルトの自動採番方式では、PostgreSQL上に hibernate_sequence という BIGINT 型のシーケンスを生成して、以降ではそれを使って採番します。 複数のドメインクラスをまたいでこの共通のシーケンスが使われるため、以下のように異なるドメインクラスに対して、重複のないIDが払い出されることになります。
DomainA: {1, 2, 5, 9, 23496} DomainB: {3, 4, 8} DomainC: {6, 7, 10, 11, ..., 23495}
気にしない、という考えもありますが、この場合レコード数が突出しているDomainCのせいでDomainAにも突然 23496 のような大きなIDが登場することにもなって精神衛生上よろしくありませんし、IDの最大値的にもオーバフローしやすくなりますし、ここは素直に
DomainA: {1, 2, 3, 4, 5} DomainB: {1, 2, 3} DomainC: {1, 2, 3, ..., 23490}
のように採番したいところです。
個別対処
ドメインクラスごとにmappingに設定することで、そのドメインクラス独自のシーケンスを定義できます。
// DomainA.groovy class DomainA { // ...省略... static mapping = { id generator: 'sequence', params: [sequence:'domain_a_id_seq'] } }
// DomainB.groovy class DomainB { // ...省略... static mapping = { id generator: 'sequence', params: [sequence:'domain_b_id_seq'] } }
// DomainC.groovy class DomainC { // ...省略... static mapping = { id generator: 'sequence', params: [sequence:'domain_c_id_seq'] } }
(B) String型のプロパティに対してサイズ系の制約を指定しないと、実行時に例外がスローされる場合がある
どんな問題?
ドメインクラスを定義すると、Grails起動時にHibernateが自動的にDDLを発行して、データベース上に対応するテーブルを生成してくれます。 String 型のプロパティは、DB上では可変長文字列である VARCHAR 型にマッピングされます。
VARCHAR は可変長とはいっても最大サイズが決まっています。 たとえば、以下のように制約で maxSize: 100 と指定しておけば、
class DomainA { String value static constraints = { value maxSize: 100 } }
生成されるテーブルのスキーマでは、value カラムの型は VARCHAR(100) となります。 このように、maxSize や size 制約などを使って最大文字列長が決定されれば何も問題はありません。
問題となるのは、「サイズ制限無し」という意図(または単にうっかり)で、そのようなサイズ系の制約をつけていないプロパティです。
class DomainA { String value // 危ない例 }
この場合、実はデフォルトで VARCHAR(255) にマッピングされてしまうのです。
すると、制約がないので、value プロパティに何文字指定されてもバリデーション的には問題ないのですが、256文字以上を指定した場合にはDBへの登録の時点で型違反の例外がスローされてしまうことになります。 ちなみにスローされるのは、org.postgresql.util.PSQLException が org.springframework.dao.DataIntegrityViolationException でラップされたものです。
def domainA = new DomainA(value: "x"* 256) // 256文字のx
domainA.validate() assert !domainA.hasErrors() //=> バリデーションは問題ない
domainA.save() //=> 保存すると例外がスローされる // org.springframework.dao.DataIntegrityViolationException: // Hibernate operation: could not execute statement; SQL [n/a]; // ERROR: value too long for type character varying(255); // nested exception is org.postgresql.util.PSQLException: // ERROR: value too long for type character varying(255)
個別対処
String 型のプロパティを宣言するときは、必ず maxSize か size 制約を使って上限値を明示指定しましょう。上限なしの可変長文字列にしたい場合は、TEXT 型にマッピングします。
class DomainA { String name String value String infinite static constraints = { name size: 4..10 //=> name: VARCHAR(10) value maxSize: 100 //=> value: VARCHAR(100) } static mapping = { infinite type: 'text' //=> infinite: TEXT } }
Dialectを使って一括で対処する
さて、前述した個別対処では、ドメインクラスを作るたびに、または、プロパティを追加するたびに、ひとつずつ漏れなく正しく実装していかなければなりません。 まあ、きちんとやれば問題ないのですが、うっかり対処が漏れてしまうリスクもありますし、ソースコードも冗長になりがちです。
Hibernateでは、RDBMSごとの方言や固有の機構を吸収するためにDialectという仕組みが用意されています。 各RDBMSごとのDialect実装が提供されていて、通常はそれを使えばOKです。 このDialect機構を利用して、独自のDialectを実装すれば、(1)と(2)の両方に対して一括で対処できます。 個々のドメインクラスのmapping設定は不要になります。
1.以下の内容のクラスを src/groovy 配下の適切なパッケージ配下に作成する
// 適切なパッケージ配下に入れること package hibernate
import org.hibernate.dialect.Dialect import org.hibernate.dialect.PostgreSQL9Dialect import org.hibernate.id.SequenceGenerator import org.hibernate.type.Type import java.sql.Types
class MyPostgreSQLDialect extends PostgreSQL9Dialect { // 9.x用の場合。8.1以前はPostgreSQL81Dialect、8.2以降の8.xはPostgreSQL82Dialectを継承すること。
MyPostgreSQLDialect() { // (B) Stringに対するデフォルトカラム長である255になっている場合に、強制的にTEXTマッピングする。 // それ以外はvarchar($l)で長さ指定のVARCHARにマッピングする。 registerColumnType(Types.VARCHAR, 254, 'varchar($l)') // 254以下はVARCHAR(デフォルトのまま) registerColumnType(Types.VARCHAR, 255, 'text') // 255はTEXT registerColumnType(Types.VARCHAR, 'varchar($l)') // 256以上はVARCHAR(デフォルトのまま) }
@Override Class<?> getNativeIdentifierGeneratorClass() { // (A) デフォルトの実装では全テーブル共通で一つのhibernate_sequenceを使用してid採番が行われる。 // これをテーブル個別にシーケンスが生成されるように独自のジェネレータを指定する。 TableNameSequenceGenerator }
static class TableNameSequenceGenerator extends SequenceGenerator { @Override void configure(Type type, Properties params, Dialect dialect) { if (!params.getProperty(SEQUENCE)) { String tableName = params.getProperty(TABLE) if (tableName) { params.setProperty(SEQUENCE, "${tableName}_id_seq") } } super.configure(type, params, dialect) } } }
2.データソースの設定でこの独自Dialectクラスを使うように指定する
Grails 3.0.xの場合:
// grails-app/conf/application.yml //... dataSource: pooled: true jmxExport: true driverClassName: org.postgresql.Driver dialect: hibernate.MyPostgreSQLDialect // この行を追加する username: xxxx password: xxxx //...
Grails 2.5.xの場合:
// grails-app/conf/DataSource.groovy //... dataSource { pooled = true jmxExport = true driverClassName = "org.postgresql.Driver" dialect = "hibernate.MyPostgreSQLDialect" // この行を追加する username = "xxxx" password = "xxxx" } //...
注意事項
上記の独自Dialectによる対処では、単純に文字列長がデフォルト値の255である場合に何も考えずに TEXT 型にマッピングする、という多少雑な実装になっています。 これは、このDialectの時点で判断する材料が文字列長しかないためやむを得ずこうなっているのですが、ご想像の通り、この実装では要件として VARCHAR(255) でなければならないプロパティがあるときに逆に問題になってしまいます。
class DomainX { String max255 static constraints = { max255 maxSize: 255 //=> VARCHAR(255)を指定したはずが、独自DialectによってTEXT型とみなされてしまう! } }
このため、VARCHAR(255) というプロパティが存在する場合は、独自Dialectを使わずにすべて個別に対処するか、または、VARCHAR(255) にすべきプロパティに対してのみ mappingでゴリ押しの明示指定 max255 sqlType: "varchar(255)" を書くことで独自Dialectでの指定を上書きする必要があります。
とはいえ、個人的には、一般的なアプリケーションであえて16進数を意識した VARCHAR(255) という上限値を必要とするようなユースケースはほとんどないんじゃないかと思います。 実際、独自Dialectを使って困ったことはありませんし、あまり気にしていません。
おわりに
なお、今回はG* Advent Calendar 2015の10日目の記事も兼ねています。
よかったら他の方の記事もご参照ください。
今回のサンプルコードは、GitHubに公開してあります。
- Grails 3.x: https://github.com/nobeans/grails3x-postgresql-dialect (Grails 3.0.10で作成&動作確認)
- Grails 2.x: https://github.com/nobeans/grails2x-postgresql-dialect (Grails 2.5.3で作成&動作確認)
コメントや不具合等があればIssuesやPull Requestでお願いいたします。
JVM上で動作する動的型付け言語であるGroovyと、Groovyで記述するWebアプリケーションフレームワークのGrailsを社内外へ推進するために日々奮闘している。 Groovyスクリプトの起動時間を短縮するGroovyServや、GroovyスクリプトでのExcel操作を劇的に楽にするGExcelAPIなどのOSSを業務/プライベートで開発、 一般に公開。Groovy/Grails/GradleなどのOSSへのバグフィックスや機能パッチの提供などにも積極的に貢献している。 また、国内外(JJUG CCC、Java Day Tokyo, Gr8conf EUなど)での講演や書籍執筆などでも活動中。 著書に『プログラミングGroovy』(技術評論社/共著)がある。自他共に認めるビール党。