...って言うのは少々大げさなのですが、普段は何気ないメソッドも、
油断をすると牙を向いてきますよ、というお話。
親子関係にある2つのエンティティが1:Nの関係で存在したとき、
親エンティティをSetに詰めるコードがあったのですが、
なぜか親エンティティをSetに詰めた時点で、
子エンティティが遅延ロードされてしまうという問題が発生していたのです。
まず、エンティティの定義は次のような感じ。
import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; @Entity public class Parent { @Id private Long id; @OneToMany(fetch = FetchType.LAZY) private Set<Child> children; ...(以下略) // Setに詰めるので、こいつらの実装が必須 public int hashCode() { return HashCodeBuilder.reflectionHashCode(this); } public boolean equals(Object that) { return EqualsBuilder.reflectionEquals(this, that); } } @Entity public class Child { @Id private Long id; ...(以下略) }
で、コレを使ったコードがコチラ。
EntityManager em = ...; TypedQuery<Parent> tq = em.createQuery("select p from Parent p where ...", Parent.class); Set<Parent> parents = new HashSet<Parent>(); for (Parent p : tq.getResultList()) { parents.add(p); // ← ここでParent.childrenの遅延ロードが実行されてしまう!! }
さて、文章にすると簡単なんですが、ここまで来るのに3時間かかりました。
何がまずかったのか?
HashSetって、名前の通りハッシュ値をベースに一意性を保証していますよね。
御存知の通りだと思いますが。
では、Parent#hashCodeってどういう実装になってましたっけ?
public int hashCode() { return HashCodeBuilder.reflectionHashCode(this); }
...んん〜??
HashCodeBuilderは、引数で指定したインスタンスの全フィールドを使ってハッシュ値を計算します。
ということは、ハッシュコードを計算している時点で遅延ロードしに行っているのか…
/(^o^)\ナンテコッタイ
...ここまで来るのに5時間かかりました。いやこれでも結構頑張ったんですよ僕。
結論
遅延ロードする子エンティティを持つエンティティはhashCodeを実装してはいけません。
Setに詰めたり、HashMapのキーとして使うならば。
ただ、SetとかHashMapなんてほぼJavaの基本型と言っても過言ではないくらいによく使うものだし、
プロジェクトの規模がそこそこだと「エンティティをSetにつめるんじゃねぇ!」というお達しは、
末端まで届かない可能性大です。
とりあえず今回はこんなふうにしときました。
オブジェクトIDだけをみて判定する方法です。まあコレで概ねうまく行っているのでよしとします。
@Entity public class Parent { ... public int hashCode() { return getId() ^ 17; } public boolean equals(Object that) { if (this == that) return true; if (that == null) return false; if (!(that instanceof Parent)) return false; if (this.getId() == null) return false; // 比較しても無意味なのでfalseとする return this.getId() == ((Parent) that).getId(); } }