読者です 読者をやめる 読者になる 読者になる

この日記は私的なものであり所属会社の見解とは無関係です。 GitHub: takahashikzn

エンティティオブジェクトはObject#hashCodeを実装してはいけない

EclipseLink


...って言うのは少々大げさなのですが、普段は何気ないメソッドも、
油断をすると牙を向いてきますよ、というお話。


親子関係にある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();
    }
}