JPA hashCode()/ equals()のジレンマ


311

JPAエンティティーと、JPAエンティティークラスにどの/ 実装を使用する必要がある について、ここでいくつかの議論がありました。それらのほとんど(すべてではない)がHibernateに依存していますが、JPA-implementation-neutrally(ちなみに私はEclipseLinkを使用しています)について説明したいと思います。hashCode()equals()

考えられるすべての実装には、以下に関する独自の長所短所があります。

  • hashCode()/equals()契約準拠のため(不変性)List/ Setオペレーション
  • 同一のオブジェクト(たとえば、異なるセッションから、遅延ロードされたデータ構造からの動的プロキシ)を検出できるかどうか
  • エンティティがデタッチ(または非永続)状態で正しく動作するかどうか

私が見る限り、3つのオプションがあります

  1. それらを上書きしないでください。頼りにしObject.equals()Object.hashCode()
    • hashCode()/ equals()仕事
    • 同一のオブジェクトを識別できない、動的プロキシの問題
    • エンティティの分離に問題はありません
  2. 主キーに基づいてそれらを上書きする
    • hashCode()/ equals()壊れている
    • 正しいID(すべての管理対象エンティティ)
    • 分離されたエンティティに関する問題
  3. Business-Idに基づいてそれらをオーバーライドします(非主キーフィールド。外部キーはどうですか?)
    • hashCode()/ equals()壊れている
    • 正しいID(すべての管理対象エンティティ)
    • エンティティの分離に問題はありません

私の質問は:

  1. オプションやプロ/コンポイントを逃しましたか?
  2. どのオプションを選択しましたか、またその理由は何ですか?



更新1:

「によってhashCode()/ equals()壊れている」、私はその連続した意味hashCode()(正しく実装時)の意味で壊れされていない、異なる値を返す可能性があります呼び出しをObjectAPIドキュメント、しかしから変更されたエンティティを取得しようとしたときにこれは問題を引き起こすMapSetまたは他のハッシュベースCollection。その結果、JPA実装(少なくともEclipseLink)が正しく動作しない場合があります。

更新2:

あなたの答えをありがとう-それらのほとんどは驚くべき品質を持っています。
残念ながら、どのアプローチが実際のアプリケーションに最適か、または自分のアプリケーションに最適なアプローチをどのように決定するかはまだわかりません。だから、私は質問を開いたままにし、いくつかの議論や意見を期待します。


4
「hashCode()/ equals()が壊れている」とはどういう意味ですか理解できません
nanda

4
その意味でそれらは「壊れた」ものではなく、オプション2と3と同じように、同じ戦略を使用してequals()とhashCode()の両方を実装します。
matt b

11
これはオプション3には当てはまりません。hashCode()とequals()は同じ基準を使用する必要があるため、フィールドのいずれかが変更された場合、hashcode()メソッドは同じインスタンスに対して以前とは異なる値を返します。 equals()も同様です。hashcode()javadocの文の2番目の部分を省略しました。Javaアプリケーションの実行中に同じオブジェクトで2回以上呼び出された場合、情報がない場合、hashCodeメソッドは常に同じ整数を返す必要があります。オブジェクトのイコール比較で使用されるものが変更されます。
matt b

1
実際、文のその部分は反対を意味します- 実装でhashcode()使用されるフィールドがequals()変更されない限り、同じオブジェクトインスタンスを呼び出すと同じ値が返されます。言い換えると、クラスに3つのフィールドがあり、equals()メソッドがそれらの2つだけを使用してインスタンスの同等性を判断する場合、hashcode()それらのフィールドの値の1つを変更すると、戻り値が変化することが期待できます。このオブジェクトインスタンスは、古いインスタンスが表していた値と「等しくない」ことになります。
マットb

2
「マップ、セット、またはその他のハッシュベースのコレクションから変更されたエンティティを取得しようとしたときの問題」...これは「変更されたエンティティをHashMap、HashSet、またはその他のハッシュベースのコレクションから取得しようとしたときの問題」
nanda

回答:


122

この非常に素晴らしい記事を読んでくださいHibernateにあなたのアイデンティティを盗まないでください

記事の結論は次のようになります。

オブジェクトがデータベースに永続化されている場合、オブジェクトIDを正しく実装するのは一見困難です。ただし、問題は、オブジェクトが保存される前にIDなしで存在できるようにすることから完全に生じます。これらの問題は、HibernateなどのオブジェクトリレーショナルマッピングフレームワークからオブジェクトIDを割り当てる責任を取り除くことで解決できます。代わりに、オブジェクトがインスタンス化されるとすぐにオブジェクトIDを割り当てることができます。これにより、オブジェクトIDがシンプルでエラーのないものになり、ドメインモデルで必要なコードの量が減ります。


21
いいえ、それは良い記事ではありません。これは、この件に関する驚くべきすばらしい記事であり、すべてのJPAプログラマーにとって必読の記事です。+1!
トムアンダーソン

2
うん、私は同じソリューションを使用しています。DBにIDを生成させないことには、オブジェクトを作成し、永続化する前にそれを参照する他のオブジェクトを既に作成できるなど、他の利点もあります。これにより、クライアントサーバーアプリでのレイテンシと複数のリクエスト/レスポンスサイクルを削除できます。そのようなソリューションのインスピレーションが必要な場合は、私のプロジェクトをチェックしてください:suid.jssuid-server-java。基本的にはsuid.jsIDブロックをフェッチし、suid-server-javaそこからクライアント側を取得して使用できます。
Stijn de Witt

2
これは単に異常です。私は内部で休止状態の作業に不慣れで、単体テストを作成していて、オブジェクトを変更した後、セットからオブジェクトを削除できないことがわかり、ハッシュコードの変更が原因であると結論付けましたが、方法を理解できませんでした解決する。シンプルでゴージャスな品です!
XMight 2016

素晴らしい記事です。ただし、リンクを初めて見る人にとっては、ほとんどのアプリケーションにとってはやり過ぎかもしれないことをお勧めします。このページにリストされている他の3つのオプションは、多かれ少なかれ問題を複数の方法で解決するはずです。
HopeKing 2017

1
Hibernate / JPAは、エンティティのequalsおよびhashcodeメソッドを使用して、レコードがデータベースにすでに存在するかどうかを確認しますか?
Tushar Banne

64

equals / hashcodeを常にオーバーライドし、ビジネスIDに基づいて実装します。私にとって最も合理的な解決策のようです。次のリンクを参照してください

これらすべてをまとめるために、equals / hashCodeを処理するさまざまな方法で機能する、または機能しないもののリストを次に示します。 ここに画像の説明を入力してください

編集

これが私にとってうまくいく理由を説明するには:

  1. 私のJPAアプリケーションでは通常、ハッシュベースのコレクション(HashMap / HashSet)を使用しません。必要に応じて、UniqueListソリューションを作成することを好みます。
  2. 実行時にビジネスIDを変更することは、どのデータベースアプリケーションにとってもベストプラクティスではないと思います。他の解決策がないまれなケースでは、要素を削除してハッシュベースのコレクションに戻すなどの特別な処理を行います。
  3. 私のモデルでは、コンストラクターにビジネスIDを設定し、そのためのセッターを提供していません。JPA実装に、プロパティーではなくフィールドを変更させる。
  4. UUIDソリューションはやり過ぎのようです。自然なビジネスIDを持っているのに、なぜUUIDなのですか?結局、データベースにビジネスIDの一意性を設定します。では、なぜデータベースの各テーブルに3つのインデックスがあるのでしょうか。

1
しかし、このテーブルには、「リスト/セットで機能する」5番目の行がありません(OneToManyマッピングからセットの一部であるエンティティを削除する場合)。hashCode( )契約に違反する変更。
MRalwasser

質問のコメントを参照してください。
Equals

1
@MRalwasser:私はあなたが正しいことを意味すると思います、違反されているのはequals / hashCode()コントラクト自体ではありません。しかし、可変のequals / hashCodeはSetコントラクトで問題を引き起こします。
Chris Lercher、2011

3
@MRalwasser:ハッシュコードは、ビジネスIDが変更された場合にのみ変更できます。重要なのは、ビジネスID 変更されないことです。したがって、ハッシュコードは変更されません。これは、ハッシュされたコレクションで完全に機能します。
トムアンダーソン

1
自然なビジネスキーがない場合はどうなりますか?たとえば、グラフ描画アプリケーションで2次元のポイントPoint(X、Y)の場合はどうでしょうか。そのポイントをエンティティとしてどのように保存しますか?
jhegedus 14

35

通常、エンティティには2つのIDがあります。

  1. 永続化レイヤー専用です(永続化プロバイダーとデータベースがオブジェクト間の関係を把握できるようにするため)。
  2. 私たちのアプリケーションのニーズのためである(equals()hashCode()特に)

見てください:

@Entity
public class User {

    @Id
    private int id;  // Persistence ID
    private UUID uuid; // Business ID

    // assuming all fields are subject to change
    // If we forbid users change their email or screenName we can use these
    // fields for business ID instead, but generally that's not the case
    private String screenName;
    private String email;

    // I don't put UUID generation in constructor for performance reasons. 
    // I call setUuid() when I create a new entity
    public User() {
    }

    // This method is only called when a brand new entity is added to 
    // persistence context - I add it as a safety net only but it might work 
    // for you. In some cases (say, when I add this entity to some set before 
    // calling em.persist()) setting a UUID might be too late. If I get a log 
    // output it means that I forgot to call setUuid() somewhere.
    @PrePersist
    public void ensureUuid() {
        if (getUuid() == null) {
            log.warn(format("User's UUID wasn't set on time. " 
                + "uuid: %s, name: %s, email: %s",
                getUuid(), getScreenName(), getEmail()));
            setUuid(UUID.randomUUID());
        }
    }

    // equals() and hashCode() rely on non-changing data only. Thus we 
    // guarantee that no matter how field values are changed we won't 
    // lose our entity in hash-based Sets.
    @Override
    public int hashCode() {
        return getUuid().hashCode();
    }

    // Note that I don't use direct field access inside my entity classes and
    // call getters instead. That's because Persistence provider (PP) might
    // want to load entity data lazily. And I don't use 
    //    this.getClass() == other.getClass() 
    // for the same reason. In order to support laziness PP might need to wrap
    // my entity object in some kind of proxy, i.e. subclassing it.
    @Override
    public boolean equals(final Object obj) {
        if (this == obj)
            return true;
        if (!(obj instanceof User))
            return false;
        return getUuid().equals(((User) obj).getUuid());
    }

    // Getters and setters follow
}

編集:setUuid()メソッドの呼び出しに関する私のポイントを明確にするため。典型的なシナリオは次のとおりです。

User user = new User();
// user.setUuid(UUID.randomUUID()); // I should have called it here
user.setName("Master Yoda");
user.setEmail("yoda@jedicouncil.org");

jediSet.add(user); // here's bug - we forgot to set UUID and 
                   //we won't find Yoda in Jedi set

em.persist(user); // ensureUuid() was called and printed the log for me.

jediCouncilSet.add(user); // Ok, we got a UUID now

テストを実行してログ出力を確認したら、問題を修正します。

User user = new User();
user.setUuid(UUID.randomUUID());

あるいは、別のコンストラクターを提供することもできます。

@Entity
public class User {

    @Id
    private int id;  // Persistence ID
    private UUID uuid; // Business ID

    ... // fields

    // Constructor for Persistence provider to use
    public User() {
    }

    // Constructor I use when creating new entities
    public User(UUID uuid) {
        setUuid(uuid);
    }

    ... // rest of the entity.
}

だから私の例は次のようになります:

User user = new User(UUID.randomUUID());
...
jediSet.add(user); // no bug this time

em.persist(user); // and no log output

私はデフォルトのコンストラクターとセッターを使用していますが、2つのコンストラクターのアプローチの方が適している場合があります。


2
私は、これが正しくて良い解決策だと信じています。通常、整数はデータベースインデックスでuuidよりもパフォーマンスが高いため、パフォーマンス上の利点が少しある場合もあります。しかし、それとは別に、おそらく現在の整数idプロパティを削除し、それを(アプリケーションが割り当てた)uuidで置き換えることができますか?
Chris Lercher、2011

4
これは、JVMの等価性と永続性の等価性のデフォルトのhashCode/ equalsメソッドを使用する場合とどう違うのidですか?これは私にはまったく意味がありません。
Behrang Saeedzadeh

2
複数のエンティティオブジェクトがデータベースの同じ行を指している場合に機能します。Objectさんがequals()戻ってくるfalseこのケースでは。UUIDベースのequals()戻り値true
AndrewАндрейЛисточкин

4
-1-2つのIDを使用する理由がまったくないので、2種類のIDがあります。これは完全に無意味であり、潜在的に有害なようです。
トムアンダーソン

1
私が好むものを指さずにソリューションを批判して申し訳ありません。つまり、オブジェクトに単一のIDフィールドを与え、それに基づいてequalsとhashCodeを実装し、データベースに保存するときではなく、オブジェクトの作成時にその値を生成します。このようにして、オブジェクトのすべての形式が同じように機能します(非永続、永続、および分離)。Hibernateプロキシ(または同様の)も正しく動作するはずです。equalsとhashCode呼び出しを処理するためにハイドレートする必要さえないと思います。
トムアンダーソン

31

私はすでに、これらの3つの州すべてを異なるプロジェクトで使用しました。そして、私はオプション1が現実のアプリで最も実用的であると私の意見では言わなければなりません。私の経験では、hashCode()/ equals()の互換性を破ると、エンティティがコレクションに追加された後に等価性の結果が変化する状況に陥るたびに、多くのクレイジーなバグが発生します。

しかし、さらにオプションがあります(長所と短所も一緒に):


A)ハッシュコード/のセットに基づいて等しく不変NOT NULLコンストラクタに割り当てられ、フィールド

(+)3つの基準すべてが保証されている

(-)新しいインスタンスを作成するには、フィールド値を使用できる必要があります

(-)そのいずれかを変更する必要がある場合、処理が複雑になります。


b)hashCode / equalsは、JPAの代わりに(コンストラクターで)アプリケーションによって割り当てられた主キーに基づいています。

(+)3つの基準すべてが保証されている

(-)DBシーケンスのような単純で信頼性の高いID生成状態を利用できない

(-)分散環境(クライアント/サーバー)またはアプリサーバークラスターで新しいエンティティを作成する場合は複雑


c)エンティティのコンストラクタによって割り当てられたUUIDに基づくhashCode / equals

(+)3つの基準すべてが保証されている

(-)UUID生成のオーバーヘッド

(-)使用されるアルゴリズムによっては、同じUUIDが2度使用されるリスクが少しある可能性があります(DB上の一意のインデックスによって検出される場合があります)


私はオプション1アプローチCも好きです。絶対に必要になるまで何もしないでください。それはより機敏なアプローチです。
アダム・ゲント

2
オプション(b)の+1。IMHO、エンティティに自然なビジネスIDがある場合、それはデータベースの主キーでもあるはずです。これはシンプルでわかりやすい、優れたデータベース設計です。そのようなIDがない場合は、代理キーが必要です。これをオブジェクトの作成時に設定すると、他のすべてが簡単になります。人々は自然キーを使用していない、とき、それはだ、彼らがトラブルに巻き込まれることを早期に代理キーを生成しません。実装の複雑さについては-はい、いくつかあります。しかし、実際にはそれほど多くはなく、すべてのエンティティに対して一度解決する非常に一般的な方法で実行できます。
トムアンダーソン、

私もオプション1を好みますが、完全な等価性をアサートするためのユニットテストの記述方法は大きな問題ですCollection
OODウォーターボール


29

equals()/hashCode()セットに使用したい場合、同じエンティティがそこに一度しか存在できないという意味では、オプションは1つしかありません。それは、オプション2です。これは、エンティティの主キーが本質的に変更されないためです(誰かが実際に更新した場合)それ、それはもう同じ実体ではありません)

あなたは文字通りそれを取るべきです:あなたequals()/hashCode()は主キーに基づいているので、主キーが設定されるまでこれらのメソッドを使用してはいけません。そのため、主キーが割り当てられるまで、エンティティをセットに配置しないでください。(はい、UUIDと同様の概念は主キーを早期に割り当てるのに役立ちます。)

いわゆる「ビジネスキー」には、変更できるという厄介な欠点がありますが、理論的にはオプション3でそれを実現することも可能です。「既に挿入されているエンティティをセットから削除するだけです( s)、それらを再度挿入します。」それは本当です-しかし、それはまた、分散システムでは、データが挿入されたすべての場所で絶対に行われることを確認する必要があることを意味します(そして、更新が実行されることを確認する必要があります、他のことが起こる前に)。特に一部のリモートシステムに現在アクセスできない場合は、高度な更新メカニズムが必要になります...

オプション1は、セット内のすべてのオブジェクトが同じHibernateセッションからのものである場合にのみ使用できます。Hibernateのドキュメントでは、これを13.1.3章で非常に明確にしていますオブジェクトの同一性を考慮する

セッション内では、アプリケーションは==を使用してオブジェクトを安全に比較できます。

ただし、Sessionの外部で==を使用するアプリケーションは、予期しない結果をもたらす可能性があります。これは、予期しない場所でも発生する可能性があります。たとえば、2つの分離されたインスタンスを同じセットに入れると、両方が同じデータベースIDを持つ可能性があります(つまり、それらは同じ行を表します)。ただし、JVM IDは、定義により、切り離された状態のインスタンスに対して保証されません。開発者は、永続クラスのequals()メソッドとhashCode()メソッドをオーバーライドし、独自のオブジェクト等価の概念を実装する必要があります。

それはオプション3を支持して議論し続けています:

注意点が1つあります。平等を実装するためにデータベース識別子を使用しないでください。一意の、通常は不変の属性の組み合わせであるビジネスキーを使用します。一時オブジェクトを永続化すると、データベース識別子が変更されます。一時的なインスタンス(通常はデタッチされたインスタンスと共に)がセットに保持されている場合、ハッシュコードを変更するとセットのコントラクトが壊れます。

これは、真であるならば、あなた

  • 早期にIDを割り当てることができない(UUIDを使用するなど)
  • それでも、オブジェクトが一時的な状態にある間は、絶対にオブジェクトをセットに配置する必要があります。

それ以外の場合は、オプション2を自由に選択できます。

次に、相対的な安定性の必要性について言及します。

ビジネスキーの属性は、データベースの主キーほど安定している必要はありません。オブジェクトが同じセット内にある限り、安定性を保証する必要があります。

これは正しいです。これに関して私が目にする実際的な問題は、絶対的な安定性を保証できない場合、「オブジェクトが同じセット内にある限り」どのようにして安定性を保証できるでしょうか。いくつかの特別なケース(会話にのみセットを使用してからそれを捨てるなど)を想像できますが、これの一般的な実用性には疑問があります。


短縮版:

  • オプション1は、単一セッション内のオブジェクトでのみ使用できます。
  • 可能な場合は、オプション2を使用します(PKが割り当てられるまでオブジェクトをセットで使用できないため、できるだけ早くPKを割り当てます)。
  • 相対的な安定性を保証できる場合は、オプション3を使用できますが、これには注意してください。

主キーが決して変更されないというあなたの仮定は誤りです。たとえば、Hibernateは、セッションが保存されたときにのみ主キーを割り当てます。そのため、hashCodeとして主キーを使用する場合、最初にオブジェクトを保存する前と最初に保存した後のhashCode()の結果は異なります。さらに悪いことに、セッションを保存する前に、新しく作成された2つのオブジェクトは同じhashCodeを持ち、コレクションに追加されると互いに上書きする可能性があります。そのアプローチを使用するには、オブジェクトの作成時にすぐに保存/フラッシュを強制する必要があることに気づくかもしれません。
ウィリアム・ビリングスリー、

2
@William:エンティティの主キーは変更されません。マップされたオブジェクトのidプロパティは変更される可能性があります。これは、説明したように、特に一時オブジェクトが永続化されたときに発生します。equals / hashCodeメソッドについて私が言った私の回答の部分を注意深く読んでください:「主キーが設定されるまで、これらのメソッドを使用してはなりません。」
Chris Lercher、

完全に同意する。オプション2を使用すると、スーパークラスのequals /ハッシュコードを分解して、すべてのエンティティで再利用することもできます。
Theo

+1 JPAは初めてですが、ここでのコメントや回答の一部は、「主キー」という用語の意味が理解されていないことを示しています。
Raedwald、2012年

16
  1. ビジネスキーがある場合は、equals/に使用する必要がありますhashCode
  2. ビジネスキーがない場合は、デフォルトのObjectequalsとhashCode実装のままにしないでください。これは、ユーザーmergeとエンティティの後で機能しないためです。
  3. この投稿で提案されているエンティティ識別子を使用できます。唯一の問題は、次のhashCodeように常に同じ値を返す実装を使用する必要があることです。

    @Entity
    public class Book implements Identifiable<Long> {
    
        @Id
        @GeneratedValue
        private Long id;
    
        private String title;
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Book)) return false;
            Book book = (Book) o;
            return getId() != null && Objects.equals(getId(), book.getId());
        }
    
        @Override
        public int hashCode() {
            return 31;
        }
    
        //Getters and setters omitted for brevity
    }

どちらが良いですか:(1)onjava.com/pub/a/onjava/2006/09/13/…または(2)vladmihalcea.com/…?ソリューション(2)は(1)よりも簡単です。それで、なぜ私は(1)を使うべきですか?両方の効果は同じですか?どちらも同じソリューションを保証しますか?
nimo23

そしてあなたの解決策:同じインスタンス間で「hashCode値は変化しない」。これは、比較されている(ソリューション(1)からの)「同じ」uuidと同じ動作をします。私は正しいですか?
nimo23

1
そして、データベースにUUIDを格納し、レコードとバッファープールのフットプリントを増やしますか?これは、長期的には、固有のhashCodeよりもパフォーマンスの問題を引き起こす可能性があると思います。他のソリューションについては、それをチェックアウトして、すべてのエンティティの状態遷移にわたって一貫性を提供するかどうかを確認できます。これをチェックするテストはGitHubで見つけることができます。
Vlad Mihalcea

1
不変のビジネスキーを持っている場合、hashCodeはそれを使用でき、複数のバケットを利用することになるので、持っている場合は使用する価値があります。それ以外の場合は、私の記事で説明されているようにエンティティ識別子を使用します。
Vlad Mihalcea

1
気に入ってもらってうれしいです。JPAとHibernateに関する記事がさらに数百あります
Vlad Mihalcea

10

ビジネスキー(オプション3)を使用するのが最も一般的に推奨されるアプローチ(Hibernateコミュニティwiki、「Java Persistence with Hibernate」p。398)ですが、これが主に使用されている方法ですが、Hibernateのバグがあり、熱心にフェッチされますセット:HHH-3799。この場合、Hibernateは、フィールドが初期化される前にエンティティをセットに追加できます。なぜこのバグが注目されなくなったのかはわかりません。推奨されているビジネスキーのアプローチが本当に問題になるからです。

問題の核心は、equalsとhashCodeは不変状態(Odersky et al。を参照)に基づいている必要があり、Hibernateが管理する主キーを持つHibernateエンティティにはそのような不変状態がないということです。一時オブジェクトが永続的になると、主キーはHibernateによって変更されます。ビジネスキーは、初期化の過程でオブジェクトをハイドレートするときにHibernateによっても変更されます。

これにより、オプション1のみが残されます。オブジェクトIDに基づいてjava.lang.Object実装を継承するか、James Brundegeが提案するアプリケーション管理の主キーを使用します。 「HibernateがIDをないよう」で(すでにStijn Geukensの回答で参照されています) )および「オブジェクト生成:Hibernate統合へのより良いアプローチ」の Lance Arlausによる。

オプション1の最大の問題は、デタッチされたインスタンスを.equals()を使用して永続インスタンスと比較できないことです。しかし、それは問題ありません。equalsとhashCodeのコントラクトは、各クラスの等価性の意味を決定する開発者に任されています。つまり、equalsとhashCodeをObjectから継承させます。デタッチされたインスタンスを永続インスタンスと比較する必要がある場合は、その目的のために、おそらくboolean sameEntityまたはboolean dbEquivalentまたはで、新しいメソッドを明示的に作成できますboolean businessEquals


5

アンドリューの答えに同意します。アプリケーションでも同じことを行いますが、UUIDをVARCHAR / CHARとして保存する代わりに、2つの長い値に分割します。UUID.getLeastSignificantBits()およびUUID.getMostSignificantBits()を参照してください。

もう1つ検討する必要があるのは、UUID.randomUUID()の呼び出しはかなり遅いため、永続化中やequals()/ hashCode()の呼び出し中など、必要な場合にのみUUIDを遅延生成することを検討したい場合があることです。

@MappedSuperclass
public abstract class AbstractJpaEntity extends AbstractMutable implements Identifiable, Modifiable {

    private static final long   serialVersionUID    = 1L;

    @Version
    @Column(name = "version", nullable = false)
    private int                 version             = 0;

    @Column(name = "uuid_least_sig_bits")
    private long                uuidLeastSigBits    = 0;

    @Column(name = "uuid_most_sig_bits")
    private long                uuidMostSigBits     = 0;

    private transient int       hashCode            = 0;

    public AbstractJpaEntity() {
        //
    }

    public abstract Integer getId();

    public abstract void setId(final Integer id);

    public boolean isPersisted() {
        return getId() != null;
    }

    public int getVersion() {
        return version;
    }

    //calling UUID.randomUUID() is pretty expensive, 
    //so this is to lazily initialize uuid bits.
    private void initUUID() {
        final UUID uuid = UUID.randomUUID();
        uuidLeastSigBits = uuid.getLeastSignificantBits();
        uuidMostSigBits = uuid.getMostSignificantBits();
    }

    public long getUuidLeastSigBits() {
        //its safe to assume uuidMostSigBits of a valid UUID is never zero
        if (uuidMostSigBits == 0) {
            initUUID();
        }
        return uuidLeastSigBits;
    }

    public long getUuidMostSigBits() {
        //its safe to assume uuidMostSigBits of a valid UUID is never zero
        if (uuidMostSigBits == 0) {
            initUUID();
        }
        return uuidMostSigBits;
    }

    public UUID getUuid() {
        return new UUID(getUuidMostSigBits(), getUuidLeastSigBits());
    }

    @Override
    public int hashCode() {
        if (hashCode == 0) {
            hashCode = (int) (getUuidMostSigBits() >> 32 ^ getUuidMostSigBits() ^ getUuidLeastSigBits() >> 32 ^ getUuidLeastSigBits());
        }
        return hashCode;
    }

    @Override
    public boolean equals(final Object obj) {
        if (obj == null) {
            return false;
        }
        if (!(obj instanceof AbstractJpaEntity)) {
            return false;
        }
        //UUID guarantees a pretty good uniqueness factor across distributed systems, so we can safely
        //dismiss getClass().equals(obj.getClass()) here since the chance of two different objects (even 
        //if they have different types) having the same UUID is astronomical
        final AbstractJpaEntity entity = (AbstractJpaEntity) obj;
        return getUuidMostSigBits() == entity.getUuidMostSigBits() && getUuidLeastSigBits() == entity.getUuidLeastSigBits();
    }

    @PrePersist
    public void prePersist() {
        // make sure the uuid is set before persisting
        getUuidLeastSigBits();
    }

}

まあ、実際にequals()/ hashCode()をオーバーライドする場合は、とにかくすべてのエンティティのUUIDを生成する必要があります(コードで作成したすべてのエンティティを永続化したいと思います)。データベースに初めて保存する前に、一度だけ実行します。その後、UUIDは永続プロバイダーによってロードされます。したがって、私はそれを怠惰に行うことのポイントを見ていません。
AndrewАндрейЛисточкин

私はあなたの他のアイデアが本当に好きなので、私はあなたの答えを賛成しました:データベースにUUIDをペアの数値として保存し、equals()メソッド内の特定の型にキャストしない-それは本当に素晴らしいです!私は間違いなくこれらの2つのトリックを将来使用します。
AndrewАндрейЛисточкин

1
賛成票をありがとう。UUIDを遅延して初期化する理由は、HashMapに入れられない、または永続化されない多くのエンティティを作成するアプリにありました。そのため、オブジェクトを作成すると、パフォーマンスが100倍低下しました(10万個)。したがって、必要な場合にのみ、UUIDを初期化します。MySqlで128ビットの数値が適切にサポートされていれば、idにもUUIDを使用でき、auto_incrementは関係ありません。
ドリュー

ああなるほど。私の場合、対応するエンティティがコレクションに配置されない場合は、UUIDフィールドも宣言しません。欠点は、後で実際にそれらをコレクションに配置する必要があることが判明したため、追加する必要がある場合があることです。これは開発中に時々発生しますが、幸いなことに、顧客への最初の展開後には発生しなかったため、大した問題ではありませんでした。システムの稼働後にそれが発生する場合は、dbの移行が必要になります。レイジーUUIDは、このような状況で非常に役立ちます。
AndrewАндрейЛисточкин

状況でパフォーマンスが重要な問題である場合は、Adamが彼の回答で提案したより高速なUUIDジェネレーターも試してみてください。
AndrewАндрейЛисточкин

3

私よりはるかに賢い他の人々がすでに指摘したように、そこには多くの戦略があります。適用された設計パターンの大部分が成功への道を切り開かそうとしているのは事実のようです。これらは、特殊なコンストラクターとファクトリーメソッドを使用したコンストラクターの呼び出しを完全に妨げない限り、コンストラクターへのアクセスを制限します。確かに、それは明確なAPIで常に快適です。しかし、唯一の理由が、equals-およびhashcodeオーバーライドをアプリケーションと互換性を持つようにすることである場合、それらの戦略はKISS(Keep It Simple Stupid)に準拠しているかどうか疑問に思います。

私にとっては、idを調べることで、equalsとhashcodeをオーバーライドするのが好きです。これらのメソッドでは、IDがnullでないことを要求し、この動作を適切に文書化します。したがって、別の場所に彼を格納する前に新しいエンティティを永続化することは、開発者の契約になります。この契約を守らないアプリケーションは、1分以内に(できれば)失敗します。

ただし、エンティティが異なるテーブルに格納されており、プロバイダーが主キーの自動生成戦略を使用している場合は、エンティティタイプ間で主キーが重複します。そのような場合は、実行時の型をObject#getClass()の呼び出しと比較して、当然2つの異なる型が等しいと見なされることを不可能にします。それはほとんどの場合私にぴったりです。


DBシーケンス(Mysqlなど)が欠落している場合でも、シーケンスをシミュレートすることができます(たとえば、テーブルhibernate_sequence)。したがって、常にテーブル間で一意のIDを取得できます。+++しかし、あなたはそれを必要としません。Object#getClass() H.プロキシのため、呼び出しは良くありません。召しHibernate.getClass(o)は役立ちますが、異なる種類のエンティティの平等の問題は残っています。canEqualを使用するソリューションがあります。少し複雑ですが使用可能です。通常は必要ないことに同意しました。+++ null IDでeq / hcをスローすると契約に違反しますが、それは非常に実用的です。
maaartinus

2

ここにはすでに非常に有益な答えが明らかにありますが、私たちが何をするかをお話しします。

何もしません(つまり、オーバーライドしません)。

コレクションでequals / hashcodeが必要な場合は、UUIDを使用します。コンストラクタでUUIDを作成するだけです。UUIDにはhttp://wiki.fasterxml.com/JugHomeを使用します。UUIDは、少し賢いCPUですが、シリアライゼーションやdbアクセスに比べて安価です。


1

これらの議論を知っていて、正しいことを知るまで何もしない方が良いと思ったので、私は過去にオプション1を常に使用していました。これらのシステムはすべて正常に実行されています。

ただし、次回はオプション2を試す場合があります-データベースで生成されたIDを使用します。

IDが設定されていない場合、ハッシュコードと等号はIllegalStateExceptionをスローします。

これにより、保存されていないエンティティに関連する微妙なエラーが予期せず表示されるのを防ぎます。

このアプローチについて人々はどう思いますか?


1

ビジネスキーのアプローチは私たちには適していません。我々は、DBが生成使用ID、一時的な過渡tempIdオーバーライドジレンマを解決するために等しい()/ハッシュコードを()。すべてのエンティティはエンティティの子孫です。長所:

  1. DBに追加フィールドはありません
  2. 子孫エンティティでの余分なコーディングなし、すべての人への1つのアプローチ
  3. パフォーマンスの問題はありません(UUIDなど)、DB Idの生成
  4. ハッシュマップに問題はありません(equal&などの使用を覚えておく必要はありません)
  5. 新しいエンティティのハッシュコードは、永続化しても時間内に変更されません

短所:

  1. 永続化されていないエンティティのシリアライズとデシリアライズに問題がある可能性があります
  2. 保存されたエンティティのハッシュコードは、DBからの再読み込み後に変更される場合があります
  3. 常に異なると見なされる永続化されたオブジェクトではない(多分これは正しいですか?)
  4. ほかに何か?

コードを見てください:

@MappedSuperclass
abstract public class Entity implements Serializable {

    @Id
    @GeneratedValue
    @Column(nullable = false, updatable = false)
    protected Long id;

    @Transient
    private Long tempId;

    public void setId(Long id) {
        this.id = id;
    }

    public Long getId() {
        return id;
    }

    private void setTempId(Long tempId) {
        this.tempId = tempId;
    }

    // Fix Id on first call from equal() or hashCode()
    private Long getTempId() {
        if (tempId == null)
            // if we have id already, use it, else use 0
            setTempId(getId() == null ? 0 : getId());
        return tempId;
    }

    @Override
    public boolean equals(Object obj) {
        if (super.equals(obj))
            return true;
        // take proxied object into account
        if (obj == null || !Hibernate.getClass(obj).equals(this.getClass()))
            return false;
        Entity o = (Entity) obj;
        return getTempId() != 0 && o.getTempId() != 0 && getTempId().equals(o.getTempId());
    }

    // hash doesn't change in time
    @Override
    public int hashCode() {
        return getTempId() == 0 ? super.hashCode() : getTempId().hashCode();
    }
}

1

事前定義されたタイプ識別子とIDに基づく次のアプローチを検討してください。

JPAの具体的な前提:

  • 同じ「タイプ」のエンティティと同じ非null IDは等しいと見なされます
  • 非永続エンティティ(IDがないと仮定)が他のエンティティと等しくなることはありません

抽象エンティティ:

@MappedSuperclass
public abstract class AbstractPersistable<K extends Serializable> {

  @Id @GeneratedValue
  private K id;

  @Transient
  private final String kind;

  public AbstractPersistable(final String kind) {
    this.kind = requireNonNull(kind, "Entity kind cannot be null");
  }

  @Override
  public final boolean equals(final Object obj) {
    if (this == obj) return true;
    if (!(obj instanceof AbstractPersistable)) return false;
    final AbstractPersistable<?> that = (AbstractPersistable<?>) obj;
    return null != this.id
        && Objects.equals(this.id, that.id)
        && Objects.equals(this.kind, that.kind);
  }

  @Override
  public final int hashCode() {
    return Objects.hash(kind, id);
  }

  public K getId() {
    return id;
  }

  protected void setId(final K id) {
    this.id = id;
  }
}

具体的なエンティティの例:

static class Foo extends AbstractPersistable<Long> {
  public Foo() {
    super("Foo");
  }
}

テスト例:

@Test
public void test_EqualsAndHashcode_GivenSubclass() {
  // Check contract
  EqualsVerifier.forClass(Foo.class)
    .suppress(Warning.NONFINAL_FIELDS, Warning.TRANSIENT_FIELDS)
    .withOnlyTheseFields("id", "kind")
    .withNonnullFields("id", "kind")
    .verify();
  // Ensure new objects are not equal
  assertNotEquals(new Foo(), new Foo());
}

ここの主な利点:

  • シンプルさ
  • サブクラスが型のアイデンティティを提供することを保証します
  • プロキシされたクラスでの予測された動作

短所:

  • 各エンティティが呼び出す必要があります super()

ノート:

  • 継承を使用する場合は注意が必要です。たとえば、class Aおよびのインスタンスの等価性class B extends Aアプリケーションの具体的な詳細に依存してもよいです。
  • 理想的には、ビジネスキーをIDとして使用します

あなたのコメントを楽しみにしています。


0

これは、JavaとJPAを使用するすべてのITシステムに共通の問題です。課題は、equals()とhashCode()の実装にとどまらず、組織がエンティティを参照する方法と、クライアントが同じエンティティを参照する方法に影響します。私は自分の考えを表現するために自分のブログを書いたという点でビジネスの鍵を握らないことの十分な苦痛を見てきました。

つまり、RAM以外のストレージに依存せずに生成されるビジネスキーとして、意味のある接頭辞が付いた、短くて人間が読める連続したIDを使用します。Twitterのスノーフレークは非常に良い例です。


0

IMOには、equals / hashCodeを実装するための3つのオプションがあります

  • アプリケーションで生成されたID、つまりUUIDを使用する
  • ビジネスキーに基づいて実装する
  • 主キーに基づいて実装する

アプリケーションで生成されたIDを使用するのが最も簡単なアプローチですが、いくつかの欠点があります

  • 128ビットは32ビットまたは64ビットよりも単純に大きいため、PKとして使用すると結合が遅くなります。
  • 一部のデータが正しいかどうかを自分の目で確認するのはかなり難しいので、「デバッグは難しい」

これらの欠点に対処できる場合、このアプローチを使用してください。

結合の問題を克服するには、UUIDを自然キーとして使用し、シーケンス値を主キーとして使用することができますが、その後、ベースの結合が必要になるため、IDが埋め込まれている構成子エンティティでequals / hashCode実装の問題が発生する可能性があります主キー。子エンティティIDの自然キーと親を参照するための主キーを使用することは、適切な妥協案です。

@Entity class Parent {
  @Id @GeneratedValue Long id;
  @NaturalId UUID uuid;
  @OneToMany(mappedBy = "parent") Set<Child> children;
  // equals/hashCode based on uuid
}

@Entity class Child {
  @EmbeddedId ChildId id;
  @ManyToOne Parent parent;

  @Embeddable class ChildId {
    UUID parentUuid;
    UUID childUuid;
    // equals/hashCode based on parentUuid and childUuid
  }
  // equals/hashCode based on id
}

IMOこれは、すべての欠点を回避し、同時にシステム内部を公開することなく外部システムと共有できる値(UUID)を提供するため、最もクリーンなアプローチです。

ユーザーからのアイデアが良いと期待できる場合はビジネスキーに基づいて実装しますが、いくつかの欠点もあります。

ほとんどの場合、このビジネスキーは、ユーザーが提供するある種のコードであり、それほど頻繁ではありませんが、複数の属性の複合です。

  • 可変長テキストに基づく結合は単に遅いため、結合は遅くなります。一部のDBMSでは、キーが特定の長さを超えると、インデックスの作成に問題が発生する場合があります。
  • 私の経験では、ビジネスキーは変化する傾向があり、それを参照するオブジェクトへのカスケード更新が必要になります。外部システムが参照している場合、これは不可能です

IMOは、ビジネスキーを実装したり、ビジネスキーだけで操作したりしないでください。これは素晴らしいアドオンです。つまり、ユーザーはそのビジネスキーですばやく検索できますが、システムはそれを操作に頼るべきではありません。

主キーに基づいて実装すると問題がありますが、それほど大した問題ではないかもしれません

IDを外部システムに公開する必要がある場合は、私が提案したUUIDアプローチを使用してください。そうでない場合でも、UUIDアプローチを使用できますが、そうする必要はありません。equals / hashCodeでDBMSが生成したIDを使用する際の問題は、IDを割り当てる前に、オブジェクトがハッシュベースのコレクションに追加されている可能性があることに起因しています。

これを回避する明白な方法は、IDを割り当てる前に、ハッシュベースのコレクションにオブジェクトを追加しないことです。すでにIDを割り当てる前に重複排除が必要になる可能性があるため、これが常に可能とは限らないことを理解しています。ハッシュベースのコレクションを引き続き使用するには、IDを割り当てた後でコレクションを再構築する必要があります。

あなたはこのようなことをすることができます:

@Entity class Parent {
  @Id @GeneratedValue Long id;
  @OneToMany(mappedBy = "parent") Set<Child> children;
  // equals/hashCode based on id
}

@Entity class Child {
  @EmbeddedId ChildId id;
  @ManyToOne Parent parent;

  @PrePersist void postPersist() {
    parent.children.remove(this);
  }
  @PostPersist void postPersist() {
    parent.children.add(this);
  }

  @Embeddable class ChildId {
    Long parentId;
    @GeneratedValue Long childId;
    // equals/hashCode based on parentId and childId
  }
  // equals/hashCode based on id
}

私は自分で正確なアプローチをテストしていません。そのため、永続化前イベントと永続化イベントでコレクションを変更する方法がわからないのですが、アイデアは次のとおりです。

  • ハッシュベースのコレクションからオブジェクトを一時的に削除します
  • 持続する
  • ハッシュベースのコレクションにオブジェクトを再度追加する

これを解決する別の方法は、更新/永続化の後にすべてのハッシュベースのモデルを単純に再構築することです。

最後に、それはあなた次第です。私は個人的にシーケンスベースのアプローチをほとんどの場合使用し、外部システムに識別子を公開する必要がある場合にのみUUIDアプローチを使用します。


0

instanceofJava 14の新しいスタイルを使用すると、equalsを1行で実装できます。

@Override
public boolean equals(Object obj) {
    return this == obj || id != null && obj instanceof User otherUser && id.equals(otherUser.id);
}

@Override
public int hashCode() {
    return 31;
}

-1

多くの人にとってUUIDが答えである場合、ビジネスレイヤーのファクトリメソッドを使用してエンティティを作成し、作成時に主キーを割り当ててみませんか?

例えば:

@ManagedBean
public class MyCarFacade {
  public Car createCar(){
    Car car = new Car();
    em.persist(car);
    return car;
  }
}

このようにして、永続化プロバイダーからエンティティのデフォルトの主キーを取得し、hashCode()関数とequals()関数がそれに依存できるようにします。

また、Carのコンストラクターを保護されていると宣言し、ビジネスメソッドでリフレクションを使用してそれらにアクセスすることもできます。このようにして、開発者は新しいでCarをインスタンス化するのではなく、ファクトリーメソッドを使用します。

どうだい?


データベースルックアップを実行するときに、GUIDの生成とパフォーマンスのヒットの両方を実行する場合は、適切に機能するアプローチです。
Michael Wiles

1
車の単体テストはどうですか?この場合、テストのためにデータベース接続が必要ですか?また、ドメインオブジェクトは永続性に依存しないようにする必要があります。
jhegedus 14

-1

私はこの質問に自分で答えようとしましたが、この投稿、特にDREWの投稿を読むまで、見つかった解決策にまったく満足していませんでした。私は彼が怠惰にUUIDを作成し、それを最適に保存する方法が好きでした。

しかし、私はさらに柔軟性を追加したいと思いました。つまり、各ソリューションの利点を持つエンティティの最初の永続化の前にhashCode()/ equals()にアクセスする場合にのみ、遅延UUIDを作成します。

  • equals()は、「オブジェクトが同じ論理エンティティを参照している」ことを意味します
  • データベースIDをできるだけ使用するのは、なぜ作業を2度行うのか(パフォーマンスの懸念)
  • まだ永続化されていないエンティティのhashCode()/ equals()へのアクセス中の問題を回避し、実際に永続化された後も同じ動作を維持する

以下の混合ソリューションに関するフィードバックを本当に感謝します

public class MyEntity { 

    @Id()
    @Column(name = "ID", length = 20, nullable = false, unique = true)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id = null;

    @Transient private UUID uuid = null;

    @Column(name = "UUID_MOST", nullable = true, unique = false, updatable = false)
    private Long uuidMostSignificantBits = null;
    @Column(name = "UUID_LEAST", nullable = true, unique = false, updatable = false)
    private Long uuidLeastSignificantBits = null;

    @Override
    public final int hashCode() {
        return this.getUuid().hashCode();
    }

    @Override
    public final boolean equals(Object toBeCompared) {
        if(this == toBeCompared) {
            return true;
        }
        if(toBeCompared == null) {
            return false;
        }
        if(!this.getClass().isInstance(toBeCompared)) {
            return false;
        }
        return this.getUuid().equals(((MyEntity)toBeCompared).getUuid());
    }

    public final UUID getUuid() {
        // UUID already accessed on this physical object
        if(this.uuid != null) {
            return this.uuid;
        }
        // UUID one day generated on this entity before it was persisted
        if(this.uuidMostSignificantBits != null) {
            this.uuid = new UUID(this.uuidMostSignificantBits, this.uuidLeastSignificantBits);
        // UUID never generated on this entity before it was persisted
        } else if(this.getId() != null) {
            this.uuid = new UUID(this.getId(), this.getId());
        // UUID never accessed on this not yet persisted entity
        } else {
            this.setUuid(UUID.randomUUID());
        }
        return this.uuid; 
    }

    private void setUuid(UUID uuid) {
        if(uuid == null) {
            return;
        }
        // For the one hypothetical case where generated UUID could colude with UUID build from IDs
        if(uuid.getMostSignificantBits() == uuid.getLeastSignificantBits()) {
            throw new Exception("UUID: " + this.getUuid() + " format is only for internal use");
        }
        this.uuidMostSignificantBits = uuid.getMostSignificantBits();
        this.uuidLeastSignificantBits = uuid.getLeastSignificantBits();
        this.uuid = uuid;
    }

「永続化される前にこのエンティティで生成された1日のUUID」とはどういう意味ですか?この場合の例を挙げていただけますか?
jhegedus 14

割り当てられた世代タイプを使用できますか?アイデンティティ生成タイプが必要なのはなぜですか?割り当てよりも利点がありますか?
jhegedus 14

1)新しいMyEntityを作成する、2)リストに追加する、3)次にデータベースに保存する、4)そのエンティティをDBからロードして戻す、5)ロードされたインスタンスがリストにあるかどうかを確認する。私の推測では、そうあるべきだとしてもそうではありません。
jhegedus 14

最初のコメントをありがとうございます。私にははっきりとはわかりませんでした。まず、「永続化される前にこのエンティティで生成された1日のUUID」はタイプミスでした...「ITが永続化される前」は代わりに読み込まれるべきでした。他の発言については、私の解決策をよりよく説明するために投稿をすぐに編集します。
user2083808 2014

-1

実際には、オプション2(主キー)が最も頻繁に使用されているようです。自然で不変のビジネスキーはめったにありません。合成キーの作成とサポートは、状況を解決するには重すぎるため、おそらく起こりません。見てい春-データ-JPA AbstractPersistable(:唯一の実装のHibernate実装用にHibernate.getClass)。

public boolean equals(Object obj) {
    if (null == obj) {
        return false;
    }
    if (this == obj) {
        return true;
    }
    if (!getClass().equals(ClassUtils.getUserClass(obj))) {
        return false;
    }
    AbstractPersistable<?> that = (AbstractPersistable<?>) obj;
    return null == this.getId() ? false : this.getId().equals(that.getId());
}

@Override
public int hashCode() {
    int hashCode = 17;
    hashCode += null == getId() ? 0 : getId().hashCode() * 31;
    return hashCode;
}

HashSet / HashMapで新しいオブジェクトを操作することに注意してください。反対に、オプション1(残りのObject実装)はの直後に壊れmergeます。これは非常に一般的な状況です。

ビジネスキーがなく、REALが新しいエンティティをハッシュ構造で操作する必要があるhashCode場合は、以下のようにVlad Mihalceaが推奨されているように、定数にオーバーライドします。


-2

以下は、Scalaのシンプルな(そしてテスト済みの)ソリューションです。

  • このソリューションは、質問で指定された3つのカテゴリのいずれにも当てはまらないことに注意してください。

  • 私のすべてのエンティティはUUIDEntityのサブクラスであるため、私は自己複製禁止(DRY)の原則に従います。

  • 必要に応じて、(より多くの疑似乱数を使用して)UUID生成をより正確にすることができます。

Scalaコード:

import javax.persistence._
import scala.util.Random

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
abstract class UUIDEntity {
  @Id  @GeneratedValue(strategy = GenerationType.TABLE)
  var id:java.lang.Long=null
  var uuid:java.lang.Long=Random.nextLong()
  override def equals(o:Any):Boolean= 
    o match{
      case o : UUIDEntity => o.uuid==uuid
      case _ => false
    }
  override def hashCode() = uuid.hashCode()
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.