Crea l'entità JPA perfetta [chiusa]


422

Lavoro con JPA (implementazione Hibernate) da un po 'di tempo e ogni volta che devo creare entità mi trovo alle prese con problemi come AccessType, proprietà immutabili, equals / hashCode, ....
Così ho deciso di provare a scoprire le migliori pratiche generali per ogni problema e scriverlo per uso personale.
Non mi dispiacerebbe comunque che qualcuno commentasse o mi dicesse dove sbaglio.

Classe di entità

  • implementare serializzabile

    Motivo: le specifiche indicano che è necessario, ma alcuni provider JPA non lo applicano. Sospendere come provider JPA non impone questo, ma può fallire da qualche parte nel profondo con ClassCastException, se Serializable non è stato implementato.

Costruttori

  • creare un costruttore con tutti i campi obbligatori dell'entità

    Motivo: un costruttore deve sempre lasciare l'istanza creata in uno stato sano.

  • oltre a questo costruttore: avere un costruttore predefinito del pacchetto

    Motivo: per consentire a Hibernate di inizializzare l'entità è necessario il costruttore predefinito; privato è consentito ma la visibilità del pacchetto privato (o pubblico) è richiesta per la generazione del proxy di runtime e il recupero efficiente dei dati senza strumentazione bytecode.

Campi / Proprietà

  • Utilizzare l'accesso al campo in generale e l'accesso alla proprietà quando necessario

    Motivo: questo è probabilmente il problema più discutibile in quanto non esistono argomenti chiari e convincenti per l'uno o l'altro (accesso alla proprietà vs accesso al campo); tuttavia, l'accesso ai campi sembra essere il preferito in generale a causa del codice più chiaro, dell'incapsulamento migliore e della necessità di creare setter per campi immutabili

  • Ometti setter per campi immutabili (non richiesto per il campo del tipo di accesso)

  • le proprietà possono essere private
    Motivo: una volta ho sentito che la protezione è migliore per le prestazioni (ibernazione) ma tutto ciò che posso trovare sul web è: Hibernate può accedere a metodi di accesso pubblici, privati ​​e protetti, nonché direttamente ai campi pubblici, privati ​​e protetti . La scelta spetta a te e puoi abbinarla per adattarla al design della tua applicazione.

Equals / hashCode

  • Non utilizzare mai un ID generato se questo ID è impostato solo quando si persiste l'entità
  • Per preferenza: utilizzare valori immutabili per formare una chiave aziendale univoca e utilizzarla per testare l'uguaglianza
  • se non è disponibile una chiave business univoca, utilizzare un UUID non transitorio che viene creato al momento dell'inizializzazione dell'entità; Vedi questo fantastico articolo per ulteriori informazioni.
  • non fare mai riferimento a entità correlate (ManyToOne); se questa entità (come un'entità principale) deve far parte della chiave aziendale, confronta solo l'ID. La chiamata di getId () su un proxy non attiverà il caricamento dell'entità, purché si utilizzi il tipo di accesso alla proprietà .

Entità di esempio

@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

}

Altri suggerimenti da aggiungere a questo elenco sono più che benvenuti ...

AGGIORNARE

Da quando ho letto questo articolo ho adattato il mio modo di implementare eq / hC:

  • se è disponibile una chiave business semplice immutabile: usala
  • in tutti gli altri casi: utilizzare un uuid

6
Questa non è una domanda, è una richiesta di revisione con una richiesta per un elenco. Inoltre, è molto aperto e vago, o in altre parole: se un'entità JPA è perfetta dipende da cosa verrà utilizzata. Dovremmo elencare tutte le cose di cui un'entità potrebbe aver bisogno in tutti i possibili usi di un'entità?
Meriton,

So che non è una domanda chiara per la quale mi scuso. Non è in realtà una richiesta di un elenco, ma piuttosto una richiesta di commenti / commenti sebbene altri suggerimenti siano ben accetti. Sentiti libero di elaborare i possibili usi di un'entità JPA.
Stijn Geukens,

Vorrei anche che i campi fossero final(a giudicare dalla tua omissione di setter, immagino che lo faccia anche tu).
Sridhar Sarnobat,

Dovrei provarlo ma non credo che final funzionerà poiché Hibernate deve ancora essere in grado di impostare i valori su quelle proprietà.
Stijn Geukens,

Da dove notNullviene
bruno

Risposte:


73

Cercherò di rispondere a diversi punti chiave: questo è frutto di una lunga esperienza di ibernazione / persistenza che comprende diverse applicazioni importanti.

Classe di entità: implementare serializzabile?

Le chiavi devono implementare Serializable. Le cose che andranno in HttpSession, o che saranno inviate via cavo da RPC / Java EE, devono implementare Serializable. Altre cose: non così tanto. Trascorri il tuo tempo su ciò che è importante.

Costruttori: creare un costruttore con tutti i campi obbligatori dell'entità?

I costruttori per la logica dell'applicazione, dovrebbero avere solo pochi campi critici "chiave esterna" o "tipo / tipo" che saranno sempre conosciuti durante la creazione dell'entità. Il resto dovrebbe essere impostato chiamando i metodi setter - ecco a cosa servono.

Evita di mettere troppi campi nei costruttori. I costruttori dovrebbero essere convenienti e dare un buon senso di salute all'oggetto. Nome, tipo e / o genitori sono generalmente utili.

OTOH se le regole di applicazione (oggi) richiedono che un cliente abbia un indirizzo, lascialo a un setter. Questo è un esempio di "regola debole". Forse la prossima settimana, vuoi creare un oggetto Cliente prima di accedere alla schermata Inserisci dettagli? Non inciampare, lasciare la possibilità di dati sconosciuti, incompleti o "parzialmente inseriti".

Costruttori: inoltre, pacchetto costruttore predefinito privato?

Sì, ma usa "protetto" anziché il pacchetto privato. Le cose di sottoclasse sono un vero dolore quando gli interni necessari non sono visibili.

Campi / Proprietà

Utilizzare l'accesso al campo "proprietà" per Hibernate e dall'esterno dell'istanza. All'interno dell'istanza, utilizzare direttamente i campi. Motivo: consente la riflessione standard, il metodo più semplice e basilare per Hibernate, per funzionare.

Per quanto riguarda i campi "immutabili" nell'applicazione, Hibernate deve ancora essere in grado di caricarli. È possibile provare a rendere questi metodi "privati" e / o inserire un'annotazione su di essi, per impedire al codice dell'applicazione di accedere in modo indesiderato.

Nota: quando si scrive una funzione equals (), utilizzare getter per i valori nell'istanza "altra"! Altrimenti, colpirai i campi non inizializzati / vuoti sulle istanze proxy.

Protetto è meglio per le prestazioni (ibernazione)?

Improbabile.

Eguali / HashCode?

Questo è rilevante per lavorare con le entità, prima che siano state salvate, il che è un problema spinoso. Hashing / confronto su valori immutabili? Nella maggior parte delle applicazioni aziendali, non ce ne sono.

Un cliente può cambiare indirizzo, cambiare il nome della propria attività, ecc. Ecc. - Non comune, ma succede. È inoltre necessario apportare correzioni quando i dati non sono stati inseriti correttamente.

Le poche cose che sono normalmente mantenute immutabili, sono Parenting e forse Type / Kind - normalmente l'utente ricrea il record, piuttosto che cambiarlo. Ma questi non identificano in modo univoco l'entità!

Quindi, lunghi e brevi, i dati "immutabili" dichiarati non sono realmente. I campi Chiave primaria / ID vengono generati con lo scopo preciso di fornire tale stabilità e immutabilità garantite.

È necessario pianificare e considerare la necessità di confronto e hashing e fasi di lavoro di elaborazione delle richieste quando A) lavora con "dati modificati / associati" dall'interfaccia utente se si confronta / hash su "campi modificati di rado" o B) lavora con " dati non salvati ", se si confronta / hash su ID.

Equals / HashCode: se non è disponibile una chiave business univoca, utilizzare un UUID non transitorio che viene creato quando l'entità viene inizializzata

Sì, questa è una buona strategia quando richiesto. Tieni presente che gli UUID non sono gratuiti, per quanto riguarda le prestazioni, e il clustering complica le cose.

Equals / HashCode - non fare mai riferimento a entità correlate

"Se l'entità correlata (come un'entità padre) deve far parte della chiave aziendale, aggiungere un campo non inseribile e non aggiornabile per memorizzare l'id padre (con lo stesso nome di ManytoOne JoinColumn) e utilizzare questo ID nel controllo di uguaglianza "

Sembra un buon consiglio.

Spero che sia di aiuto!


2
Ri: costruttori, vedo spesso zero arg solo (cioè nessuno) e il codice chiamante ha una lunga lista di setter che mi sembra un po 'confusa. C'è davvero qualche problema con avere un paio di costruttori adatti alle tue esigenze, rendendo il codice chiamante più succinto?
Uragano,

totalmente supponente, specialmente riguardo a ctor. qual è il codice più bello? un gruppo di diversi ctors che ti permettono di sapere quali (combinazione di) valori sono necessari per creare uno stato sano dell'obj o un ctor no-arg che non dà idea di cosa debba essere impostato e in quale ordine e lo rende soggetto a errori dell'utente ?
Mohamnag,

1
@mohamnag Depends. Per i dati generati dal sistema interno, i bean strettamente validi sono ottimi; tuttavia le moderne applicazioni aziendali sono costituite da un gran numero di schermate CRUD o di procedura guidata per l'immissione dei dati dell'utente. I dati inseriti dall'utente sono spesso parzialmente o scarsamente formati, almeno durante la modifica. Abbastanza spesso c'è persino un valore commerciale nel poter registrare uno stato incompleto per un successivo completamento - pensa all'acquisizione di applicazioni assicurative, registrazioni di clienti ecc. Mantenere i vincoli al minimo (ad es. Chiave primaria, chiave aziendale e stato) consente una maggiore flessibilità reale situazioni aziendali.
Thomas W,

1
@ThomasW per prima cosa devo dire, sono fortemente motivato verso la progettazione guidata dal dominio e l'utilizzo di nomi per i nomi di classe e che significa verbi completi per metodi. In questo paradigma, a cui ti riferisci, sono in realtà DTO e non le entità di dominio che dovrebbero essere utilizzate per l'archiviazione temporanea dei dati. Oppure hai appena frainteso / strutturato il tuo dominio.
Mohamnag,

@ThomasW quando filtrerò tutte le frasi che stai cercando di dire che sono un novizio, non ci sono informazioni nel tuo commento se non per quanto riguarda l'input dell'utente. Quella parte, come ho detto prima, deve essere effettuata nei DTO e non direttamente nell'entità. parliamo in altri 50 anni che potresti diventare il 5% di ciò che la grande mente dietro DDD come Fowler ha vissuto! salute: D
Mohamnag,

144

La specifica JPA 2.0 afferma che:

  • La classe di entità deve avere un costruttore no-arg. Potrebbe avere anche altri costruttori. Il costruttore no-arg deve essere pubblico o protetto.
  • La classe entità deve essere una classe di livello superiore. Un enum o un'interfaccia non devono essere designati come entità.
  • La classe di entità non deve essere definitiva. Nessun metodo o variabile di istanza persistente della classe di entità può essere definitivo.
  • Se un'istanza di entità deve essere passata per valore come oggetto separato (ad es. Attraverso un'interfaccia remota), la classe di entità deve implementare l'interfaccia serializzabile.
  • Entrambe le classi astratte e concrete possono essere entità. Le entità possono estendere le classi non entità nonché le classi entità e le classi non entità possono estendere le classi entità.

La specifica non contiene requisiti sull'implementazione dei metodi egals e hashCode per le entità, solo per le classi di chiavi primarie e le chiavi della mappa, per quanto ne so.


13
Vero, uguale, hashcode, ... non sono un requisito JPA ma sono ovviamente raccomandati e considerati buone pratiche.
Stijn Geukens,

6
@TheStijn Bene, a meno che tu non preveda di confrontare entità distaccate per l'uguaglianza, questo probabilmente non è necessario. Il gestore entità è garantito per restituire la stessa istanza di una determinata entità ogni volta che lo richiedi. Quindi, per quanto ne so, puoi fare benissimo con confronti di identità per entità gestite. Potresti per favore approfondire un po 'di più quegli scenari in cui considereresti questa una buona pratica?
Edwin Dalorzo,

2
Mi sforzo di avere sempre un'implementazione corretta di equals / hashCode. Non richiesto per JPA ma lo considero una buona pratica per quando entità o aggiunte ai set. Potresti decidere di implementare solo uguale quando le entità verranno aggiunte ai Set ma lo sai sempre in anticipo?
Stijn Geukens,

10
@TheStijn Il fornitore JPA assicurerà che in un determinato momento nel contesto vi sia solo un'istanza di una determinata entità, quindi anche i tuoi set sono al sicuro senza implementare uguale / hascode, a condizione che tu usi solo entità gestite. L'implementazione di questi metodi per le entità non è priva di difficoltà, ad esempio dare un'occhiata a questo articolo di ibernazione sull'argomento. Il punto è che, se lavori solo con entità gestite, stai meglio senza di esse, altrimenti fornisci un'implementazione molto attenta.
Edwin Dalorzo,

2
@TheStijn Questo è lo scenario misto buono. Giustifica la necessità di implementare eq / hC come inizialmente suggerito perché una volta che le entità abbandonano la sicurezza del livello di persistenza, non è più possibile fidarsi delle regole imposte dallo standard JPA. Nel nostro caso, il modello DTO è stato applicato dal punto di vista architettonico sin dall'inizio. In base alla progettazione, la nostra API di persistenza non offre un modo pubblico per interagire con gli oggetti business, ma solo un'API per interagire con il nostro livello di persistenza utilizzando DTO.
Edwin Dalorzo,

13

I miei 2 centesimi in aggiunta alle risposte qui sono:

  1. Con riferimento all'accesso al campo o alla proprietà (lontano dalle considerazioni sulle prestazioni), entrambi sono legittimamente accessibili tramite getter e setter, quindi la mia logica del modello può impostarli / ottenerli allo stesso modo. La differenza si manifesta quando il provider di runtime di persistenza (Hibernate, EclipseLink o altro) deve persistere / impostare alcuni record nella Tabella A che ha una chiave esterna che fa riferimento ad alcune colonne della Tabella B. In caso di un tipo di accesso Proprietà, la persistenza il sistema di runtime utilizza il mio metodo setter codificato per assegnare alla cella nella colonna Tabella B un nuovo valore. Nel caso di un tipo di accesso al campo, il sistema di runtime di persistenza imposta direttamente la cella nella colonna Tabella B. Questa differenza non è importante nel contesto di una relazione unidirezionale, tuttavia è NECESSARIO utilizzare il mio metodo setter codificato (tipo di accesso alla proprietà) per una relazione bidirezionale, a condizione che il metodo setter sia ben progettato per tenere conto della coerenza. La coerenza è una questione critica per le relazioni bidirezionali che fanno riferimento a questolink per un semplice esempio per un setter ben progettato.

  2. Con riferimento a Equals / hashCode: è impossibile utilizzare i metodi Equals / hashCode generati automaticamente da Eclipse per le entità che partecipano a una relazione bidirezionale, altrimenti avranno un riferimento circolare che determina un'eccezione stackoverflow. Dopo aver provato una relazione bidirezionale (ad esempio OneToOne) e generato automaticamente Equals () o hashCode () o persino toString (), verrai catturato da questa eccezione di stackoverflow.


9

Interfaccia di entità

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);
}

L'implementazione di base per tutte le Entità semplifica le implementazioni 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);
}
}

Impl. Entità ambiente:

@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
}

Non vedo il punto di confrontare l'uguaglianza delle entità in base ai settori di attività in ogni caso di entità JPA. Questo potrebbe essere più un caso se queste entità JPA sono pensate come Domain-Driven ValueObjects, invece di Domain-Driven Entities (a cui servono questi esempi di codice).


4
Sebbene sia un buon approccio utilizzare una classe di entità padre per estrarre il codice della piastra della caldaia, non è una buona idea usare l'id definito dal DB nel metodo equals. Nel tuo caso, confrontando 2 nuove entità, verrebbe persino lanciato un NPE. Anche se lo rendi nullo sicuro, allora 2 nuove entità sarebbero sempre uguali, fino a quando non saranno persistite. Eq / hC dovrebbe essere immutabile.
Stijn Geukens,

2
Equals () non genererà NPE in quanto esiste un controllo se l'id DB è nullo o no e nel caso in cui l'id DB sia null, l'uguaglianza sarebbe falsa.
ahaaman,

3
In effetti, non vedo quanto mi sia sfuggito che il codice sia nullo e sicuro. Ma IMO che utilizza l'id è ancora una cattiva pratica. Argomenti: onjava.com/pub/a/onjava/2006/09/13/…
Stijn Geukens

Nel libro "Implementing DDD" di Vaughn Vernon, si sostiene che è possibile utilizzare id per uguali se si utilizza la "generazione PK iniziale" (Generare prima un id e passarlo nel costruttore dell'entità anziché lasciare che il database generi l'id quando persistete l'entità.)
Wim Deblauwe,

o se non pianifichi su uguali entità non persistenti di confronto? Perché dovresti ...
Enerccio il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.