ここではJPAエンティティについて somediscussions があり、JPAエンティティクラスにはどのhashCode()
/equals()
実装を使用する必要があります。それらのほとんど(すべてではないにしても)はHibernateに依存していますが、JPA-implementation-neutrallyについて説明したいと思います(ちなみにEclipseLinkを使用しています)。
可能なすべての実装には、独自のadvantagesおよびdisadvantagesがあります。
hashCode()
/equals()
contractconformity(不変性)List
name __/Set
name__操作私が見る限り、3つのオプションがあります:
Object.equals()
およびObject.hashCode()
に依存します。hashCode()
/equals()
workhashCode()
/equals()
が壊れていますhashCode()
/equals()
が壊れています私の質問は:
UPDATE 1:
「hashCode()
/equals()
are broken」とは、連続するhashCode()
呼び出しが異なる値を返す可能性があることを意味します。これは、(正しく実装されている場合)Object
APIドキュメントの意味では壊れていませんが、 Map
name __、Set
name__、またはその他のハッシュベースのCollection
name__。その結果、JPA実装(少なくともEclipseLink)は場合によっては正しく動作しません。
更新2:
ご回答いただきありがとうございます-それらのほとんどは顕著な品質を持っています。
残念ながら、実際のアプリケーションに最適なアプローチはどれか、またはアプリケーションに最適なアプローチをどのように決定するかはまだわかりません。それで、私は質問を開いたままにして、さらなる議論や意見を期待します。
このテーマに関する非常に素晴らしい記事を読んでください: Hibernateがあなたのアイデンティティを盗まないでください 。
記事の結論は次のようになります。
オブジェクトがデータベースに永続化されると、オブジェクトIDを正しく実装するのは一見困難です。ただし、問題は、オブジェクトが保存される前にIDなしで存在できるようにすることに完全に起因します。 HibernateなどのオブジェクトリレーショナルマッピングフレームワークからオブジェクトIDを割り当てる責任を引き受けることにより、これらの問題を解決できます。代わりに、オブジェクトがインスタンス化されるとすぐにオブジェクトIDを割り当てることができます。これにより、オブジェクトIDがシンプルかつエラーフリーになり、ドメインモデルで必要なコードの量が削減されます。
私は常にequals/hashcodeをオーバーライドし、ビジネスIDに基づいて実装します。私にとって最も合理的な解決策のようです。次の link を参照してください。
これらすべてをまとめると、equals/hashCodeを処理するさまざまな方法で機能する、または機能しないもののリストです。
編集:
これがなぜ私にとって役立つのかを説明するには:
通常、エンティティには2つのIDがあります。
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あなた
それ以外の場合は、オプション2を自由に選択できます。
次に、相対的な安定性の必要性に言及しています。
ビジネスキーの属性は、データベースのプライマリキーほど安定している必要はありません。オブジェクトが同じセットにある限り、安定性を保証する必要があります。
これは正しいです。私がこれに関して見ている実際的な問題は、絶対的な安定性を保証できない場合、「オブジェクトが同じセットにある限り」、どのように安定性を保証できるかということです。いくつかの特別なケース(会話にのみセットを使用し、それを捨てるなど)を想像できますが、これの一般的な実行可能性には疑問があります。
短縮版:
私は個人的に、これらの3つの国家のすべてを異なるプロジェクトで使用しました。私の意見では、オプション1は実際のアプリで最も実用的であると言わなければなりません。 hashCode()/ equals()の適合性を破った経験は、エンティティがコレクションに追加された後に等値の結果が変わる状況で毎回終わるため、多くのクレイジーなバグにつながります。
しかし、さらに長所と短所があります:
a)immutableのセットに基づくhashCode/equals、not null 、constructor assigned、フィールド
(+)3つの基準すべてが保証されています
(-)フィールド値は、新しいインスタンスを作成するために利用可能でなければなりません
(-)その後のいずれかを変更する必要がある場合、処理が複雑になる
b)JPAの代わりに(コンストラクターで)アプリケーションによって割り当てられた主キーに基づくhashCode/equals
(+)3つの基準すべてが保証されています
(-)DBシーケンスのような単純で信頼性の高いID生成方法を利用することはできません
(-)分散環境(クライアント/サーバー)またはアプリサーバークラスターで新しいエンティティが作成される場合は複雑
c)エンティティのコンストラクタによって割り当てられた UUID に基づくhashCode/equals
(+)3つの基準すべてが保証されています
(-)UUID生成のオーバーヘッド
(-)使用されるアルゴリズムに応じて、同じUUIDの2倍が使用されるという少しのリスクがあります(DBの一意のインデックスによって検出される場合があります)
equals
/hashCode
に使用する必要があります。Object
equalsおよびhashCode実装のままにしないでください。これは、merge
およびentityの後では機能しないためです。この投稿で提案されているエンティティ識別子を使用してください 。唯一の問題は、次のように常に同じ値を返す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
}
ビジネスキー(オプション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
のような新しいメソッドを明示的に作成できます。
アンドリューの答えに同意します。アプリケーションで同じことを行いますが、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();
}
}
他の人が私よりもずっと賢いように、すでに多くの戦略があります。ただし、適用されたデザインパターンの大部分が成功への道をハックしようとしているのは事実のようです。これらは、特殊なコンストラクターとファクトリーメソッドを使用して、コンストラクターの呼び出しを完全に妨げない場合、コンストラクターのアクセスを制限します。実際、明確なAPIを使用することは常に快適です。しかし、唯一の理由がequals-およびhashcodeオーバーライドをアプリケーションと互換にすることである場合、これらの戦略がKISS(Keep It Simple Stupid)に準拠しているかどうか疑問に思います。
私にとっては、IDを調べることにより、equalsとhashcodeをオーバーライドするのが好きです。これらのメソッドでは、IDがnullでないことを要求し、この動作を適切に文書化します。したがって、開発者は、新しいエンティティを別の場所に保存する前に永続化することを契約します。この契約を守らないアプリケーションは、数分以内に(できれば)失敗します。
ただし、エンティティが異なるテーブルに格納され、プロバイダーがプライマリキーに自動生成戦略を使用している場合、エンティティタイプ間でプライマリキーが重複することになります。そのような場合は、実行時タイプと Object#getClass() の呼び出しも比較してください。これにより、2つの異なるタイプが等しいと見なされることはもちろん不可能になります。ほとんどの場合、それで十分です。
明らかに非常に有益な答えがここにありますが、私たちが何をするかをお話しします。
何もしません(オーバーライドしません)。
コレクションで動作するためにequals/hashcodeが必要な場合は、UUIDを使用します。コンストラクタでUUIDを作成するだけです。 UUIDには http://wiki.fasterxml.com/JugHome を使用します。 UUIDは、CPUに関しては少し高価ですが、シリアライゼーションおよびdbアクセスと比較すると安価です。
ビジネスキーのアプローチは私たちには適していません。 DB生成のID、一時的な一時的なtempIdおよびoverride equal()/ hashcode()を使用してジレンマを解決します。すべてのエンティティは、エンティティの子孫です。長所:
短所:
コードを見てください:
@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();
}
}
定義済みのタイプ識別子とIDに基づいて、次のアプローチを検討してください。
JPAの特定の前提:
抽象エンティティ:
@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
のインスタンスの等価性は、アプリケーションの具体的な詳細に依存する場合があります。あなたのコメントを楽しみにしています。
私はこれらの議論に気づいていて、正しいことを知るまで何もしない方が良いと考えたため、過去にオプション1を常に使用していました。これらのシステムはすべて正常に実行されています。
ただし、次回はオプション2を試すことができます-データベース生成IDを使用します。
IDが設定されていない場合、HashcodeとequalsはIllegalStateExceptionをスローします。
これにより、保存されていないエンティティに関連する微妙なエラーが予期せず表示されるのを防ぎます。
人々はこのアプローチについてどう思いますか?
IMOには、equals/hashCodeを実装するための3つのオプションがあります
アプリケーションで生成されたIDを使用するのが最も簡単なアプローチですが、欠点がいくつかあります
これらの欠点で作業できる場合は、このアプローチを使用してください。
結合の問題を克服するには、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)を提供するため、最もクリーンなアプローチです。
ユーザーからの素晴らしいアイデアであると期待できる場合、ビジネスキーに基づいて実装しますが、いくつかの欠点もあります
ほとんどの場合、このビジネスキーは、ユーザーが提供する何らかのコードであり、複数の属性の複合体ではありません。
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アプローチを使用します。