web-dev-qa-db-ja.com

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

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

可能なすべての実装には、独自のadvantagesおよびdisadvantagesがあります。

  • hashCode()/equals()contractconformity(不変性)Listname __/Setname__操作
  • identicalオブジェクト(異なるセッション、遅延ロードされたデータ構造からの動的プロキシなど)を検出できるかどうか
  • エンティティがデタッチ(または非永続)状態で正しく動作するかどうか

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

  1. それらをオーバーライドしないでください。 Object.equals()およびObject.hashCode() に依存します。
    • hashCode()/equals() work
    • 同一オブジェクト、動的プロキシの問題を特定できない
    • 独立したエンティティには問題ありません
  2. プライマリキーに基づいて上書きします
    • hashCode()/equals()が壊れています
    • 正しいアイデンティティ(すべての管理対象エンティティ)
    • 独立したエンティティの問題
  3. Business-Id(非プライマリキーフィールド。外部キーはどうですか?)に基づいて、それらをオーバーライドします
    • hashCode()/equals()が壊れています
    • 正しいアイデンティティ(すべての管理対象エンティティ)
    • 独立したエンティティには問題ありません

私の質問は:

  1. オプションや賛否両論のポイントを見逃しましたか?
  2. どのオプションを選択しましたか、またその理由は何ですか?



UPDATE 1:

hashCode()/equals() are broken」とは、連続するhashCode()呼び出しが異なる値を返す可能性があることを意味します。これは、(正しく実装されている場合)ObjectAPIドキュメントの意味では壊れていませんが、 Mapname __、Setname__、またはその他のハッシュベースのCollectionname__。その結果、JPA実装(少なくともEclipseLink)は場合によっては正しく動作しません。

更新2:

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

288
MRalwasser

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

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

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

109
Stijn Geukens

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

これらすべてをまとめると、equals/hashCodeを処理するさまざまな方法で機能する、または機能しないもののリストです。 enter image description here

編集

これがなぜ私にとって役立つのかを説明するには:

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

通常、エンティティには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("[email protected]");

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つのコンストラクターのアプローチがより適している場合があります。

セットにequals()/hashCode()を使用したい場合、同じエンティティが一度しか存在できないという意味で、オプション2のみがあります:オプション2 プライマリキー定義によるエンティティの変更はありません(誰かが実際に更新した場合、それはもはや同じエンティティではありません)

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

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

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

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

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

オプション3を支持して議論を続けています。

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

これは本当ですifあなた

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

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

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

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

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


短縮版:

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

私は個人的に、これらの3つの国家のすべてを異なるプロジェクトで使用しました。私の意見では、オプション1は実際のアプリで最も実用的であると言わなければなりません。 hashCode()/ equals()の適合性を破った経験は、エンティティがコレクションに追加された後に等値の結果が変わる状況で毎回終わるため、多くのクレイジーなバグにつながります。

しかし、さらに長所と短所があります:


a)immutableのセットに基づくhashCode/equals、not nullconstructor assigned、フィールド

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

(-)フィールド値は、新しいインスタンスを作成するために利用可能でなければなりません

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


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

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

(-)DBシーケンスのような単純で信頼性の高いID生成方法を利用することはできません

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


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

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

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

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

27
lweller
  1. ビジネスキー がある場合は、equals/hashCodeに使用する必要があります。
  2. ビジネスキーがない場合は、デフォルトのObject equalsおよびhashCode実装のままにしないでください。これは、mergeおよびentityの後では機能しないためです。
  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
    }
    
12
Vlad Mihalcea

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

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

オプション1のみが残ります。オブジェクトIDに基づいてJava.lang.Object実装を継承するか、 「HibernateがあなたのIDを盗まないようにする」でJames Brundegeによって提案されたアプリケーション管理の主キーを使用します。 (既にStijn Geukensの回答で参照されています)およびLance Arlausによる 「Object Generation:A Better Approach to Hibernate Integration」

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

10
jbyler

アンドリューの答えに同意します。アプリケーションで同じことを行いますが、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();
    }

}
5
Drew

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

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

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

3

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

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

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

2
Adam Gent

ビジネスキーのアプローチは私たちには適していません。 DB生成のID、一時的な一時的なtempIdおよびoverride equal()/ hashcode()を使用してジレンマを解決します。すべてのエンティティは、エンティティの子孫です。長所:

  1. DBに余分なフィールドはありません
  2. 子孫エンティティに余分なコーディングはなく、すべてに対応する1つのアプローチ
  3. パフォーマンスの問題なし(UUIDなど)、DB ID生成
  4. ハッシュマップに問題はありません(等号などの使用を覚えておく必要はありません)
  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
Demel

定義済みのタイプ識別子と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 Aclass B extends Aのインスタンスの等価性は、アプリケーションの具体的な詳細に依存する場合があります。
  • IDとしてビジネスキーを使用するのが理想的です

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

1
aux

これは、JavaおよびJPAを使用するすべてのITシステムで共通の問題です。痛みのポイントは、equals()とhashCode()の実装を超えて広がり、組織がエンティティを参照する方法と、そのクライアントが同じエンティティを参照する方法に影響します。私は 自分のブログ を書いた時点でビジネスキーを持っていないという十分な痛みを見て、自分の意見を表明しました。

要するに、RAM以外のストレージに依存せずに生成されるビジネスキーとして、意味のあるプレフィックスを持つ人間が読み取れる短い連続IDを使用します。 Twitterの Snowflake は非常に良い例です。

0

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

ただし、次回はオプション2を試すことができます-データベース生成IDを使用します。

IDが設定されていない場合、HashcodeとequalsはIllegalStateExceptionをスローします。

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

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

0
Neil Stevens

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

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

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

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

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

結合の問題を克服するには、UUIDを自然キーとして使用し、シーケンス値を主キーとして使用することができますが、結合ベースの主キー。子エンティティ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