IMO haben Sie 3 Optionen für die Implementierung von equals/hashCode
- Verwendung einer von der Anwendung generierten Identität, z. B. einer UUID
- Implementierung auf der Grundlage eines Geschäftsschlüssels
- Implementieren Sie es auf der Grundlage des Primärschlüssels
Die Verwendung einer von einer Anwendung generierten Identität ist der einfachste Ansatz, hat aber auch einige Nachteile
- Joins sind langsamer, wenn sie als PK verwendet werden, weil 128 Bit einfach größer sind als 32 oder 64 Bit
- "Debugging ist schwieriger", weil es ziemlich schwierig ist, mit eigenen Augen zu überprüfen, ob bestimmte Daten korrekt sind.
Wenn Sie damit arbeiten können Schattenseiten verwenden Sie einfach diesen Ansatz.
Um das Problem der Verknüpfung zu überwinden, könnte man die UUID als natürlichen Schlüssel und einen Sequenzwert als Primärschlüssel verwenden, aber dann könnte man immer noch auf die Probleme mit der Implementierung von equals/hashCode in untergeordneten Entitäten stoßen, die eingebettete IDs haben, da man auf der Grundlage des Primärschlüssels verknüpfen möchte. Die Verwendung des natürlichen Schlüssels in den untergeordneten Entitäten id und des Primärschlüssels für den Verweis auf die übergeordnete Entität 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 ist dies der sauberste Ansatz, da er alle Nachteile vermeidet und Ihnen gleichzeitig einen Wert (die UUID) liefert, den Sie mit externen Systemen teilen können, ohne Systeminterna preiszugeben.
Die Implementierung auf der Grundlage eines geschäftlichen Schlüssels, wenn man das von einem Nutzer erwarten kann, ist eine nette Idee, hat aber auch ein paar Nachteile
In den meisten Fällen ist dieser Geschäftsschlüssel eine Art von Code die der Benutzer angibt, und seltener eine Kombination aus mehreren Attributen.
- Joins sind langsamer, weil Joins auf der Grundlage von Text variabler Länge einfach langsam sind. Einige DBMS können sogar Probleme haben, einen Index zu erstellen, wenn der Schlüssel eine bestimmte Länge überschreitet.
- Meiner Erfahrung nach ändern sich geschäftliche Schlüssel häufig, was kaskadierende Aktualisierungen von Objekten, die sich auf sie beziehen, erforderlich macht. Dies ist unmöglich, wenn externe Systeme darauf verweisen.
IMO sollten Sie nicht ausschließlich einen Geschäftsschlüssel einführen oder damit arbeiten. Es ist ein nettes Add-on, d.h. die Benutzer können schnell nach diesem geschäftlichen Schlüssel suchen, aber das System sollte sich nicht darauf verlassen, dass es funktioniert.
Die Implementierung auf der Grundlage des Primärschlüssels ist nicht ganz unproblematisch, aber vielleicht ist das auch gar nicht so schlimm.
Wenn Sie IDs für externe Systeme bereitstellen müssen, verwenden Sie den von mir vorgeschlagenen UUID-Ansatz. Ist dies nicht der Fall, können Sie immer noch den UUID-Ansatz verwenden, müssen es aber nicht. Das Problem bei der Verwendung einer vom DBMS generierten ID in equals/hashCode ergibt sich aus der Tatsache, dass das Objekt vor der Zuweisung der ID zu hashbasierten Sammlungen hinzugefügt worden sein könnte.
Der offensichtliche Weg, um dies zu umgehen, ist einfach nicht das Objekt zu Hash-basierten Sammlungen hinzufügen, bevor Sie die ID zuweisen. Mir ist klar, dass dies nicht immer möglich ist, weil Sie vielleicht schon vor der Zuweisung der ID eine Deduplizierung wünschen. Um die hashbasierten Sammlungen dennoch verwenden zu können, müssen Sie die Sammlungen nach der Zuweisung der ID einfach neu erstellen.
Sie könnten etwa so vorgehen:
@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 den genauen Ansatz nicht selbst getestet, daher bin ich mir nicht sicher, wie das Ändern von Sammlungen in Pre- und Post-Persist-Ereignissen funktioniert, aber die Idee ist:
- Vorübergehendes Entfernen des Objekts aus hashbasierten Sammlungen
- Bestehen Sie darauf
- Erneutes Hinzufügen des Objekts zu den hashbasierten Sammlungen
Eine andere Möglichkeit, dieses Problem zu lösen, besteht darin, alle Hash-basierten Modelle nach einem Update/Persist einfach neu zu erstellen.
Letztendlich liegt es an Ihnen. Ich persönlich verwende die meiste Zeit den sequenzbasierten Ansatz und verwende den UUID-Ansatz nur, wenn ich einen Bezeichner für externe Systeme bereitstellen muss.