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 は有効になっている。Oracle や SQLServerではもちろん使えるようだ。
@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 などあちこちでよく出ているようだ。
- http://opensource.atlassian.com/projects/hibernate/browse/HHH-2712
- http://forum.hibernate.org/viewtopic.php?p=2381079
- http://jira.jboss.org/jira/browse/HIBERNATE-73
解決するには、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 を使ったほうが数倍まし、という結論でした。