Erstellen Sie die perfekte JPA-Entität [geschlossen]

422

Ich arbeite seit einiger Zeit mit JPA (Implementation Hibernate) und jedes Mal, wenn ich Entitäten erstellen muss, habe ich Probleme mit AccessType, unveränderlichen Eigenschaften, equals / hashCode, ....
Deshalb habe ich mich entschlossen, die allgemeinen Best Practices für jede Ausgabe herauszufinden und diese für den persönlichen Gebrauch aufzuschreiben.
Es würde mir jedoch nichts ausmachen, wenn jemand dies kommentiert oder mir sagt, wo ich falsch liege.

Entitätsklasse

  • Serializable implementieren

    Grund: Die Spezifikation besagt, dass Sie dies tun müssen, aber einige JPA-Anbieter erzwingen dies nicht. Der Ruhezustand als JPA-Anbieter erzwingt dies nicht, kann jedoch mit ClassCastException irgendwo tief im Magen fehlschlagen, wenn Serializable nicht implementiert wurde.

Konstruktoren

  • Erstellen Sie einen Konstruktor mit allen erforderlichen Feldern der Entität

    Grund: Ein Konstruktor sollte die erstellte Instanz immer in einem vernünftigen Zustand belassen.

  • Neben diesem Konstruktor: Haben Sie ein Paket privaten Standardkonstruktor

    Grund: Der Standardkonstruktor ist erforderlich, damit der Ruhezustand die Entität initialisiert. privat ist zulässig, aber für die Laufzeit-Proxy-Generierung und den effizienten Datenabruf ohne Bytecode-Instrumentierung ist eine private (oder öffentliche) Paketsichtbarkeit erforderlich.

Felder / Eigenschaften

  • Verwenden Sie den Feldzugriff im Allgemeinen und den Eigenschaftenzugriff bei Bedarf

    Grund: Dies ist wahrscheinlich das umstrittenste Problem, da es für das eine oder andere keine klaren und überzeugenden Argumente gibt (Eigenschaftszugriff vs. Feldzugriff). Der Feldzugriff scheint jedoch allgemeiner Favorit zu sein, da der Code klarer ist, die Kapselung besser ist und keine Setter für unveränderliche Felder erstellt werden müssen

  • Setter für unveränderliche Felder weglassen (für Zugriffstypfeld nicht erforderlich)

  • Eigenschaften können privat sein
    Grund: Ich habe einmal gehört, dass geschützt besser für die Leistung im Ruhezustand ist, aber alles, was ich im Web finden kann, ist: Der Ruhezustand kann direkt auf öffentliche, private und geschützte Zugriffsmethoden sowie auf öffentliche, private und geschützte Felder zugreifen . Die Wahl liegt bei Ihnen und Sie können sie an Ihr Anwendungsdesign anpassen.

Gleich / hashCode

  • Verwenden Sie niemals eine generierte ID, wenn diese ID nur festgelegt wird, wenn die Entität beibehalten wird
  • Nach Präferenz: Verwenden Sie unveränderliche Werte, um einen eindeutigen Geschäftsschlüssel zu bilden, und testen Sie damit die Gleichheit
  • Wenn kein eindeutiger Geschäftsschlüssel verfügbar ist, verwenden Sie eine nicht vorübergehende UUID, die bei der Initialisierung der Entität erstellt wird. Weitere Informationen finden Sie in diesem großartigen Artikel .
  • Verweisen Sie niemals auf verwandte Entitäten (ManyToOne). Wenn diese Entität (wie eine übergeordnete Entität) Teil des Geschäftsschlüssels sein muss, vergleichen Sie nur die IDs. Das Aufrufen von getId () auf einem Proxy löst das Laden der Entität nicht aus, solange Sie den Eigenschaftszugriffstyp verwenden .

Beispiel Entität

@Entity
@Table(name = "ROOM")
public class Room implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue
    @Column(name = "room_id")
    private Integer id;

    @Column(name = "number") 
    private String number; //immutable

    @Column(name = "capacity")
    private Integer capacity;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "building_id")
    private Building building; //immutable

    Room() {
        // default constructor
    }

    public Room(Building building, String number) {
        // constructor with required field
        notNull(building, "Method called with null parameter (application)");
        notNull(number, "Method called with null parameter (name)");

        this.building = building;
        this.number = number;
    }

    @Override
    public boolean equals(final Object otherObj) {
        if ((otherObj == null) || !(otherObj instanceof Room)) {
            return false;
        }
        // a room can be uniquely identified by it's number and the building it belongs to; normally I would use a UUID in any case but this is just to illustrate the usage of getId()
        final Room other = (Room) otherObj;
        return new EqualsBuilder().append(getNumber(), other.getNumber())
                .append(getBuilding().getId(), other.getBuilding().getId())
                .isEquals();
        //this assumes that Building.id is annotated with @Access(value = AccessType.PROPERTY) 
    }

    public Building getBuilding() {
        return building;
    }


    public Integer getId() {
        return id;
    }

    public String getNumber() {
        return number;
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder().append(getNumber()).append(getBuilding().getId()).toHashCode();
    }

    public void setCapacity(Integer capacity) {
        this.capacity = capacity;
    }

    //no setters for number, building nor id

}

Andere Vorschläge, die dieser Liste hinzugefügt werden sollen, sind mehr als willkommen ...

AKTUALISIEREN

Seit ich diesen Artikel gelesen habe, habe ich meine Art der Implementierung von eq / hC angepasst:

  • Wenn ein unveränderlicher einfacher Geschäftsschlüssel verfügbar ist: Verwenden Sie diesen
  • in allen anderen fällen: benutze eine uuid
Stijn Geukens
quelle
6
Dies ist keine Frage, sondern eine Anfrage zur Überprüfung mit einer Anfrage nach einer Liste. Darüber hinaus ist es sehr offen und vage oder anders ausgedrückt: Ob eine JPA-Entität perfekt ist, hängt davon ab, wofür sie verwendet wird. Sollten wir alle Dinge auflisten, die eine Entität für alle möglichen Verwendungen einer Entität benötigen könnte?
Meriton
Ich weiß, dass es keine eindeutige Frage ist, für die ich mich entschuldige. Es ist nicht wirklich eine Anfrage nach einer Liste, sondern eine Anfrage nach Kommentaren / Bemerkungen, obwohl andere Vorschläge willkommen sind. Fühlen Sie sich frei, die möglichen Verwendungen einer JPA-Entität zu erläutern.
Stijn Geukens
Ich würde auch wollen, dass Felder vorhanden sind final(gemessen an Ihrer Auslassung von Setzern würde ich vermuten, dass Sie dies auch tun).
Sridhar Sarnobat
Ich müsste es versuchen, aber ich denke nicht, dass final funktionieren wird, da Hibernate immer noch in der Lage sein muss, die Werte für diese Eigenschaften festzulegen.
Stijn Geukens
Woher kommt notNulldas?
Bruno

Antworten:

73

Ich werde versuchen, einige wichtige Punkte zu beantworten: Dies beruht auf langjähriger Erfahrung im Ruhezustand / in der Persistenz, einschließlich mehrerer wichtiger Anwendungen.

Entitätsklasse: Serializable implementieren?

Schlüssel müssen Serializable implementieren. Dinge, die in die HttpSession gehen oder von RPC / Java EE über das Kabel gesendet werden, müssen Serializable implementieren. Andere Sachen: nicht so sehr. Verbringen Sie Ihre Zeit mit dem, was wichtig ist.

Konstruktoren: Erstellen Sie einen Konstruktor mit allen erforderlichen Feldern der Entität?

Konstruktoren für die Anwendungslogik sollten nur wenige kritische Felder "Fremdschlüssel" oder "Typ / Art" enthalten, die beim Erstellen der Entität immer bekannt sind. Der Rest sollte durch Aufrufen der Setter-Methoden festgelegt werden - dafür sind sie da.

Vermeiden Sie es, zu viele Felder in Konstruktoren einzufügen. Konstruktoren sollten bequem sein und dem Objekt grundlegende Vernunft verleihen. Name, Typ und / oder Eltern sind normalerweise nützlich.

OTOH Wenn die Anwendungsregeln (heute) erfordern, dass ein Kunde eine Adresse hat, überlassen Sie dies einem Setter. Das ist ein Beispiel für eine "schwache Regel". Vielleicht möchten Sie nächste Woche ein Kundenobjekt erstellen, bevor Sie zum Bildschirm Details eingeben wechseln? Stolpern Sie nicht, lassen Sie die Möglichkeit für unbekannte, unvollständige oder "teilweise eingegebene" Daten.

Konstruktoren: Paket privater Standardkonstruktor?

Ja, aber verwenden Sie "geschützt" anstatt "Paket privat". Unterklassen sind ein echtes Problem, wenn die erforderlichen Einbauten nicht sichtbar sind.

Felder / Eigenschaften

Verwenden Sie den Feldzugriff "Eigenschaft" für den Ruhezustand und von außerhalb der Instanz. Verwenden Sie innerhalb der Instanz die Felder direkt. Grund: Ermöglicht die Standardreflexion, die einfachste und grundlegendste Methode für den Ruhezustand.

Felder, die für die Anwendung unveränderlich sind, müssen im Ruhezustand noch geladen werden können. Sie können versuchen, diese Methoden "privat" zu machen und / oder sie mit Anmerkungen zu versehen, um zu verhindern, dass Anwendungscode unerwünschten Zugriff gewährt.

Hinweis: Wenn Sie eine equals () -Funktion schreiben, verwenden Sie Getter für Werte in der 'anderen' Instanz! Andernfalls werden Sie auf Proxy-Instanzen auf nicht initialisierte / leere Felder stoßen.

Geschützt ist besser für die Leistung im Ruhezustand?

Unwahrscheinlich.

Gleich / HashCode?

Dies ist relevant für die Arbeit mit Entitäten, bevor diese gespeichert wurden - was ein heikles Problem ist. Hashing / Vergleich mit unveränderlichen Werten? In den meisten Geschäftsanwendungen gibt es keine.

Ein Kunde kann die Adresse ändern, den Namen seines Unternehmens ändern usw. usw. - nicht üblich, aber es passiert. Korrekturen müssen auch möglich sein, wenn die Daten nicht korrekt eingegeben wurden.

Die wenigen Dinge, die normalerweise unveränderlich bleiben, sind Elternschaft und möglicherweise Typ / Art - normalerweise erstellt der Benutzer den Datensatz neu, anstatt diese zu ändern. Diese identifizieren die Entität jedoch nicht eindeutig!

Kurz und gut, die behaupteten "unveränderlichen" Daten sind nicht wirklich. Primärschlüssel- / ID-Felder werden genau zu dem Zweck generiert, eine solche garantierte Stabilität und Unveränderlichkeit zu gewährleisten.

Sie müssen Ihren Bedarf an Vergleichs-, Hashing- und Anforderungsverarbeitungsphasen planen und berücksichtigen, wenn Sie A) mit "geänderten / gebundenen Daten" auf der Benutzeroberfläche arbeiten, wenn Sie "selten geänderte Felder" vergleichen / hashen, oder B) mit "arbeiten". nicht gespeicherte Daten ", wenn Sie die ID vergleichen / hashen.

Equals / HashCode - Wenn kein eindeutiger Geschäftsschlüssel verfügbar ist, verwenden Sie eine nicht vorübergehende UUID, die bei der Initialisierung der Entität erstellt wird

Ja, dies ist bei Bedarf eine gute Strategie. Beachten Sie jedoch, dass UUIDs in Bezug auf die Leistung nicht kostenlos sind - und Clustering erschwert die Arbeit.

Equals / HashCode - Verweisen Sie niemals auf verwandte Entitäten

"Wenn eine verwandte Entität (wie eine übergeordnete Entität) Teil des Geschäftsschlüssels sein muss, fügen Sie ein nicht einfügbares, nicht aktualisierbares Feld hinzu, um die übergeordnete ID (mit demselben Namen wie die ManytoOne JoinColumn) zu speichern und diese ID bei der Gleichheitsprüfung zu verwenden ""

Klingt nach einem guten Rat.

Hoffe das hilft!

Thomas W.
quelle
2
Betreff: Konstruktoren, ich sehe oft nur null Argumente (dh keine) und der aufrufende Code hat eine große lange Liste von Setzern, die mir etwas chaotisch erscheint. Gibt es wirklich ein Problem damit, ein paar Konstruktoren zu haben, die Ihren Anforderungen entsprechen und den aufrufenden Code prägnanter machen?
Hurrikan
total eigensinnig, speziell über ctor. Was ist schöner Code? Eine Reihe verschiedener Ctors, mit denen Sie wissen, welche (Kombination von) Werten erforderlich sind, um einen vernünftigen Zustand des Objekts oder eines No-Arg-Ctors zu erzeugen, der keinen Hinweis darauf gibt, was in welcher Reihenfolge eingestellt werden soll, und es für Fehler des Benutzers anfällig macht ?
Mohamnag
1
@mohamnag Kommt darauf an. Für interne systemgenerierte Daten sind streng gültige Beans großartig. Moderne Geschäftsanwendungen bestehen jedoch aus einer großen Anzahl von CRUD- oder Assistentenbildschirmen zur Eingabe von Benutzerdaten. Vom Benutzer eingegebene Daten sind zumindest während der Bearbeitung häufig teilweise oder schlecht geformt. Sehr oft ist es sogar geschäftlich wertvoll, einen unvollständigen Status für eine spätere Fertigstellung aufzuzeichnen - denken Sie an die Erfassung von Versicherungsanwendungen, Kundenanmeldungen usw. Wenn Sie die Einschränkungen auf ein Minimum beschränken (z. B. Primärschlüssel, Geschäftsschlüssel und Status), können Sie real flexibler sein Geschäftssituationen.
Thomas W
1
@ThomasW Zuerst muss ich sagen, ich bin stark von domänengetriebenem Design und der Verwendung von Namen für Klassennamen und der Bedeutung vollständiger Verben für Methoden überzeugt. In diesem Paradigma beziehen Sie sich auf DTOs und nicht auf Domänenentitäten, die für die temporäre Datenspeicherung verwendet werden sollen. Oder Sie haben Ihre Domain einfach falsch verstanden / strukturiert.
Mohamnag
@ThomasW Wenn ich alle Sätze herausfiltere, die Sie als Anfänger bezeichnen möchten, enthält Ihr Kommentar keine Informationen mehr, außer zu Benutzereingaben. Dieser Teil wird, wie ich bereits sagte, in DTOs und nicht direkt in der Einheit durchgeführt. Lassen Sie uns in weiteren 50 Jahren darüber sprechen, dass Sie vielleicht 5% dessen werden, was DDD wie Fowler erlebt hat!
Prost
144

In der JPA 2.0-Spezifikation heißt es:

  • Die Entitätsklasse muss einen Konstruktor ohne Argumente haben. Es kann auch andere Konstruktoren haben. Der Konstruktor ohne Argumente muss öffentlich oder geschützt sein.
  • Die Entitätsklasse muss eine Klasse der obersten Ebene sein. Eine Aufzählung oder Schnittstelle darf nicht als Entität festgelegt werden.
  • Die Entitätsklasse darf nicht endgültig sein. Es dürfen keine Methoden oder persistenten Instanzvariablen der Entitätsklasse endgültig sein.
  • Wenn eine Entitätsinstanz als getrenntes Objekt (z. B. über eine Remote-Schnittstelle) als Wert übergeben werden soll, muss die Entitätsklasse die serialisierbare Schnittstelle implementieren.
  • Sowohl abstrakte als auch konkrete Klassen können Entitäten sein. Entitäten können sowohl Nicht-Entitätsklassen als auch Entitätsklassen erweitern, und Nicht-Entitätsklassen können Entitätsklassen erweitern.

Die Spezifikation enthält keine Anforderungen an die Implementierung von equals- und hashCode-Methoden für Entitäten, soweit ich weiß, nur für Primärschlüsselklassen und Map-Schlüssel.

Edwin Dalorzo
quelle
13
Richtig, gleich, Hashcode, ... sind keine JPA-Anforderung, werden aber natürlich empfohlen und als gute Praxis angesehen.
Stijn Geukens
6
@TheStijn Nun, es sei denn, Sie planen, getrennte Entitäten auf Gleichheit zu vergleichen, ist dies wahrscheinlich unnötig. Es wird garantiert, dass der Entitätsmanager jedes Mal, wenn Sie danach fragen, dieselbe Instanz einer bestimmten Entität zurückgibt. Soweit ich weiß, können Sie also Identitätsvergleiche für verwaltete Entitäten problemlos durchführen. Könnten Sie bitte etwas näher auf die Szenarien eingehen, in denen Sie dies als gute Praxis betrachten würden?
Edwin Dalorzo
2
Ich bemühe mich immer um eine korrekte Implementierung von equals / hashCode. Für JPA nicht erforderlich, aber ich halte es für eine gute Vorgehensweise, wenn Entitäten oder Sets hinzugefügt werden. Sie könnten entscheiden, nur Gleichheit zu implementieren, wenn Entitäten zu Sets hinzugefügt werden, aber wissen Sie immer im Voraus?
Stijn Geukens
10
@TheStijn Der JPA-Anbieter stellt sicher, dass zu einem bestimmten Zeitpunkt nur eine Instanz einer bestimmten Entität im Kontext vorhanden ist. Daher sind auch Ihre Sets ohne Implementierung von equals / hascode sicher, sofern Sie nur verwaltete Entitäten verwenden. Die Implementierung dieser Methoden für Unternehmen ist nicht frei von Schwierigkeiten. Schauen Sie sich beispielsweise diesen Artikel im Ruhezustand zu diesem Thema an. Mein Punkt ist, wenn Sie nur mit verwalteten Entitäten arbeiten, sind Sie ohne diese besser dran, ansonsten bieten Sie eine sehr sorgfältige Implementierung.
Edwin Dalorzo
2
@TheStijn Dies ist das gute gemischte Szenario. Dies rechtfertigt die Notwendigkeit, eq / hC wie ursprünglich vorgeschlagen zu implementieren, da Sie den vom JPA-Standard erzwungenen Regeln nicht mehr vertrauen können, sobald die Entitäten die Sicherheit der Persistenzschicht aufgeben. In unserem Fall wurde das DTO-Muster von Anfang an architektonisch durchgesetzt. Unsere Persistenz-API bietet keine öffentliche Möglichkeit zur Interaktion mit den Geschäftsobjekten, sondern nur eine API zur Interaktion mit unserer Persistenzschicht mithilfe von DTOs.
Edwin Dalorzo
13

Meine 2 Cent zusätzlich zu den Antworten hier sind:

  1. In Bezug auf den Feld- oder Eigenschaftszugriff (abgesehen von Leistungsüberlegungen) wird auf beide mit Hilfe von Gettern und Setzern legitim zugegriffen, sodass meine Modelllogik sie auf dieselbe Weise setzen / abrufen kann. Der Unterschied tritt auf, wenn der Persistenz-Laufzeitanbieter (Hibernate, EclipseLink oder sonst) einen Datensatz in Tabelle A beibehalten / festlegen muss, dessen Fremdschlüssel auf eine Spalte in Tabelle B verweist. Bei einem Eigenschaftszugriffstyp die Persistenz Das Laufzeitsystem verwendet meine codierte Setter-Methode, um der Zelle in Tabelle B einen neuen Wert zuzuweisen. Bei einem Feldzugriffstyp legt das Persistenz-Laufzeitsystem die Zelle in der Spalte Tabelle B direkt fest. Dieser Unterschied ist im Kontext einer unidirektionalen Beziehung nicht von Bedeutung. Es ist jedoch ein MUSS, meine eigene codierte Setter-Methode (Eigenschaftszugriffstyp) für eine bidirektionale Beziehung zu verwenden, vorausgesetzt, die Setter-Methode ist so konzipiert, dass sie die Konsistenz berücksichtigt. Konsistenz ist ein kritisches Thema für bidirektionale BeziehungenLink für ein einfaches Beispiel für einen gut gestalteten Setter.

  2. In Bezug auf Equals / hashCode: Es ist unmöglich, die automatisch generierten Equals / hashCode-Methoden von Eclipse für Entitäten zu verwenden, die an einer bidirektionalen Beziehung teilnehmen. Andernfalls wird eine Zirkelreferenz erstellt, die zu einer Stackoverflow-Ausnahme führt. Sobald Sie eine bidirektionale Beziehung (z. B. OneToOne) versuchen und Equals () oder hashCode () oder sogar toString () automatisch generieren, werden Sie von dieser Stackoverflow-Ausnahme erfasst.

Sym-Sym
quelle
9

Entitätsschnittstelle

public interface Entity<I> extends Serializable {

/**
 * @return entity identity
 */
I getId();

/**
 * @return HashCode of entity identity
 */
int identityHashCode();

/**
 * @param other
 *            Other entity
 * @return true if identities of entities are equal
 */
boolean identityEquals(Entity<?> other);
}

Grundlegende Implementierung für alle Entitäten, vereinfacht die Implementierung von Equals / Hashcode:

public abstract class AbstractEntity<I> implements Entity<I> {

@Override
public final boolean identityEquals(Entity<?> other) {
    if (getId() == null) {
        return false;
    }
    return getId().equals(other.getId());
}

@Override
public final int identityHashCode() {
    return new HashCodeBuilder().append(this.getId()).toHashCode();
}

@Override
public final int hashCode() {
    return identityHashCode();
}

@Override
public final boolean equals(final Object o) {
    if (this == o) {
        return true;
    }
    if ((o == null) || (getClass() != o.getClass())) {
        return false;
    }

    return identityEquals((Entity<?>) o);
}

@Override
public String toString() {
    return getClass().getSimpleName() + ": " + identity();
    // OR 
    // return ReflectionToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
}
}

Raumentität impliziert:

@Entity
@Table(name = "ROOM")
public class Room extends AbstractEntity<Integer> {

private static final long serialVersionUID = 1L;

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "room_id")
private Integer id;

@Column(name = "number") 
private String number; //immutable

@Column(name = "capacity")
private Integer capacity;

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "building_id")
private Building building; //immutable

Room() {
    // default constructor
}

public Room(Building building, String number) {
    // constructor with required field
    notNull(building, "Method called with null parameter (application)");
    notNull(number, "Method called with null parameter (name)");

    this.building = building;
    this.number = number;
}

public Integer getId(){
    return id;
}

public Building getBuilding() {
    return building;
}

public String getNumber() {
    return number;
}


public void setCapacity(Integer capacity) {
    this.capacity = capacity;
}

//no setters for number, building nor id
}

Ich sehe keinen Grund, die Gleichheit von Entitäten basierend auf Geschäftsfeldern in jedem Fall von JPA-Entitäten zu vergleichen. Dies ist möglicherweise eher der Fall, wenn diese JPA-Entitäten als domänengesteuerte ValueObjects anstelle von domänengesteuerten Entitäten (für die diese Codebeispiele bestimmt sind) betrachtet werden.

Ahaaman
quelle
4
Obwohl es ein guter Ansatz ist, eine übergeordnete Entitätsklasse zum Herausnehmen des Kesselplattencodes zu verwenden, ist es keine gute Idee, die DB-definierte ID in Ihrer equals-Methode zu verwenden. In Ihrem Fall würde der Vergleich von 2 neuen Entitäten sogar eine NPE auslösen. Selbst wenn Sie es auf Null setzen, sind 2 neue Entitäten immer gleich, bis sie bestehen bleiben. Gleichung / hC sollte unveränderlich sein.
Stijn Geukens
2
Equals () löst keine NPE aus, da geprüft wird, ob die DB-ID null ist oder nicht. Falls die DB-ID null ist, ist die Gleichheit falsch.
Ahaaman
3
In der Tat sehe ich nicht, wie ich übersehen habe, dass der Code null-sicher ist. Aber IMO mit der ID ist immer noch eine schlechte Praxis. Argumente: onjava.com/pub/a/onjava/2006/09/13/…
Stijn Geukens
In dem Buch 'Implementing DDD' von Vaughn Vernon wird argumentiert, dass Sie id for equals verwenden können, wenn Sie "frühe PK-Generierung" verwenden (Generieren Sie zuerst eine ID und übergeben Sie sie an den Konstruktor der Entität, anstatt die Datenbank generieren zu lassen die ID, wenn Sie die Entität
beibehalten
oder wenn Sie nicht planen, nicht persistente Entitäten gleich zu vergleichen? Warum sollten Sie ...
Enerccio