Es gab einigeDiskussionen hier über JPA-Entitäten und welche Implementierung von hashCode()
/equals()
für JPA-Entitätsklassen verwendet werden sollte. Die meisten (wenn nicht alle) von ihnen hängen von Hibernate ab, aber ich würde sie gerne JPA-implementierungsneutral diskutieren (ich benutze übrigens EclipseLink).
Alle möglichen Implementierungen haben ihre eigenen Vorteile und Nachteile in Bezug auf:
hashCode()
/equals()
contract conformity (Unveränderlichkeit) für List
name __/Set
OperationenSoweit ich sehen kann, gibt es drei Optionen :
Object.equals()
und Object.hashCode()
hashCode()
/equals()
funktionierenhashCode()
/equals()
sind defekthashCode()
/equals()
sind defektMeine Fragen sind:
UPDATE 1:
Mit "hashCode()
/equals()
are broken" meine ich, dass aufeinanderfolgende hashCode()
-Aufrufe unterschiedliche Werte zurückgeben können, die (bei korrekter Implementierung) nicht im Sinne der API-Dokumentation für Object
beschädigt sind, aber Probleme verursachen, wenn versucht wird, eine geänderte Entität von abzurufen ein Map
name__, Set
oder ein anderer hash-basierter Collection
name__. Folglich funktionieren JPA-Implementierungen (mindestens EclipseLink) in einigen Fällen nicht richtig.
UPDATE 2:
Vielen Dank für Ihre Antworten - die meisten von ihnen haben eine bemerkenswerte Qualität.
Leider bin ich mir immer noch nicht sicher, welcher Ansatz für eine reale Anwendung am besten geeignet ist oder wie ich den besten Ansatz für meine Anwendung ermitteln kann. Also werde ich die Frage offen halten und auf weitere Diskussionen und/oder Meinungen hoffen.
Lesen Sie diesen sehr schönen Artikel zum Thema: Lassen Sie den Hibernate Ihre Identität nicht stehlen .
Die Schlussfolgerung des Artikels lautet wie folgt:
Die Objektidentität ist schwer zu implementieren, wenn Objekte bleiben in einer Datenbank erhalten. Die Probleme bleiben jedoch __. gänzlich davon abzulassen, dass Objekte ohne eine ID existieren, bevor sie .__ sind. Gerettet. Wir können diese Probleme lösen, indem wir die Verantwortung von .__ übernehmen. Zuweisen von Objekt-IDs von objektrelationalen Mapping-Frameworks wie Hibernate. Objekt-IDs können stattdessen bereits mit .__ zugewiesen werden. Objekt wird instanziiert. Dies macht die Objektidentität einfach und fehlerfrei und reduziert die Menge an Code, die im Domänenmodell benötigt wird.
Ich überschreibe immer gleich/Hashcode und implementiere es basierend auf der Geschäfts-ID. Scheint die vernünftigste Lösung für mich. Siehe den folgenden link .
Um das Ganze zusammenzufassen, finden Sie hier eine Auflistung dessen, was mit den verschiedenen Möglichkeiten, mit equals/hashCode umzugehen, funktioniert oder nicht funktioniert:
EDIT:
Um zu erklären, warum das für mich funktioniert:
Wir haben normalerweise zwei IDs in unseren Entitäten:
equals()
und hashCode()
)Schau mal:
@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
}
BEARBEITEN: um meinen Standpunkt bezüglich der Aufrufe der setUuid()
-Methode zu klären. Hier ist ein typisches Szenario:
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
Wenn ich meine Tests durchführe und die Protokollausgabe sehe, behebe ich das Problem:
User user = new User();
user.setUuid(UUID.randomUUID());
Alternativ kann ein separater Konstruktor bereitgestellt werden:
@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.
}
Mein Beispiel würde also so aussehen:
User user = new User(UUID.randomUUID());
...
jediSet.add(user); // no bug this time
em.persist(user); // and no log output
Ich verwende einen Standardkonstruktor und einen Setter, aber es könnte sein, dass zwei Konstruktoren für Sie geeigneter sind.
Wenn Sie equals()/hashCode()
für Ihre Sets verwenden möchten, in dem Sinne, dass die gleiche Entität nur einmal vorhanden sein kann, gibt es nur eine Option: Option 2. Dies liegt daran, dass sich ein Primärschlüssel für eine Entität per Definition nie ändert (wenn jemand ihn tatsächlich aktualisiert, ist er nicht mehr dieselbe Entität).
Sie sollten das wörtlich nehmen: Da Ihr equals()/hashCode()
auf dem Primärschlüssel basiert, dürfen Sie diese Methoden nicht verwenden, bis der Primärschlüssel festgelegt ist. Daher sollten Sie Entitäten erst dann in die Gruppe aufnehmen, wenn ihnen ein Primärschlüssel zugewiesen wurde. (Ja, UUIDs und ähnliche Konzepte können dazu beitragen, Primärschlüssel frühzeitig zuzuweisen.)
Nun ist es theoretisch auch möglich, dies mit Option 3 zu erreichen, obwohl sogenannte "Business-Keys" den Nachteil haben, dass sie sich ändern können: "Alles, was Sie tun müssen, ist, die bereits eingefügten Entities aus dem Set zu löschen ( s) und fügen Sie sie erneut ein. " Das stimmt - aber es bedeutet auch, dass Sie in einem verteilten System sicherstellen müssen, dass dies absolut überall dort erfolgt, wo die Daten eingefügt wurden (und Sie müssen sicherstellen, dass das Update durchgeführt wird , bevor andere Dinge passieren). Sie benötigen einen ausgeklügelten Update-Mechanismus, insbesondere wenn einige Remote-Systeme derzeit nicht erreichbar sind ...
Option 1 kann nur verwendet werden, wenn alle Objekte in Ihren Gruppen aus derselben Ruhezustandssitzung stammen. Die Dokumentation zum Ruhezustand macht dies in Kapitel 13.1.3. Berücksichtigung der Objektidentität :
Innerhalb einer Sitzung kann die Anwendung sicher == zum Vergleichen von Objekten verwenden.
Eine Anwendung, die == außerhalb einer Sitzung verwendet, kann jedoch zu unerwarteten Ergebnissen führen. Dies kann sogar an unerwarteten Stellen auftreten. Wenn Sie beispielsweise zwei getrennte Instanzen in dieselbe Gruppe einfügen, haben beide möglicherweise dieselbe Datenbankidentität (d. H. Sie repräsentieren dieselbe Zeile). Die JVM-Identität ist jedoch per Definition für Instanzen in einem getrennten Zustand nicht garantiert. Der Entwickler muss die Methoden equals () und hashCode () in persistenten Klassen überschreiben und einen eigenen Begriff der Objektgleichheit implementieren.
Sie spricht sich weiterhin für Option 3 aus:
Es gibt eine Einschränkung: Verwenden Sie niemals die Datenbankkennung, um Gleichheit zu implementieren. Verwenden Sie einen Geschäftsschlüssel, der aus einer Kombination eindeutiger, normalerweise unveränderlicher Attribute besteht. Die Datenbankkennung ändert sich, wenn ein transientes Objekt persistent gemacht wird. Wenn sich die vorübergehende Instanz (normalerweise zusammen mit getrennten Instanzen) in einem Set befindet, wird durch Ändern des Hashcodes der Vertrag des Sets verletzt.
Dies ist wahr, wenn Sie
Ansonsten können Sie Option 2 wählen.
Dann wird die Notwendigkeit einer relativen Stabilität erwähnt:
Attribute für Geschäftsschlüssel müssen nicht so stabil sein wie Datenbankprimärschlüssel. Sie müssen nur die Stabilität garantieren, solange sich die Objekte im selben Set befinden.
Das ist richtig. Das praktische Problem, das ich damit sehe, ist: Wenn Sie keine absolute Stabilität garantieren können, wie können Sie Stabilität garantieren, "solange sich die Objekte in derselben Menge befinden". Ich kann mir einige Sonderfälle vorstellen (wie das Verwenden von Sets nur für ein Gespräch und das anschließende Wegwerfen), aber ich würde die allgemeine Praktikabilität davon in Frage stellen.
Kurzfassung:
Ich persönlich habe alle diese drei Strategien bereits in verschiedenen Projekten verwendet. Ich muss sagen, dass Option 1 meiner Meinung nach die praktikabelste in einer realen App ist. Wenn die Erfahrung gemacht wurde, dass die hashCode ()/equals () -Konformität gebrochen wird, führt dies zu vielen verrückten Fehlern, da Sie jedes Mal in Situationen enden, in denen sich das Ergebnis der Gleichheit ändert, nachdem eine Entität zu einer Sammlung hinzugefügt wurde.
Es gibt jedoch weitere Optionen (auch mit ihren Vor- und Nachteilen):
a) hashCode/equals basiert auf einer Menge von immutable, nicht null, Konstruktor zugewiesen, Feldern
(+) Alle drei Kriterien sind garantiert
(-) Feldwerte müssen verfügbar sein, um eine neue Instanz erstellen zu können
(-) erschweren die Handhabung, wenn Sie eine der Einstellungen ändern müssen
b) hashCode/equals basiert auf dem Primärschlüssel, der von der Anwendung (im Konstruktor) anstelle von JPA zugewiesen wird
(+) Alle drei Kriterien sind garantiert
(-) Sie können keine einfachen zuverlässigen ID-Generierungsstrategien wie DB-Sequenzen nutzen
(-) kompliziert, wenn neue Entitäten in einer verteilten Umgebung (Client/Server) oder App-Server-Cluster erstellt werden
c) hashCode/equals basiert auf einem vom Konstruktor der Entität zugewiesenen UUID
(+) Alle drei Kriterien sind garantiert
(-) Overhead der UUID-Generierung
(-) kann ein geringes Risiko sein, dass zweimal die gleiche UUID verwendet wird, abhängig vom verwendeten Algorythm (wird möglicherweise durch einen eindeutigen Index der Datenbank ermittelt)
Obwohl die Verwendung eines Geschäftsschlüssels (Option 3) der am häufigsten empfohlene Ansatz ist ( Hibernate-Community-Wiki , "Java-Persistence with Hibernate", Seite 398). Dies ist das, was wir am häufigsten verwenden. Es gibt jedoch einen Hibernate-Fehler, der dies bricht für abgefragte Sets: HHH-3799 . In diesem Fall kann der Ruhezustand eine Entität zu einer Gruppe hinzufügen, bevor ihre Felder initialisiert werden. Ich bin mir nicht sicher, warum dieser Fehler nicht mehr Beachtung gefunden hat, da er den empfohlenen Business-Key-Ansatz wirklich problematisch macht.
Ich denke, der Kern der Sache ist, dass equals und hashCode auf dem unveränderlichen Zustand basieren sollten (Referenz Odersky et al. ), und eine Hibernate-Entität mit Hibernate-verwaltetem Primärschlüssel hat no einen solchen unveränderlichen Zustand. Der Primärschlüssel wird von Hibernate geändert, wenn ein transientes Objekt persistent wird. Der Geschäftsschlüssel wird auch von Hibernate geändert, wenn ein Objekt während des Initialisierungsprozesses hydratisiert wird.
Es bleibt nur die Option 1 übrig, die Java.lang.Object-Implementierungen basierend auf der Objektidentität zu erben oder einen von James Brundege vorgeschlagenen, von der Anwendung verwalteten Primärschlüssel in "Lassen Sie den Ruhezustand Ihre Identität nicht stehlen" (bereits verwiesen) von Stijn Geukens 'Antwort) und von Lance Arlaus in "Objektgenerierung: Ein besserer Ansatz für die Integration in den Ruhezustand" .
Das größte Problem bei Option 1 besteht darin, dass getrennte Instanzen mit .equals () nicht mit persistenten Instanzen verglichen werden können. Aber das ist in Ordnung; Der Vertrag von equals und hashCode überlässt es dem Entwickler, zu entscheiden, was Gleichheit für jede Klasse bedeutet. Lassen Sie also gleich und hashCode von Object erben. Wenn Sie eine getrennte Instanz mit einer persistenten Instanz vergleichen müssen, können Sie explizit eine neue Methode erstellen, beispielsweise boolean sameEntity
oder boolean dbEquivalent
oder boolean businessEquals
.
equals
/hashCode
verwenden.Object
equals und hashCode belassen, da dies nach merge
und entity nicht funktioniert.Sie können die Entity-ID verwenden, wie in diesem Beitrag vorgeschlagen . Der einzige Haken ist, dass Sie eine hashCode
-Implementierung verwenden müssen, die immer denselben Wert zurückgibt, z.
@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
}
Ich stimme Andrews Antwort zu. Wir machen dasselbe in unserer Anwendung, aber anstatt UUIDs als VARCHAR/CHAR zu speichern, teilen wir sie in zwei lange Werte auf. Siehe UUID.getLeastSignificantBits () und UUID.getMostSignificantBits ().
Zu beachten ist außerdem, dass Aufrufe an UUID.randomUUID () ziemlich langsam sind. Daher sollten Sie die UUID nur langsam generieren, wenn dies erforderlich ist, z.
@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();
}
}
Wie andere Leute schon klüger als ich schon sagte, gibt es eine Vielzahl von Strategien. Es scheint jedoch so zu sein, dass die Mehrzahl der angewandten Entwurfsmuster versuchen, ihren Erfolg zu sichern. Sie beschränken den Konstruktorzugriff, wenn sie Konstruktoraufrufe nicht vollständig mit spezialisierten Konstruktoren und Factory-Methoden behindern. In der Tat ist es immer angenehm mit einer klaren API. Aber wenn der einzige Grund darin besteht, Gleichungen und Hashcode-Überschreibungen mit der Anwendung kompatibel zu machen, frage ich mich, ob diese Strategien mit KISS (Keep It Simple Stupid) übereinstimmen.
Ich mag es, Equals und Hashcode durch die Überprüfung der ID zu überschreiben. Bei diesen Methoden muss die ID nicht null sein und dieses Verhalten gut dokumentieren. So wird es der Entwicklervertrag, eine neue Entität bestehen zu lassen, bevor sie an einem anderen Ort abgelegt wird. Eine Anwendung, die diesen Vertrag nicht einhält, würde innerhalb einer Minute (hoffentlich) fehlschlagen.
Vorsicht: Wenn Ihre Entitäten in verschiedenen Tabellen gespeichert sind und Ihr Provider eine Strategie zur automatischen Generierung des Primärschlüssels verwendet, erhalten Sie über Entitätstypen doppelte Primärschlüssel. Vergleichen Sie in einem solchen Fall auch Laufzeittypen mit einem Aufruf von Object # getClass () , wodurch es natürlich unmöglich wird, dass zwei verschiedene Typen als gleich betrachtet werden. Das passt für mich zum größten Teil gut.
Natürlich gibt es hier bereits sehr informative Antworten, aber ich werde Ihnen sagen, was wir tun.
Wir machen nichts (dh wir überschreiben es nicht).
Wenn wir equals/hashcode benötigen, um für Sammlungen zu arbeiten, verwenden wir UUIDs ..__ Sie erstellen die UUID einfach im Konstruktor. Wir verwenden http://wiki.fasterxml.com/JugHome für UUID. UUID ist etwas teurer als die CPU, aber im Vergleich zu Serialisierung und Datenbankzugriff billig.
Business Keys Ansatz passt nicht zu uns. Wir verwenden von DB generierteID, temporäre transiente tempId und override equal ()/hashcode (), um das Dilemma zu lösen. Alle Entitäten sind Nachkommen der Entität. Pros:
Nachteile:
Schauen Sie sich unseren Code an:
@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();
}
}
Beachten Sie den folgenden Ansatz, der auf der vordefinierten Typenbezeichnung und der ID basiert.
Die spezifischen Annahmen für die JPA:
Die abstrakte Entität:
@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;
}
}
Konkretes Entity-Beispiel:
static class Foo extends AbstractPersistable<Long> {
public Foo() {
super("Foo");
}
}
Testbeispiel:
@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());
}
Hauptvorteile hier:
Nachteile:
super()
aufruftAnmerkungen:
class A
und class B extends A
kann von konkreten Details der Anwendung abhängen.Ich freue mich auf Eure Kommentare.
Dies ist ein häufiges Problem in jedem IT-System, das Java und JPA verwendet. Der Schmerzpunkt geht über die Implementierung von equals () und hashCode () hinaus und beeinflusst, wie sich eine Organisation auf eine Entität bezieht und wie sich ihre Kunden auf dieselbe Entität beziehen. Ich habe genug Schmerzen gehabt, wenn ich bis zu dem Punkt, an dem ich mein eigener Blog schrieb, keinen Geschäftsschlüssel hatte, um meine Ansicht auszudrücken.
Kurz gesagt: Verwenden Sie eine kurze, von Menschen lesbare, sequentielle ID mit sinnvollen Präfixen als Geschäftsschlüssel, die ohne Abhängigkeit von einem anderen Speicher als dem RAM generiert wird. Twitter's Snowflake ist ein sehr gutes Beispiel.
Ich habe Option 1 in der Vergangenheit immer verwendet, weil ich mir dieser Diskussionen bewusst war und es für besser hielt, nichts zu tun, bis ich wusste, was richtig ist. Diese Systeme laufen alle noch erfolgreich.
Beim nächsten Mal kann ich jedoch Option 2 ausprobieren - die von der Datenbank generierte ID.
Hashcode und equals lösen IllegalStateException aus, wenn die ID nicht festgelegt ist.
Dadurch wird verhindert, dass subtile Fehler, die nicht gespeicherte Entitäten betreffen, unerwartet angezeigt werden.
Was denken die Leute über diesen Ansatz?
IMO haben Sie 3 Möglichkeiten, Equals/HashCode zu implementieren
Die Verwendung einer von einer Anwendung generierten Identität ist der einfachste Ansatz, bringt jedoch einige Nachteile mit sich.
Wenn Sie mit diesen Downsides arbeiten können, verwenden Sie einfach diesen Ansatz.
Um das Join-Problem zu überwinden, könnte es sein, dass die UUID als natürlicher Schlüssel und ein Sequenzwert als Primärschlüssel verwendet wird. Allerdings könnten Sie immer noch auf die Implementierungsprobleme equals/hashCode in zusammengesetzten untergeordneten Entitäten mit eingebetteten IDs stoßen, da Sie beitreten möchten auf dem Primärschlüssel. Die Verwendung des natürlichen Schlüssels in der untergeordneten Entitäts-ID und des Primärschlüssels zum Verweisen auf das übergeordnete Element ist ein guter Kompromiss.
@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 Dies ist der sauberste Ansatz, da dadurch alle Nachteile vermieden werden und Sie gleichzeitig einen Wert (die UUID) erhalten, den Sie mit externen Systemen teilen können, ohne Systemintern offen zu legen.
Implementiere es basierend auf einem Geschäftsschlüssel, wenn du davon ausgehen kannst, dass ein Benutzer eine schöne Idee ist, aber auch ein paar Nachteile hat
In den meisten Fällen handelt es sich bei diesem Geschäftsschlüssel um eine Art code, die der Benutzer bereitstellt, und seltener um eine Kombination mehrerer Attribute.
IMO sollten Sie nicht ausschließlich einen Business Key implementieren oder damit arbeiten. Es ist ein nettes Add-On, d. H. Benutzer können schnell nach diesem Geschäftsschlüssel suchen, aber das System sollte sich beim Betrieb nicht darauf verlassen.
Implementiere es basierend auf dem Primärschlüssel, hat seine Probleme, aber vielleicht ist es nicht so eine große Sache
Wenn Sie IDs für ein externes System verfügbar machen müssen, verwenden Sie die von mir vorgeschlagene UUID-Methode. Wenn Sie dies nicht tun, können Sie immer noch die UUID-Methode verwenden, müssen dies jedoch nicht .. Das Problem der Verwendung einer von DBMS generierten ID in equals/hashCode rührt von der Tatsache her, dass das Objekt möglicherweise zu einem Hash-basierten Objekt hinzugefügt wurde Sammlungen vor dem Zuweisen der ID.
Der naheliegende Weg, dies zu umgehen, besteht darin, das Objekt vor der Zuweisung der ID nicht zu Hash-basierten Sammlungen hinzuzufügen. Ich verstehe, dass dies nicht immer möglich ist, weil Sie die Deduplizierung vor der Zuweisung der ID wünschen. Um die Hash-basierten Sammlungen weiterhin verwenden zu können, müssen Sie die Sammlungen nach dem Zuweisen der ID einfach neu erstellen.
Sie könnten so etwas tun:
@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
}
Ich habe die genaue Vorgehensweise nicht selbst getestet, daher bin ich mir nicht sicher, wie sich das Ändern von Sammlungen in vor- und nach persistenten Ereignissen auswirkt. Die Idee ist jedoch:
Eine andere Möglichkeit, dieses Problem zu lösen, besteht darin, nach einem Update/Persist einfach alle auf Hash basierenden Modelle neu zu erstellen.
Am Ende liegt es an dir. Ich persönlich verwende den sequenzbasierten Ansatz meistens und verwende den UUID-Ansatz nur, wenn ich einen Identifikator externen Systemen offenlegen muss.