Hibernate Annotationsでの拡張

リファレンスガイドの2章まで訳したところで、そろそろ実戦に使ってみようとして色々はまった。その悪戦苦闘のメモ。後から思えば、hbm2ddlのせいではまったともいえる。

hbm2ddl

Hibernateマッピング情報から、データベースあわせたスキーマSQL)を作成してくれる非常に便利なもの。antから使ってSQL文を生成したり、hibernate.cfg の hibernate.hbm2ddl.auto を create とかにして、自動生成とかもできる。しかし、ちょっと癖があったりするので細かいところではまる。。。

まず、オブジェクトの関連(OneTo*、ManyTo*)を設定すると、それに応じた外部キー制約が”必ず”作られる。まあ、それはそれでよいのだが、後述の @OnDelete を知らないとつらいことに。

また、外部キー名はものすごい適当な名前(英数字のランダム)がつけられる。これも @ForeignKey で指定できるが、そのデフォルト値はどうかと。

Hibernateの拡張アノテーションで大体何とかなるわけだが、当然そのアノテーションJPA標準じゃないので、なんとなく気分的にちょっと。。。な感じ。そういうのって、hbm2ddlのほうで設定すべきなんじゃないかねぇ。と思ったり。それができれば拡張アノテーションも必要ないのに。

@OnDelete

ManyToOneとか、OneToOneとかの関連で、親側を消したら、子も一緒にばっさり消えてほしいことってありますよね。そういうときは、cascade=CascadeType.ALL とすればOKかと思えば、そうでもない。外部キー制約がないならばそれだけでOKかもしれないが、外部キー制約がない関連なんてほんどないのが現実。

外部キー制約があるとどうなるかというと、親側を消そうとすると、子側の外部キーでそのレコードを参照しているため、外部キー参照制約違反になってエラーになる。そのため、外部キーを作るときに、削除されるとどうするか、を定義しておかないといけない。それが Hibernate拡張アノテーションで提供されている @OnDelete。これは引数に action を取り、削除されたときにどういう動きをするかを定義する。

    @OneToMany(mappedBy="parent", cascade=CascadeType.ALL)
    @OnDelete(action=OnDeleteAction.CASCADE)
    public List<Child> getChildren() {
        return children;
    }

この例では、action に OnDeleteAction.CASCADE を指定している。これは削除されると、その操作を引き継いで、子も削除する、ということ。もうひとつのオプションとして、OnDeleteAction.NO_ACTION がある。これは何もしない。つまりデフォルトと同じ動作。

cascade=CascadeType.ALL と指定しているんだから、それくらい察してくれよ!と、ものすごい思うがそれとこれとは別らしい。。また、この @OnDelete を指定すると、hbm2dllで生成する SQL の 外部キーに on delete オプションが付く。

しかし、on delete は全てのデータベースで使えるわけではない。そういう機能を持っていないDBもある()。また、DBにその機能があっても、ドライバや、Dialectでサポートしていないと使えなかったりするので注意。

Dialect#supportsCascadeDelete() が true を返すようだと、有効になる。これは、MySQLDialect では有効ではないが、MySQLInnoDBDialect か MySQL5InnoDBDialect は有効になっている。OracleSQLServerではもちろん使えるようだ。

@ForeignKey

hbm2ddl でスキーマを自動生成すると、外部キー名がめちゃくちゃなものが付く。たとえば、FK41F3FF08AFC8938D みたいなものとか。これではあまりに気持ち悪いので、自分でわかりやすい名前をつけてやることがお勧め。それには、@*ToOne の 子側のほうのプロパティに、@ForeignKeyアノテーションをつける。nameパラメータで外部キー名を設定します。

    @ManyToOne()
    @ForeignKey(name="FK_child_parent")
    public Parent getParent() {
        return parent;
    }

上記のような感じ。ここでは、FK_、子のエンティティ名、_、親のプロパティ名を繋げたものをつけるようにしてみた。てか、デフォルトそれでいいじゃん!なんだって、あんなへんてこりんな名前になっているのか理解に苦しむ。

@PrimaryKeyJoinColumn

リファレンスの OneToOne について(2.2.5.1. One-to-one)、@PrimaryKeyJoinColumn を使うのが一番簡単かのように書いてあるが、実際は結構めんどくさいことになる。これは、単純に 主キーを外部キーにも使うだけであって、別問題を引き起こす。例えば、以下のような、Parent と Child があったとする。

@Entity
class Parent implements Serializable{
    @Id @GeneratedValue
    public int id;

    @OneToOne(optional=true)
    @PrimaryKeyJoinColumn
    public Child child;
}
@Entity
class Child implements Serializable{
    @Id @GeneratedValue
    public int id;

    @OneToOne @PrimaryKeyJoinColumn
    public Parent parent;
}

このようなマッピングがあったとする。Child テーブルに、Child.id と Parent.id の外部キーが作成され、一見するとうまく動く。しかし、Parent と Child のペアが必ず一致するならばこれでもよいかもしれないが、Parent は Child を持たない場合もある。その場合おかしなことになってしまう。

というのは、Child は id を自動生成(AUTO INCREMENT)している。そのため、Child を持たないParentを作ると、Child が 参照する Parent の id がずれてしまう。例えば、id が 1 から 3 までの Parent が存在し、それに対応する Child もあったとする。次に Child を持たない Parent を追加すると、id=4 のParentができる。さらに、今度は Child を持つ Parent を追加すると、id=5 の Parent ができ、Child の id は 4 になる。そうなると、最後に追加した Child が指す Parent は id=4 の Parent になってしまう。

では、Child の id から @GeneratedValue を削除するとどうなるか。そうすると、Child の id を Parent の id と一致するように、毎回手動でセットしなければいけない。これは非常にめんどくさい上に、バグの温床ともなる。

この問題は Hibernate Forum などあちこちでよく出ているようだ。

解決するには、Child のジェネレータを作ってやればいいらしい。

@Entity
@GenericGenerator(name="foreignGen", strategy = "foreign", parameters={
    @Parameter(name="property", value="parent")
})
class Child implements Serializable{
    @Id
    @GeneratedValue(generator="foreignGen")
    public int id;

    @OneToOne @PrimaryKeyJoinColumn
    public Parent parent;
}

GenericGenerator を使って、parent プロパティの外部キーからidを生成するようにすればよいらしい。

これで問題のほとんどは解決したが、実はまだ問題がある。このまま hbm2ddl にてスキーマを生成すると、またしても外部キーが訳の分からない名前になってしまう。そこで、先ほど同様 @ForeignKey で指定してみても、反映されない。調べてみると、既知のバグのようだ。

2007/10/22にレポートされているのに、まったく反応がなく、いつ直るとも知れない。。

結局、@PrimaryKeyJoinColumn を使うくらいなら、@JoinColumn を使ったほうが数倍まし、という結論でした。