Il dilemma hashCode () / equals () di JPA


311

Ci sono state alcune discussioni qui sulle entità dell'APP e quale hashCode()/ equals()implementazione dovrebbe essere usata per le classi di entità dell'APP. Molti (se non tutti) dipendono da Hibernate, ma mi piacerebbe discuterne in modo neutrale sull'implementazione di JPA (sto usando EclipseLink, comunque).

Tutte le possibili implementazioni hanno i loro vantaggi e svantaggi per quanto riguarda:

  • hashCode()/ equals()conformità del contratto (immutabilità) per List/ Setoperazioni
  • Se è possibile rilevare oggetti identici (ad esempio da sessioni diverse, proxy dinamici da strutture di dati caricati pigramente)
  • Se le entità si comportano correttamente in stato distaccato (o non persistente)

Per quanto posso vedere, ci sono tre opzioni :

  1. Non ignorarli; fare affidamento su Object.equals()eObject.hashCode()
    • hashCode()/ equals()lavoro
    • impossibile identificare oggetti identici, problemi con proxy dinamici
    • nessun problema con entità distaccate
  2. Sostituiscili, in base alla chiave primaria
    • hashCode()/ equals()sono rotti
    • identità corretta (per tutte le entità gestite)
    • problemi con entità distaccate
  3. Sostituiscili, in base all'ID commerciale (campi chiave non primaria; che dire delle chiavi esterne?)
    • hashCode()/ equals()sono rotti
    • identità corretta (per tutte le entità gestite)
    • nessun problema con entità distaccate

Le mie domande sono:

  1. Ho perso un'opzione e / o un punto pro / con?
  2. Quale opzione hai scelto e perché?



AGGIORNAMENTO 1:

Per " hashCode()/ equals()sono interrotti", intendo che hashCode()invocazioni successive possono restituire valori diversi, che (se implementati correttamente) non sono rotti nel senso della Objectdocumentazione API, ma che causano problemi quando si tenta di recuperare un'entità modificata da un Map, Seto altro basato sull'hash Collection. Di conseguenza, le implementazioni JPA (almeno EclipseLink) non funzioneranno correttamente in alcuni casi.

AGGIORNAMENTO 2:

Grazie per le vostre risposte - la maggior parte di loro ha una qualità notevole.
Sfortunatamente, non sono ancora sicuro di quale approccio sarà il migliore per un'applicazione reale o come determinare l'approccio migliore per la mia applicazione. Quindi, terrò aperta la domanda e spero in ulteriori discussioni e / o opinioni.


4
Non capisco cosa intendi con "hashCode () / equals () broken"
nanda,

4
In questo senso non sarebbero "spezzati" in questo senso, poiché nelle opzioni 2 e 3 implementeresti equals () e hashCode () usando la stessa strategia.
matt b

11
Ciò non vale per l'opzione 3. hashCode () e equals () dovrebbero usare gli stessi criteri, quindi se uno dei tuoi campi cambia, sì, il metodo hashcode () restituirà un valore diverso per la stessa istanza rispetto al precedente, ma così sarà uguale a (). Hai lasciato fuori la seconda parte della frase dal javadoc hashcode (): Ogni volta che viene invocato più volte sullo stesso oggetto durante l'esecuzione di un'applicazione Java, il metodo hashCode deve restituire costantemente lo stesso numero intero, senza fornire informazioni utilizzato in confronti uguali sull'oggetto viene modificato .
matt b

1
In realtà quella parte della frase significa l'opposto: invocare hashcode()la stessa istanza dell'oggetto dovrebbe restituire lo stesso valore, a meno che i campi utilizzati equals()nell'implementazione non cambino. In altre parole, se hai tre campi nella tua classe e il tuo equals()metodo ne utilizza solo due per determinare l'uguaglianza delle istanze, puoi aspettarti che il hashcode()valore restituito cambi se cambi uno di quei valori del campo - il che ha senso se consideri che questa istanza dell'oggetto non è più "uguale" al valore rappresentato dalla vecchia istanza.
matt b

2
"problemi durante il tentativo di recuperare un'entità modificata da una mappa, un set o altre raccolte basate sull'hash" ... questo dovrebbe essere "problemi quando si tenta di recuperare un'entità modificata da un HashMap, HashSet o altre raccolte basate sull'hash"
nanda

Risposte:


122

Leggi questo bellissimo articolo sull'argomento: non lasciare che l'ibernazione rubi la tua identità .

La conclusione dell'articolo va così:

L'identità dell'oggetto è ingannevolmente difficile da implementare correttamente quando gli oggetti vengono mantenuti in un database. Tuttavia, i problemi derivano interamente dal consentire agli oggetti di esistere senza un id prima di essere salvati. Siamo in grado di risolvere questi problemi togliendoci la responsabilità di assegnare gli ID oggetto ai framework di mappatura relazionale agli oggetti come Hibernate. Al contrario, gli ID oggetto possono essere assegnati non appena l'oggetto viene istanziato. Ciò rende l'identità dell'oggetto semplice e priva di errori e riduce la quantità di codice necessaria nel modello di dominio.


21
No, questo non è un bell'articolo. Questo è un fantastico articolo sull'argomento, e dovrebbe essere necessario leggere per ogni programmatore dell'APP! +1!
Tom Anderson,

2
Sì, sto usando la stessa soluzione. Non consentire al DB di generare l'ID ha anche altri vantaggi, come la possibilità di creare un oggetto e già creare altri oggetti che lo fanno riferimento prima di persistere. Ciò può rimuovere la latenza e più cicli di richiesta / risposta nelle app client-server. Se hai bisogno di ispirazione per tale soluzione, dai un'occhiata ai miei progetti: suid.js e suid-server-java . Praticamente suid.jsrecupera i blocchi ID da suid-server-javacui è possibile ottenere e utilizzare il lato client.
Stijn de Witt,

2
Questo è semplicemente folle. Sono nuovo a ibernare i meccanismi sotto il cofano, stavo scrivendo unit test e ho scoperto che non posso cancellare un oggetto da un set dopo averlo modificato, ho concluso che è a causa della modifica dell'hashcode, ma non sono riuscito a capire come risolvere. L'articolo è semplicemente stupendo!
XMight

È un ottimo articolo. Tuttavia, per le persone che vedono il collegamento per la prima volta, suggerirei che potrebbe essere eccessivo per la maggior parte delle applicazioni. Le altre 3 opzioni elencate in questa pagina dovrebbero più o meno risolvere il problema in più modi.
HopeKing

1
Hibernate / JPA utilizza il metodo equals e hashcode di un'entità per verificare se il record esiste già nel database?
Tushar Banne,

64

Sovrascrivo sempre uguale a / hashcode e lo implemento in base all'ID aziendale. Sembra la soluzione più ragionevole per me. Vedi il seguente link .

Per riassumere tutto questo, ecco un elenco di ciò che funzionerà o non funzionerà con i diversi modi di gestire equals / hashCode: inserisci qui la descrizione dell'immagine

MODIFICA :

Per spiegare perché questo funziona per me:

  1. Di solito non uso una raccolta basata su hash (HashMap / HashSet) nella mia applicazione JPA. Se devo, preferisco creare la soluzione UniqueList.
  2. Penso che cambiare l'id di business in fase di runtime non sia una buona pratica per qualsiasi applicazione di database. In rari casi in cui non esiste altra soluzione, farei un trattamento speciale come rimuovere l'elemento e rimetterlo nella raccolta basata sull'hash.
  3. Per il mio modello, ho impostato l'ID commerciale sul costruttore e non lo fornisce ai setter. Ho lasciato l'implementazione JPA per modificare il campo anziché la proprietà.
  4. La soluzione UUID sembra essere eccessiva. Perché UUID se hai un ID commerciale naturale? Dopotutto, impostarei l'unicità dell'ID commerciale nel database. Perché allora avere TRE indici per ogni tabella nel database?

1
Ma in questa tabella manca una quinta riga "funziona con Elenco / Set" (se si pensa di rimuovere un'entità che fa parte di un Set da un mapping OneToMany) a cui verrà risposto "No" nelle ultime due opzioni perché hashCode ( ) modifiche che violano il suo contratto.
MRalwasser,

Vedi il commento sulla domanda. Sembra che tu fraintenda il contratto uguale / hashcode
nanda

1
@MRalwasser: Penso che tu intenda la cosa giusta, non è proprio il contratto uguale / hashCode () a essere violato. Ma un mutable uguale a / hashCode crea problemi con il contratto Set .
Chris Lercher,

3
@MRalwasser: l'hashcode può cambiare solo se cambia l'ID aziendale e il punto è che l'ID aziendale non cambia. Quindi l'hashcode non cambia e funziona perfettamente con le raccolte hash.
Tom Anderson,

1
Cosa succede se non si dispone di una chiave business naturale? Ad esempio nel caso di un punto bidimensionale, Punto (X, Y), in un'applicazione di disegno grafico? Come memorizzeresti quel punto come Entità?
jhegedus,

35

Di solito abbiamo due ID nelle nostre entità:

  1. È solo per il livello di persistenza (in modo che il provider di persistenza e il database possano capire le relazioni tra gli oggetti).
  2. È per le nostre esigenze applicative ( equals()e hashCode()in particolare)

Guarda:

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

EDIT: chiarire il mio punto in merito alle chiamate al setUuid()metodo. Ecco uno scenario tipico:

User user = new User();
// user.setUuid(UUID.randomUUID()); // I should have called it here
user.setName("Master Yoda");
user.setEmail("yoda@jedicouncil.org");

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

Quando eseguo i test e visualizzo l'output del registro, risolvo il problema:

User user = new User();
user.setUuid(UUID.randomUUID());

In alternativa, si può fornire un costruttore separato:

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

Quindi il mio esempio sarebbe simile a questo:

User user = new User(UUID.randomUUID());
...
jediSet.add(user); // no bug this time

em.persist(user); // and no log output

Uso un costruttore predefinito e un setter, ma potresti trovare un approccio a due costruttori più adatto a te.


2
Credo che questa sia una soluzione corretta e buona. Potrebbe anche avere un piccolo vantaggio in termini di prestazioni, poiché gli interi di solito hanno prestazioni migliori negli indici del database rispetto agli uuidi. Ma a parte questo, potresti probabilmente eliminare l'attuale proprietà ID intero e sostituirla con l'Uuid (applicazione assegnata)?
Chris Lercher,

4
In che modo differisce dall'uso dei metodi hashCode/ default equalsper l'uguaglianza JVM e idper l'uguaglianza di persistenza? Questo non ha alcun senso per me.
Behrang Saeedzadeh

2
Funziona nei casi in cui sono presenti più oggetti entità che puntano alla stessa riga in un database. Object's equals()sarebbero tornati falsein questo caso. equals()Ritorni basati su UUID true.
Andrew Андрей Листочкин il

4
-1 - Non vedo alcun motivo per avere due ID e quindi due tipi di identità. Questo mi sembra del tutto inutile e potenzialmente dannoso.
Tom Anderson,

1
Ci scusiamo per aver criticato la tua soluzione senza indicarne una che preferirei. In breve, darei agli oggetti un singolo campo ID, implementerei uguale e hashCode basato su di esso, e genererei il suo valore sulla creazione dell'oggetto, piuttosto che durante il salvataggio nel database. In questo modo, tutte le forme dell'oggetto funzionano allo stesso modo: non persistente, persistente e distaccato. Anche i proxy di ibernazione (o simili) dovrebbero funzionare correttamente e penso che non dovrebbe nemmeno essere idratato per gestire le chiamate uguali e hashCode.
Tom Anderson,

31

Personalmente ho già usato tutte e tre queste categorie in diversi progetti. E devo dire che l'opzione 1 è secondo me la più praticabile in un'app di vita reale. Nella mia esperienza, rompere la conformità hashCode () / equals () porta a molti bug folli come ogni volta finirai in situazioni in cui il risultato di uguaglianza cambia dopo che un'entità è stata aggiunta a una collezione.

Ma ci sono ulteriori opzioni (anche con i loro pro e contro):


a) hashCode / equivale a una serie di campi immutabili , non nulli , assegnati dal costruttore

(+) tutti e tre i criteri sono garantiti

I valori del campo (-) devono essere disponibili per creare una nuova istanza

(-) complica la gestione se è necessario modificarne una


b) hashCode / equivale a una chiave primaria assegnata dall'applicazione (nel costruttore) anziché da JPA

(+) tutti e tre i criteri sono garantiti

(-) Non puoi trarre vantaggio da semplici strategie di generazione di ID affidabili come sequenze di DB

(-) complicato se nuove entità vengono create in un ambiente distribuito (client / server) o cluster di server di app


c) hashCode / equivale a un UUID assegnato dal costruttore dell'entità

(+) tutti e tre i criteri sono garantiti

(-) sovraccarico di generazione UUID

(-) può essere un piccolo rischio che venga utilizzato il doppio dello stesso UUID, a seconda dell'algoritmo utilizzato (può essere rilevato da un indice univoco su DB)


Sono fan di opzione 1 e l'approccio C anche. Non fare nulla finché non ne hai assolutamente bisogno è l'approccio più agile.
Adam Gent,

2
+1 per l'opzione (b). IMHO, se un'entità ha un ID aziendale naturale, questa dovrebbe essere anche la chiave primaria del database. È una progettazione di database semplice, diretta e buona. Se non ha un tale ID, è necessaria una chiave surrogata. Se lo imposti alla creazione dell'oggetto, tutto il resto è semplice. È quando le persone non usano una chiave naturale e non generano una chiave surrogata in anticipo che si mettono nei guai. Per quanto riguarda la complessità nell'attuazione - sì, ce n'è. Ma davvero non molto, e può essere fatto in un modo molto generico che lo risolve una volta per tutte le entità.
Tom Anderson,

Preferisco anche l'opzione 1, ma poi come scrivere unit test per affermare la piena uguaglianza è un grosso problema, perché dobbiamo implementare il metodo equals per Collection.
OOD Waterball,

Basta non farlo. Vedere Don't Mess With The Hibernate
alonana,

29

Se vuoi usare equals()/hashCode()per i tuoi Set, nel senso che la stessa entità può essere presente una sola volta, allora c'è una sola opzione: Opzione 2. Questo perché una chiave primaria per un'entità per definizione non cambia mai (se qualcuno effettivamente aggiorna non è più la stessa entità)

Dovresti prenderlo alla lettera: poiché equals()/hashCode()sei basato sulla chiave primaria, non devi usare questi metodi fino a quando non viene impostata la chiave primaria. Quindi non dovresti mettere entità nel set, fino a quando non viene assegnata una chiave primaria. (Sì, UUID e concetti simili possono aiutare ad assegnare le chiavi primarie in anticipo.)

Ora, in teoria, è anche possibile ottenerlo con l'opzione 3, anche se i cosiddetti "business-keys" hanno il brutto svantaggio che possono cambiare: "Tutto ciò che dovrete fare è eliminare le entità già inserite dal set ( s) e reinserirli. " Questo è vero, ma significa anche che, in un sistema distribuito, dovrai assicurarti che ciò avvenga assolutamente ovunque siano stati inseriti i dati (e dovrai assicurarti che l'aggiornamento sia eseguito , prima che accadano altre cose). Avrai bisogno di un sofisticato meccanismo di aggiornamento, soprattutto se alcuni sistemi remoti non sono attualmente raggiungibili ...

L'opzione 1 può essere utilizzata solo se tutti gli oggetti nei tuoi set provengono dalla stessa sessione di ibernazione. La documentazione di Hibernate lo chiarisce nel capitolo 13.1.3. Considerando l'identità dell'oggetto :

All'interno di una sessione l'applicazione può tranquillamente utilizzare == per confrontare gli oggetti.

Tuttavia, un'applicazione che utilizza == al di fuori di una sessione potrebbe produrre risultati imprevisti. Ciò potrebbe verificarsi anche in alcuni luoghi inaspettati. Ad esempio, se si inseriscono due istanze distaccate nello stesso set, entrambe potrebbero avere la stessa identità del database (ovvero, rappresentano la stessa riga). L'identità JVM, tuttavia, non è per definizione garantita per le istanze in uno stato distaccato. Lo sviluppatore deve sovrascrivere i metodi equals () e hashCode () in classi persistenti e implementare la propria nozione di uguaglianza di oggetti.

Continua a discutere a favore dell'opzione 3:

C'è un avvertimento: non usare mai l'identificatore del database per implementare l'uguaglianza. Utilizzare una chiave aziendale che è una combinazione di attributi unici, generalmente immutabili. L'identificatore del database cambierà se un oggetto transitorio viene reso persistente. Se l'istanza transitoria (di solito insieme a istanze distaccate) è contenuta in un set, la modifica dell'hashcode interrompe il contratto del set.

Questo è vero, se tu

  • impossibile assegnare l'id in anticipo (ad es. utilizzando gli UUID)
  • eppure vuoi assolutamente mettere i tuoi oggetti in set mentre sono in stato transitorio.

Altrimenti, sei libero di scegliere l'opzione 2.

Quindi menziona la necessità di una stabilità relativa:

Gli attributi per le chiavi aziendali non devono essere stabili come le chiavi primarie del database; devi solo garantire stabilità finché gli oggetti sono nello stesso Set.

Questo è corretto. Il problema pratico che vedo in questo è: se non puoi garantire la stabilità assoluta, come sarai in grado di garantire la stabilità "fintanto che gli oggetti sono nello stesso set". Posso immaginare alcuni casi speciali (come usare i set solo per una conversazione e poi buttarli via), ma metterei in dubbio la praticabilità generale di questo.


Versione breve:

  • L'opzione 1 può essere utilizzata solo con oggetti all'interno di una singola sessione.
  • Se è possibile, utilizzare l'opzione 2. (Assegnare PK il prima possibile, poiché non è possibile utilizzare gli oggetti nei set fino a quando non viene assegnato il PK.)
  • Se puoi garantire la stabilità relativa, puoi usare l'opzione 3. Ma fai attenzione.

La tua ipotesi che la chiave primaria non cambi mai è falsa. Ad esempio, Hibernate alloca la chiave primaria solo quando la sessione viene salvata. Pertanto, se si utilizza la chiave primaria come hashCode, il risultato di hashCode () prima di salvare l'oggetto per la prima volta e dopo averlo salvato per la prima volta sarà diverso. Peggio ancora, prima di salvare la sessione, due oggetti appena creati avranno lo stesso hashCode e possono sovrascriversi quando aggiunti alle raccolte. Potresti trovarti a dover forzare immediatamente un salvataggio / flush sulla creazione dell'oggetto per usare quell'approccio.
William Billingsley,

2
@William: la chiave primaria di un'entità non cambia. La proprietà id dell'oggetto mappato può cambiare. Ciò si verifica, come spiegato, soprattutto quando un oggetto transitorio viene reso persistente . Si prega di leggere attentamente la parte della mia risposta, in cui ho parlato dei metodi equals / hashCode: "non è necessario utilizzare questi metodi fino a quando non viene impostata la chiave primaria".
Chris Lercher,

Completamente d'accordo. Con l'opzione 2, puoi anche fattorizzare equals / hashcode in una super classe e riutilizzarlo da tutte le tue entità.
Theo

+1 Sono nuovo di JPA, ma alcuni dei commenti e delle risposte qui implicano che le persone non capiscono il significato del termine "chiave primaria".
Raedwald,

16
  1. Se si dispone di una chiave aziendale , è necessario utilizzarla per equals/ hashCode.
  2. Se non si dispone di una chiave aziendale, non è necessario lasciarla con le Objectimplementazioni uguali e hashCode predefinite perché non funziona dopo l'utente mergee l'entità.
  3. È possibile utilizzare l'identificatore di entità come suggerito in questo post . L'unico problema è che è necessario utilizzare hashCodeun'implementazione che restituisce sempre lo stesso valore, in questo modo:

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

Cosa è meglio: (1) onjava.com/pub/a/onjava/2006/09/13/… o (2) vladmihalcea.com/… ? La soluzione (2) è più semplice di (1). Quindi perché dovrei usare (1). Gli effetti di entrambi sono uguali? Entrambi garantiscono la stessa soluzione?
nimo23,

E con la tua soluzione: "il valore hashCode non cambia" tra le stesse istanze. Questo ha lo stesso comportamento di se fosse lo "stesso" uuido (dalla soluzione (1)) a confronto. Ho ragione?
nimo23,

1
E archiviare l'UUID nel database e aumentare l'impronta del record e nel pool di buffer? Penso che questo possa portare a più problemi di prestazioni a lungo termine rispetto all'esclusivo hashCode. Per quanto riguarda l'altra soluzione, puoi verificarla per vedere se fornisce coerenza in tutte le transizioni dello stato dell'entità. Puoi trovare il test che lo verifica su GitHub .
Vlad Mihalcea,

1
Se si dispone di una chiave business immutabile, l'hashCode può usarla e trarrà vantaggio da più bucket, quindi vale la pena utilizzarla se ne hai una. Altrimenti, basta usare l'identificatore di entità come spiegato nel mio articolo.
Vlad Mihalcea,

1
Sono felice che ti sia piaciuto. Ho centinaia di altri articoli su JPA e Hibernate.
Vlad Mihalcea,

10

Sebbene l'utilizzo di una chiave aziendale (opzione 3) sia l'approccio più comunemente raccomandato ( wiki della community di Hibernate , "Java Persistence with Hibernate" p. 398), e questo è ciò che usiamo principalmente, c'è un bug di Hibernate che rompe questo per entusiasti set: HHH-3799 . In questo caso, Hibernate può aggiungere un'entità a un set prima che i suoi campi siano inizializzati. Non sono sicuro del motivo per cui questo bug non abbia attirato più attenzione, in quanto rende problematico l'approccio chiave aziendale raccomandato.

Penso che il nocciolo della questione sia che eguaglia e hashCode dovrebbe essere basato sullo stato immutabile (riferimento Odersky et al. ), E un'entità Hibernate con chiave primaria gestita da Hibernate non ha tale stato immutabile. La chiave primaria viene modificata da Hibernate quando un oggetto transitorio diventa persistente. La chiave aziendale viene inoltre modificata da Hibernate, quando idrata un oggetto durante il processo di inizializzazione.

Ciò lascia solo l'opzione 1, ereditando le implementazioni java.lang.Object basate sull'identità dell'oggetto o utilizzando una chiave primaria gestita dall'applicazione come suggerito da James Brundege in "Non permettere a Hibernate di rubare la tua identità" (a cui fa già riferimento la risposta di Stijn Geukens ) e di Lance Arlaus in "Generazione di oggetti: un approccio migliore all'integrazione in letargo " .

Il problema più grande con l'opzione 1 è che le istanze staccate non possono essere confrontate con istanze persistenti usando .equals (). Ma va bene; il contratto di uguaglianza e hashCode lascia allo sviluppatore decidere cosa significhi l'uguaglianza per ogni classe. Quindi lascia che equals e hashCode ereditino da Object. Se è necessario confrontare un'istanza staccata con un'istanza persistente, è possibile creare un nuovo metodo in modo esplicito a tale scopo, forse boolean sameEntityo boolean dbEquivalento boolean businessEquals.


5

Sono d'accordo con la risposta di Andrew. Facciamo la stessa cosa nella nostra applicazione ma invece di archiviare gli UUID come VARCHAR / CHAR, li dividiamo in due valori lunghi. Vedi UUID.getLeastSignificantBits () e UUID.getMostSignificantBits ().

Un'altra cosa da considerare è che le chiamate a UUID.randomUUID () sono piuttosto lente, quindi potresti voler esaminare pigramente la generazione dell'UUID solo quando necessario, come durante la persistenza o le chiamate a equals () / hashCode ()

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

}

Bene, in realtà se sovrascrivi equals () / hashCode (), devi comunque generare UUID per ogni entità (suppongo che tu voglia persistere ogni entità che crei nel tuo codice). Lo fai solo una volta, prima di memorizzarlo in un database per la prima volta. Successivamente, l'UUID viene appena caricato dal provider di persistenza. Quindi non vedo il punto di farlo pigramente.
Andrew Андрей Листочкин,

Ho votato a favore della tua risposta perché mi piacciono molto le altre tue idee: memorizzare UUID come coppia di numeri nel database e non eseguire il cast su un tipo particolare all'interno del metodo equals () - che uno è davvero pulito! Userò sicuramente questi due trucchi in futuro.
Andrew Андрей Листочкин,

1
Grazie per il voto positivo. Il motivo per inizializzare pigramente l'UUID è stato nella nostra app che creiamo molte entità che non vengono mai inserite in una HashMap o persistono. Quindi abbiamo visto un calo delle prestazioni 100 volte durante la creazione dell'oggetto (100.000 di loro). Quindi iniziamo l'UUID solo se è necessario. Vorrei solo che ci fosse un buon supporto in MySql per i numeri a 128 bit in modo da poter usare anche l'UUID per l'id e non preoccuparci dell'incremento automatico.
Estratto il

Oh, capisco. Nel mio caso non dichiariamo nemmeno il campo UUID se l'entità corrispondente non verrà inserita nelle raccolte. Lo svantaggio è che a volte dobbiamo aggiungerlo perché in seguito si scopre che dobbiamo effettivamente inserirli nelle raccolte. Ciò accade a volte durante lo sviluppo, ma per fortuna non ci è mai successo dopo la distribuzione iniziale a un cliente, quindi non è stato un grosso problema. Se ciò dovesse accadere dopo che il sistema è diventato operativo avremmo bisogno di una migrazione db. UUID pigro sono molto utili in tali situazioni.
Andrew Андрей Листочкин,

Forse dovresti anche provare il generatore UUID più veloce che Adam ha suggerito nella sua risposta se le prestazioni sono un problema critico nella tua situazione.
Andrew Андрей Листочкин,

3

Come altre persone molto più intelligenti di me hanno già sottolineato, ci sono numerose strategie là fuori. Sembra essere il caso, tuttavia, che la maggior parte dei modelli di design applicati provi a trovare la strada per il successo. Limitano l'accesso del costruttore se non ostacolano completamente le invocazioni del costruttore con costruttori specializzati e metodi di fabbrica. In effetti è sempre piacevole con un'API chiara. Ma se l'unico motivo è rendere le sostituzioni uguali e hashcode compatibili con l'applicazione, allora mi chiedo se quelle strategie siano conformi a KISS (Keep It Simple Stupid).

Per me, mi piace sovrascrivere equals e hashcode esaminando l'id. In questi metodi, richiedo che l'id non sia null e documenti bene questo comportamento. In questo modo diventerà il contratto per gli sviluppatori di persistere in una nuova entità prima di memorizzarlo altrove. Un'applicazione che non rispetta questo contratto fallirebbe entro un minuto (si spera).

Avvertenza: se le entità sono archiviate in tabelle diverse e il provider utilizza una strategia di generazione automatica per la chiave primaria, otterrai chiavi primarie duplicate tra i tipi di entità. In tal caso, confrontare anche i tipi di runtime con una chiamata a Object # getClass () che ovviamente renderà impossibile che due diversi tipi siano considerati uguali. Questo mi va bene per la maggior parte.


Anche con sequenze mancanti di un DB (come Mysql), è possibile simularle (ad es. Tabella hibernate_sequence). Quindi potresti sempre ottenere un ID univoco tra le tabelle. +++ Ma non ne hai bisogno. La chiamata Object#getClass() è negativa a causa dei proxy di H. La chiamata Hibernate.getClass(o)aiuta, ma rimane il problema dell'uguaglianza di entità di diverso tipo. C'è una soluzione che usa canEqual , un po 'complicata, ma utilizzabile. D'accordo che di solito non è necessario. +++ Il lancio in eq / hc su null ID viola il contratto, ma è molto pragmatico.
maaartinus,

2

Ovviamente ci sono già risposte molto istruttive qui, ma ti dirò cosa facciamo.

Non facciamo nulla (ovvero non ignoriamo).

Se abbiamo bisogno di uguale a / hashcode per funzionare per le raccolte, usiamo gli UUID. Devi solo creare l'UUID nel costruttore. Usiamo http://wiki.fasterxml.com/JugHome per UUID. UUID è un po 'più costoso dal punto di vista della CPU ma è economico rispetto alla serializzazione e all'accesso db.


1

Ho sempre usato l'opzione 1 in passato perché ero a conoscenza di queste discussioni e ho pensato che fosse meglio non fare nulla finché non avessi saputo la cosa giusta da fare. Quei sistemi funzionano ancora con successo.

Tuttavia, la prossima volta potrei provare l'opzione 2 - utilizzando l'ID generato dal database.

Hashcode e uguali genereranno IllegalStateException se l'id non è impostato.

Ciò impedirà la visualizzazione inaspettata di errori impercettibili che coinvolgono entità non salvate.

Cosa pensano le persone di questo approccio?


1

L'approccio alle chiavi aziendali non è adatto a noi. Usiamo l' ID generato dal DB , il temporaneo temporaneo ID e sovrascriviamo equal () / hashcode () per risolvere il dilemma. Tutte le entità sono discendenti di Entità. Professionisti:

  1. Nessun campo aggiuntivo nel DB
  2. Nessuna codifica aggiuntiva nelle entità discendenti, un approccio per tutti
  3. Nessun problema di prestazioni (come con UUID), generazione dell'ID DB
  4. Nessun problema con le hashmap (non è necessario tenere presente l'uso di uguale e così via)
  5. L'hashcode della nuova entità non cambia nel tempo anche dopo il persistere

Contro:

  1. Potrebbero esserci problemi con la serializzazione e la deserializzazione di entità non persistenti
  2. L'hashcode dell'entità salvata può cambiare dopo il ricaricamento dal DB
  3. Oggetti non persistenti considerati sempre diversi (forse è giusto?)
  4. Cos'altro?

Guarda il nostro codice:

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

1

Si prega di considerare il seguente approccio basato sull'identificatore di tipo predefinito e sull'ID.

I presupposti specifici per l'APP:

  • entità dello stesso "tipo" e lo stesso ID non nullo sono considerate uguali
  • le entità non persistenti (presupponendo che non siano presenti ID) non sono mai uguali alle altre entità

L'entità astratta:

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

Esempio di entità concreta:

static class Foo extends AbstractPersistable<Long> {
  public Foo() {
    super("Foo");
  }
}

Esempio di test:

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

I principali vantaggi qui:

  • semplicità
  • assicura che le sottoclassi forniscano l'identità del tipo
  • comportamento previsto con classi proxy

svantaggi:

  • Richiede che ciascuna entità chiami super()

Appunti:

  • Richiede attenzione quando si utilizza l'ereditarietà. Ad esempio, l'uguaglianza class Ae class B extends Apuò dipendere da dettagli concreti dell'applicazione.
  • Idealmente, utilizzare una chiave aziendale come ID

In attesa di vostri commenti.


0

Questo è un problema comune in ogni sistema IT che utilizza Java e JPA. Il punto critico va oltre l'implementazione di equals () e hashCode (), influenza il modo in cui un'organizzazione si riferisce a un'entità e come i suoi clienti si riferiscono alla stessa entità. Ho visto abbastanza dolore di non avere una chiave aziendale al punto che ho scritto il mio blog per esprimere la mia opinione.

In breve: utilizzare un ID sequenziale breve, leggibile dall'uomo, con prefissi significativi come chiave aziendale generata senza alcuna dipendenza da alcun archivio diverso dalla RAM. Twitter Snowflake è un ottimo esempio.


0

IMO hai 3 opzioni per implementare equals / hashCode

  • Utilizzare un'identità generata dall'applicazione, ad esempio un UUID
  • Implementalo in base a una chiave aziendale
  • Implementarlo in base alla chiave primaria

L'uso di un'identità generata da un'applicazione è l'approccio più semplice, ma presenta alcuni aspetti negativi

  • I join sono più lenti quando lo si utilizza come PK perché 128 bit è semplicemente maggiore di 32 o 64 bit
  • "Il debug è più difficile" perché controllare con i tuoi occhi se alcuni dati sono corretti è piuttosto difficile

Se riesci a lavorare con questi aspetti negativi , usa questo approccio.

Per ovviare al problema del join, è possibile utilizzare l'UUID come chiave naturale e un valore di sequenza come chiave primaria, ma è possibile che si verifichino comunque problemi di implementazione equals / hashCode in entità figlio di composizione che hanno ID incorporati poiché si desidera unire in base sulla chiave primaria. L'uso della chiave naturale nell'ID entità figlio e la chiave primaria per fare riferimento al genitore è un buon compromesso.

@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 questo è l'approccio più pulito in quanto eviterà tutti gli aspetti negativi e allo stesso tempo ti fornirà un valore (l'UUID) che puoi condividere con i sistemi esterni senza esporre gli interni del sistema.

Implementalo in base a una chiave aziendale se puoi aspettarti che da un utente sia una buona idea, ma ha anche alcuni aspetti negativi

Il più delle volte questa chiave di business sarà una sorta di codice fornito dall'utente e meno spesso un composto di più attributi.

  • I join sono più lenti perché l'unione basata su testo a lunghezza variabile è semplicemente lenta. Alcuni DBMS potrebbero anche avere problemi a creare un indice se la chiave supera una certa lunghezza.
  • Nella mia esperienza, le chiavi aziendali tendono a cambiare, il che richiederà aggiornamenti a cascata degli oggetti che si riferiscono ad essa. Questo è impossibile se i sistemi esterni si riferiscono ad esso

IMO non dovresti implementare o lavorare esclusivamente con una chiave aziendale. È un bel componente aggiuntivo, vale a dire gli utenti possono cercare rapidamente per quella chiave aziendale, ma il sistema non dovrebbe fare affidamento su di esso per operare.

L'implementazione basata sulla chiave primaria ha i suoi problemi, ma forse non è un grosso problema

Se è necessario esporre gli ID a un sistema esterno, utilizzare l'approccio UUID che ho suggerito. In caso contrario, è comunque possibile utilizzare l'approccio UUID ma non è necessario. Il problema dell'utilizzo di un ID generato da DBMS in equals / hashCode deriva dal fatto che l'oggetto potrebbe essere stato aggiunto a raccolte basate su hash prima di assegnare l'id.

Il modo ovvio per aggirare il problema è semplicemente quello di non aggiungere l'oggetto alle raccolte basate sull'hash prima di assegnare l'id. Capisco che questo non è sempre possibile perché potresti voler deduplicare prima di assegnare già l'id. Per poter ancora utilizzare le raccolte basate sull'hash, devi semplicemente ricostruire le raccolte dopo aver assegnato l'id.

Potresti fare qualcosa del genere:

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

Non ho testato personalmente l'approccio esatto, quindi non sono sicuro di come cambino le raccolte negli eventi pre e post persistenti ma l'idea è:

  • Rimuovere temporaneamente l'oggetto dalle raccolte basate sull'hash
  • Persistilo
  • Aggiungere nuovamente l'oggetto alle raccolte basate sull'hash

Un altro modo per risolvere questo problema è semplicemente ricostruire tutti i modelli basati su hash dopo un aggiornamento / persistere.

Alla fine, dipende da te. Personalmente utilizzo l'approccio basato sulla sequenza per la maggior parte del tempo e utilizzo l'approccio UUID solo se devo esporre un identificatore a sistemi esterni.


0

Con il nuovo stile di instanceofda Java 14, è possibile implementare equalsin una riga.

@Override
public boolean equals(Object obj) {
    return this == obj || id != null && obj instanceof User otherUser && id.equals(otherUser.id);
}

@Override
public int hashCode() {
    return 31;
}

-1

Se UUID è la risposta per molte persone, perché non utilizziamo semplicemente i metodi factory dal livello aziendale per creare le entità e assegnare la chiave primaria al momento della creazione?

per esempio:

@ManagedBean
public class MyCarFacade {
  public Car createCar(){
    Car car = new Car();
    em.persist(car);
    return car;
  }
}

in questo modo otterremmo una chiave primaria predefinita per l'entità dal provider di persistenza e le nostre funzioni hashCode () e equals () potrebbero fare affidamento su di essa.

Potremmo anche dichiarare protetti i costruttori dell'auto e quindi utilizzare la riflessione nel nostro metodo commerciale per accedervi. In questo modo gli sviluppatori non sarebbero intenzionati a creare un'istanza di Car con il nuovo, ma con il metodo di fabbrica.

Che ne dici?


Un approccio che funziona alla grande se si è disposti a subire il colpo di prestazione sia di generare il guid quando si fa una ricerca nel database.
Michael Wiles,

1
Che dire dell'unità di test auto? In questo caso è necessaria una connessione al database per i test? Inoltre, gli oggetti del tuo dominio non dovrebbero dipendere dalla persistenza.
jhegedus,

-1

Ho provato a rispondere a questa domanda da solo e non sono mai stato completamente soddisfatto delle soluzioni trovate fino a quando non ho letto questo post e soprattutto DREW. Mi è piaciuto il modo in cui pigro ha creato l'UUID e lo ha archiviato in modo ottimale.

Ma volevo aggiungere ancora più flessibilità, ad esempio lazy create UUID SOLO quando si accede a hashCode () / equals () prima della prima persistenza dell'entità con i vantaggi di ciascuna soluzione:

  • equals () significa "oggetto si riferisce alla stessa entità logica"
  • utilizzare l'ID del database il più possibile perché perché dovrei fare il lavoro due volte (preoccupazione per le prestazioni)
  • prevenire problemi durante l'accesso a hashCode () / equals () su entità non ancora persistente e mantenere lo stesso comportamento dopo che è stato persistito

Apprezzerei davvero il feedback sulla mia soluzione mista di seguito

public class MyEntity { 

    @Id()
    @Column(name = "ID", length = 20, nullable = false, unique = true)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id = null;

    @Transient private UUID uuid = null;

    @Column(name = "UUID_MOST", nullable = true, unique = false, updatable = false)
    private Long uuidMostSignificantBits = null;
    @Column(name = "UUID_LEAST", nullable = true, unique = false, updatable = false)
    private Long uuidLeastSignificantBits = null;

    @Override
    public final int hashCode() {
        return this.getUuid().hashCode();
    }

    @Override
    public final boolean equals(Object toBeCompared) {
        if(this == toBeCompared) {
            return true;
        }
        if(toBeCompared == null) {
            return false;
        }
        if(!this.getClass().isInstance(toBeCompared)) {
            return false;
        }
        return this.getUuid().equals(((MyEntity)toBeCompared).getUuid());
    }

    public final UUID getUuid() {
        // UUID already accessed on this physical object
        if(this.uuid != null) {
            return this.uuid;
        }
        // UUID one day generated on this entity before it was persisted
        if(this.uuidMostSignificantBits != null) {
            this.uuid = new UUID(this.uuidMostSignificantBits, this.uuidLeastSignificantBits);
        // UUID never generated on this entity before it was persisted
        } else if(this.getId() != null) {
            this.uuid = new UUID(this.getId(), this.getId());
        // UUID never accessed on this not yet persisted entity
        } else {
            this.setUuid(UUID.randomUUID());
        }
        return this.uuid; 
    }

    private void setUuid(UUID uuid) {
        if(uuid == null) {
            return;
        }
        // For the one hypothetical case where generated UUID could colude with UUID build from IDs
        if(uuid.getMostSignificantBits() == uuid.getLeastSignificantBits()) {
            throw new Exception("UUID: " + this.getUuid() + " format is only for internal use");
        }
        this.uuidMostSignificantBits = uuid.getMostSignificantBits();
        this.uuidLeastSignificantBits = uuid.getLeastSignificantBits();
        this.uuid = uuid;
    }

cosa intendi con "UUID generato un giorno su questa entità prima che io fossi persistente"? potresti per favore fare un esempio per questo caso?
jhegedus,

potresti usare il tipo di generazione assegnato? perché è necessario il tipo di generazione dell'identità? ha qualche vantaggio rispetto a assegnato?
jhegedus,

cosa succede se 1) fai un nuovo MyEntity, 2) lo metti in un elenco, 3) poi lo salvi nel database quindi 4) carichi quell'entità indietro dal DB e 5) prova a vedere se l'istanza caricata è nella lista . La mia ipotesi è che non lo sarà anche se dovrebbe essere.
jhegedus,

Grazie per i tuoi primi commenti che mi hanno mostrato che non ero chiaro come avrei dovuto. In primo luogo, "UUID generato un giorno su questa entità prima che io fossi persistito" era un errore di battitura ... "prima che l'IT fosse persistito" avrebbe dovuto essere letto invece. Per le altre osservazioni, modificherò presto il mio post per cercare di spiegare meglio la mia soluzione.
user2083808

-1

In pratica sembra che l'opzione 2 (chiave primaria) sia utilizzata più frequentemente. La chiave business naturale e IMMUTABILE è raramente cosa da fare, la creazione e il supporto di chiavi sintetiche sono troppo pesanti per risolvere situazioni che probabilmente non sono mai avvenute. Dai un'occhiata alla primavera-dati-JPA AbstractPersistable implementazione (l'unica cosa: per l'uso implementazione HibernateHibernate.getClass ).

public boolean equals(Object obj) {
    if (null == obj) {
        return false;
    }
    if (this == obj) {
        return true;
    }
    if (!getClass().equals(ClassUtils.getUserClass(obj))) {
        return false;
    }
    AbstractPersistable<?> that = (AbstractPersistable<?>) obj;
    return null == this.getId() ? false : this.getId().equals(that.getId());
}

@Override
public int hashCode() {
    int hashCode = 17;
    hashCode += null == getId() ? 0 : getId().hashCode() * 31;
    return hashCode;
}

Solo a conoscenza della manipolazione di nuovi oggetti in HashSet / HashMap. Al contrario, l'opzione 1 (rimanere in Objectesecuzione) viene interrotta subito dopo merge, questa è una situazione molto comune.

Se non si dispone di una chiave aziendale e si ha un REALE bisogno di manipolare una nuova entità nella struttura dell'hash, eseguire hashCodel' override su costante, come indicato da Vlad Mihalcea.


-2

Di seguito è una soluzione semplice (e testata) per Scala.

  • Si noti che questa soluzione non rientra in nessuna delle 3 categorie indicate nella domanda.

  • Tutte le mie Entità sono sottoclassi di UUIDEntity, quindi seguo il principio di non ripeterti (DRY).

  • Se necessario, la generazione UUID può essere resa più precisa (utilizzando più numeri pseudo-casuali).

Codice Scala:

import javax.persistence._
import scala.util.Random

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
abstract class UUIDEntity {
  @Id  @GeneratedValue(strategy = GenerationType.TABLE)
  var id:java.lang.Long=null
  var uuid:java.lang.Long=Random.nextLong()
  override def equals(o:Any):Boolean= 
    o match{
      case o : UUIDEntity => o.uuid==uuid
      case _ => false
    }
  override def hashCode() = uuid.hashCode()
}
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.