JPAのManyToOneにおける各ベンダーの違い

 JPA*1エンティティー間の多対一のリレーションを表現する@ManyToOneですが、こやつの振る舞いがベンダー間でちょっと違うようなのでメモしておきます。

振る舞いの違い

 どう違うのかというと、参照先のエンティティが物理的に存在しなければ取得時にEntityNotFoundExceptionをthrowするかどうか。
例えば下記のテーブルとデータがあるとします。
EMPLOYEEテーブル

EMPLOYEE_ID NAME
1 Hoge
2 Foo
3 Bar

EXPENSEテーブル

EXPENSE_ID AMOUNT EMPLOYEE_ID
1 130 1
2 930 2
3 2,340 4

EXPENSEテーブルのEMPLOYEE_IDとEMPLOYEEテーブルのEMPLOYEE_IDとの間には論理的な参照が存在します(物理的な制約(参照整合性制約)があるわけではありません)。また、EXPENSEテーブルの3行目のデータは存在しないEMPLOYEEのIDを参照してしまっているゴミデータです。

この時の各テーブルに対するエンティティは下記になります。

Employeeエンティティ

@Entity
@Table(name = "EMPLOYEE")
public class Employee implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @Column(name = "EMPLOYEE_ID")
    private Integer employeeId;
    @Column(name = "ZIP")
    private String zip;
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "employee")
    private List<Expense> expenseList;
    //アクセサは省略

Expenseエンティティ

@Entity
@Table(name = "EXPENSE")
public class Expense implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @Column(name = "EXPENSE_ID")
    private Integer expenseId;
    @Column(name = "AMOUNT")
    private BigDecimal amount;
    @JoinColumn(name="EMPLOYEE_ID",referencedColumnName="EMPLOYEE_ID")
    @ManyToOne
    private Employee employee;

    //アクセサは省略

 この時EXPENSE_IDが3(ゴミデータ)を取得しようとした時にベンダー間で振る舞いが変わってきます。
調べたベンダーとバージョンは以下です。

  • OpenJPA 1.2.2(JPA1.0実装)
  • Hibernate EntityManager 3.2.0-GA(JPA1.0実装)
  • Hibernate EntityManager 3.5.0-FINAL(JPA2.0実装)
  • OpenJPA 2.0.0Beta3(JPA2.0実装)
  • EclipseLink2.0.0(JPA2.0実装)

んで、実際どうなのか

  • EntityNotFoundExceptionをthrowする
    • Hibernate EntityManager 3.2.0-GA(JPA1.0実装)
    • Hibernate EntityManager 3.5.0-FINAL(JPA2.0実装)
  • EntityNotFoundExceptionをthrowせずに取得時(getEmployeeが呼び出された時)にnullを返す
    • OpenJPA 1.2.2(JPA1.0実装)
    • OpenJPA 2.0.0Beta3(JPA2.0実装)
    • EclipseLink2.0.0(JPA2.0実装)

 この振る舞いの違いって結構デカいと思うのですがどうでしょうか??「ちょっくらJPAの実装変えてみようZE!」的なノリでやった場合に予期せぬ場所で例外が吹っ飛ぶことになります。テーブルに対して物理的に参照整合性制約を付けていればバッチ等でデータが流された場合も整合性は保たれるでしょうが、そもそも参照整合性制約がかけれない状況だってあるわけで。というか調べた限りHibernateだけ独自っぽいです。自分はHibernateJPA実装歴が長かったのでOpenJPAやEclipseLinkを試すまで例外が吹っ飛ぶのがJPAの仕様だと勘違いしてました。(そういや初期のJPA-Hibernate実装はfindメソッドで存在しない場合はEntityNotFoundExceptionをthrowしてました。今はちゃんとJPAの仕様どおりnullを返すようです。こちらも自分はずっとそれがJPAの仕様だと勘違いしてました。)
 じゃあHibernateでOpenJPAやEclipseLinkのような振る舞いにすることはできないのかというともちろんできます。@ManyToOneのカラムに@NotFoundアノテーションを付けます。

    @NotFound(action=NotFoundAction.IGNORE)    
    @JoinColumn(name="EMPLOYEE_ID",referencedColumnName="EMPLOYEE_ID")
    @ManyToOne
    private Employee employee;

 しかしここでも問題があります。@NotFoundアノテーションHibernate独自のアノテーションです。つまりベンダー依存になっちゃうんですよね〜。これじゃあせっかくの標準仕様が台無しです><。persistence.xmlへの記述でなんとか回避出来ないか調べてみたんですが今のところ見つけられず・・・。個人的には例外が吹っ飛んで欲しいのでOpenJPAとEclipseLinkがHibernateに合わせて(というかJPAの標準仕様にして)、例外が吹っ飛んで欲しくない場合は@ManyToOneのオプションで制御とかやって欲しい感じです。

 ちなみにOpenJPAやEclipseLinkで参照先が存在しないエンティティをそもそも取得したくない場合は下記のようにします。

    @ManyToOne(optional=false)

ただし、これだとfindメソッドでPK検索した場合も取得できないという仕様みたいです・・・。う〜ん、それもどうなの?って感じですねぇ〜。

*1:1.0、2.0の両方