JPAの@Embeddableの使い道
JPAには@Embeddableというアノテーションがありますが、このマッピング機能をうまく活用しているチームはどれくらいあるのでしょうか?私が今まで適用してきた使い方は結局以下の2通りの使い方のいずれかに集約できると思います。
1.属性の多い巨大なテーブルに対するエンティティを入れ子に構造化されたクラスとして扱う
これはちょうどCOBOLにおいて巨大なレコードをばらばらの独立項目として扱うのではなく、値の塊ごとに集団項目として一まとまりの変数としてまとめて考えるという発想に近い考え方です。たとえば、COBOLでは以下のように従業員レコードを固まりで分割して定義できます。
DATA DIVISION. WORKING-STORAGE SECTION. 01 EMPLOYEE. 05 EMP-NO PIC 9(7). 05 EMP-NAME 10 FIRST-NAME PIC X(15). 10 LAST-NAME PIC X(15). 05 JOIN-DATE. 10 YEAR PIC 9999. 10 MONTH PIC 99. 10 DAY PIC 99.
これはC言語などでも入れ子の構造体という形でよく使われるテクニックだと思います。たとえば従業員エンティティにおいて、住所や入社日といった複数のカラムからなる集合を別クラスに抽出し、@Embeddedと@Embeddableでマッピングすることができます。
@Entity public class Employee { @Id private String empNo; @Embedded private Name name; @Embedded private YearDate joinDate; ... } @Embeddable public class Name { private String firstName; private String lastName; ... } // COBOLとの対応をわかりやすくする例のため // 本来的には日付の表現としていまいちかもしれない。 @Embeddable public class YearDate { private int year; private int month; private int day; ... }
業務システムでよく使うRDBのレコードは理由はともあれ正規化を十分に行っておらず*1、多数のカラムを含むことが多いため、これをフラットなエンティティクラスに直接マッピングすると、フィールドの数が多くなり過ぎてしまうことがよくあります。この考え方で@Embeddableを使う場合、単にレコード、構造体の入れ子ですから、抽出されたクラスは読み書き可能な(ミュータブルな)クラスとして設計することが普通かと思います。
2.ValueObjectパターンの実装手段として利用する
ここではValueObjectパターンは、SunのペットショップやJ2EEパターン―明暗を分ける設計の戦略(初版)で間違って広まったDTOとしての意味ではなく、エンタープライズ アプリケーションアーキテクチャパターン (Object Oriented SELECTION)で説明されているように、それ自身IDを持たない抽象的な型(ADT)としての値クラスのことを意味するものとします。つまりValueObjectとは、JDKに最初から含まれるintなどの基本型、String、BigDecimal、Date、列挙型などのクラスの性質をユーザー定義型として自然に拡張した概念であり、典型的な例は「金額」「契約期間」「取引日」などをあらわすクラスを意味すると考えます。C、C++などの言語ではクラスや構造体をポインターではなく値として渡すことで簡単にインスタンスの値のコピーが作成可能ですが(C#などでは構造体という概念で限定的にサポートされる)、Javaの場合オブジェクトはすべて参照型なので、このような値オブジェクトのセマンティクスを保つためには参照の共有によるエリアシング問題*2を防ぐために、オブジェクトを不変(イミュータブル)にするということが一般的です。ファウラーのエンタープライズ アプリケーションアーキテクチャパターン (Object Oriented SELECTION)ではEmbedded Valueというパターンが紹介されており、このパターンに従えば、@EmbeddableをValueObjectの実装に利用するのは少なくとも理論上は理にかなった使い方であると考えられます。ValueObjectは単なる入れ子の構造というよりも値と共にロジック(Side Effect Free Function)を一緒にカプセル化したいような場合に特に有用です。
実際のところはどうか
私の経験から、前者の方法は特に問題なく使えることが多いと思います。特に(このようなケースでJPAを使うことはあまりお勧めしませんが)レガシースキーマからボトムアップでエンティティを作る場合、@Embedableを用いてクラスのサイズを適当に分割することは有用な場合が多いです。また、エントリー系のシステムでも、最近のフレームワークではEL式などでピリオドを使って入れ子の階層をたどることは容易ですから、必ずしもフラットなエンティティクラスにマッピングする必要はありません。
一方、2のValueObjectとして使う方法ですが、最初の想定とは異なり、実際はうまく使いこなせないことが多いです。最大の理由は@EmbedableがJPAでマップされたエンティティの抽象データ型とはならないという仕様上の制約があるからです。したがって、たとえば、JPQLで検索するような場合、検索パラメーターに取引日などのValueObjectを使って直接、同値や前後比較などを記述することができないのです。結局のところ、値オブジェクトとして抽象化しているのに、中身のStringやDate型のフィールドを取り出して処理しないといけないなど、ところどころでいまいちな実装が必要になっていまいます。(実はJPAの実装によってばらつきがあり、HibernateではValueObjectを直接検索条件のパラメーターとして使っても動いてしまう場合もあるのですが、これはJPAのプロバイダー間で可搬性がありません。JPQLの検索パラメーターにValueObjectを使えないことはJPA2の仕様で明確化されています。仕様書の4.12参照。)そのほか、protected以上のデフォルトコンストラクタを定義しないといけないため、厳密にイミュータブルにできなかったり、@IdClassを使って主キークラスを使う場合、ValueObjectをエンティティの主キーの型として使えないなどの制約もあります。(@EmbeddedIdを使うことは可)どうしてこういう制約があるのかわかりませんが、以前のバージョンのEJB-QLとの互換性なども関係しているのでしょうか?
したがって、独自の日付型など検索条件に普通に入れたいようなごく基本的なValueObjectの実装手段についてはJPAの@Embeddableを利用するのではなく、HibernateであればUserTypeの機能を利用する方がベターかもしれません。この方法であれば独自の型をStringやDateとほぼ同格の存在として扱えます。この方法の欠点はHibernate固有のアノテーションが必要になることと、
- UserTypeの実装クラスを作成
- package-info.javaなどで@TypeDefを使ってユーザータイプとして登録
- 値オブジェクトのフィールドに@Typeをつける
という面倒な手順が必要になることですが、どうしてもValueObjectパターンを使いたい場合には有用な手段だと思います。なお、この方法でValueObjectをマッピングするときにはValueObjectのクラスにJPAの@Embeddableをつけると動作がおかしくなるようですので注意してください。