2011-12-31 5 views
6

Ho un problema con hashCode() che delega a oggetti non inizializzati utilizzando la modalità di sospensione.Delega di funzione hash a delegati non inizializzati in cause di ibernazione che modificano hashCode

Il mio modello di dati appare come segue (il seguente codice è altamente potata a sottolineare il problema e quindi rotto, non replicare!):

class Compound { 
    @FetchType.EAGER 
    Set<Part> parts = new HashSet<Part>(); 

    String someUniqueName; 

    public int hashCode() { 
    final int prime = 31; 
    int result = 1; 
    result = prime * result + ((getSomeUniqueName() == null) ? 0 : getSomeUniqueName().hashCode()); 
    return result; 
    } 
} 

class Part { 
    Compound compound; 

    String someUniqueName; 

    public int hashCode() { 
    final int prime = 31; 
    int result = 1; 
    result = prime * result + ((getCompound() == null) ? 0 : getCompound().hashCode()); 
    result = prime * result + ((getSomeUniqueName() == null) ? 0 : getSomeUniqueName().hashCode()); 
    return result; 
    } 
} 

Si prega di notare che l'attuazione di hashCode() segue accuratamente il consiglio come dato in the hibernate documentation.

Ora se carico un oggetto di tipo Compound, carica con impazienza lo HasSet con le parti. Questo chiama lo hashCode() sulle parti, che a sua volta chiama lo hashCode() sul composto. Tuttavia, il problema è che a questo punto non sono ancora disponibili tutti i valori considerati per la creazione del codice hash del composto. Pertanto, il codice hash delle parti cambia dopo l'inizializzazione è stata completata, frenando così il contratto del HashSet e portando a tutti i tipi di errori difficili da rintracciare (come ad esempio avere lo stesso oggetto nelle parti impostato due volte).

Quindi la mia domanda è: qual è la soluzione più semplice per evitare questo problema (vorrei evitare di scrivere classi per il caricamento/inizializzazione personalizzato)? Devo fare qualcosa di sbagliato qui interamente?

Modifica: Mi manca qualcosa qui? Questo sembra essere un problema di base, perché non trovo nulla al riguardo da nessuna parte?

Invece di utilizzare l'identificatore di database per il confronto di uguaglianza, è necessario utilizzare una serie di proprietà per equals() che identificano i tuoi singoli oggetti. [...] Non c'è bisogno di usare l'identificatore persistente, il così chiamato "business key" è molto meglio. È una chiave naturale, ma questo tempo è non c'è niente di sbagliato nell'usarlo! (article from hibernate)

E

Si consiglia di implementare equals() e hashCode() utilizzando business uguaglianza chiave. Equità chiave business significa che il metodo equals() confronta solo le proprietà che formano la chiave aziendale. È una chiave che identificherebbe la nostra istanza nel mondo reale (una chiave candidata naturale ). (hibernate documentation)

Edit: Questa è la traccia dello stack quando il caricamento avviene (nel caso in cui questo aiuta). In quel momento, l'attributo someUniqueName è nullo e quindi l'hashCode viene calcolato erroneamente.

Compound.getSomeUniqueName() line: 263 
Compound.hashCode() line: 286 
Part.hashCode() line: 123 
HashMap<K,V>.put(K, V) line: 372  
HashSet<E>.add(E) line: 200 
HashSet<E>(AbstractCollection<E>).addAll(Collection<? extends E>) line: 305 
PersistentSet.endRead() line: 352 
CollectionLoadContext.endLoadingCollection(LoadingCollectionEntry, CollectionPersister) line: 261 
CollectionLoadContext.endLoadingCollections(CollectionPersister, List) line: 246  
CollectionLoadContext.endLoadingCollections(CollectionPersister) line: 219 
EntityLoader(Loader).endCollectionLoad(Object, SessionImplementor, CollectionPersister) line: 1005 
EntityLoader(Loader).initializeEntitiesAndCollections(List, Object, SessionImplementor, boolean) line: 993 
EntityLoader(Loader).doQuery(SessionImplementor, QueryParameters, boolean) line: 857  
EntityLoader(Loader).doQueryAndInitializeNonLazyCollections(SessionImplementor, QueryParameters, boolean) line: 274 
EntityLoader(Loader).loadEntity(SessionImplementor, Object, Type, Object, String, Serializable, EntityPersister, LockOptions) line: 2037  
EntityLoader(AbstractEntityLoader).load(SessionImplementor, Object, Object, Serializable, LockOptions) line: 86 
EntityLoader(AbstractEntityLoader).load(Serializable, Object, SessionImplementor, LockOptions) line: 76 
SingleTableEntityPersister(AbstractEntityPersister).load(Serializable, Object, LockOptions, SessionImplementor) line: 3293 
DefaultLoadEventListener.loadFromDatasource(LoadEvent, EntityPersister, EntityKey, LoadEventListener$LoadType) line: 496  
DefaultLoadEventListener.doLoad(LoadEvent, EntityPersister, EntityKey, LoadEventListener$LoadType) line: 477  
DefaultLoadEventListener.load(LoadEvent, EntityPersister, EntityKey, LoadEventListener$LoadType) line: 227 
DefaultLoadEventListener.proxyOrLoad(LoadEvent, EntityPersister, EntityKey, LoadEventListener$LoadType) line: 269 
DefaultLoadEventListener.onLoad(LoadEvent, LoadEventListener$LoadType) line: 152  
SessionImpl.fireLoad(LoadEvent, LoadEventListener$LoadType) line: 1090 
SessionImpl.internalLoad(String, Serializable, boolean, boolean) line: 1038 
ManyToOneType(EntityType).resolveIdentifier(Serializable, SessionImplementor) line: 630 
ManyToOneType(EntityType).resolve(Object, SessionImplementor, Object) line: 438 
TwoPhaseLoad.initializeEntity(Object, boolean, SessionImplementor, PreLoadEvent, PostLoadEvent) line: 139 
QueryLoader(Loader).initializeEntitiesAndCollections(List, Object, SessionImplementor, boolean) line: 982 
QueryLoader(Loader).doQuery(SessionImplementor, QueryParameters, boolean) line: 857 
QueryLoader(Loader).doQueryAndInitializeNonLazyCollections(SessionImplementor, QueryParameters, boolean) line: 274 
QueryLoader(Loader).doList(SessionImplementor, QueryParameters) line: 2542 
QueryLoader(Loader).listIgnoreQueryCache(SessionImplementor, QueryParameters) line: 2276  
QueryLoader(Loader).list(SessionImplementor, QueryParameters, Set, Type[]) line: 2271 
QueryLoader.list(SessionImplementor, QueryParameters) line: 459 
QueryTranslatorImpl.list(SessionImplementor, QueryParameters) line: 365 
HQLQueryPlan.performList(QueryParameters, SessionImplementor) line: 196 
SessionImpl.list(String, QueryParameters) line: 1268  
QueryImpl.list() line: 102 
<my code where the query is executed> 
+0

* (non è una risposta alla tua domanda) * ... * "Si noti che l'implementazione di hashCode segue completamente il consiglio dato nella documentazione di ibernazione" * [sic]. L'articolo che si collega a comporre i due codici hash moltiplicando per un numero primo prima di aggiungere, che è molto più comune di una semplice somma. Ti consiglio di seguire * veramente * attentamente quel consiglio e di moltiplicare il tuo * composto * con un numero primo prima di fare l'aggiunta. O quello o fai XOR i due hash. Ma aggiungerli tipicamente aumenta i rischi di collisioni. – TacticalCoder

+0

Grazie per la raccomandazione. Il codice effettivo lo fa. Tuttavia questo non aiuta esattamente ad illustrare il problema, ed è per questo che l'ho rimosso. Come spiegato nella nota precedente: "* il seguente codice è fortemente potato per sottolineare il problema *". – roesslerj

+0

Non è la soluzione più ideale, ma potrebbe risolvere il tuo problema. Per le varie raccolte di hash, la prima chiamata è per hashCode * come ottimizzazione *. Quindi equals() viene chiamato se i codici hash sono uguali. Potresti avere hashCode() restituire sempre 1, assumendo che il tuo metodo equals() non soffra dello stesso problema (ad esempio, un valore non inizializzato di compound.someUniqueValue). Come si presenta il metodo equals()? – Saish

risposta

2

Hai un perfetto caso d'uso legittimo, e in effetti dovrebbe funzionare. Tuttavia, avresti lo stesso problema in Java normale, se imposti le "parti" dell'oggetto Compound prima di impostare "someUniqueName".

Quindi, se è possibile convincere l'ibernazione per impostare la proprietà 'someUniqueName' prima della proprietà 'parti'. Hai provato a riordinarli nella classe java? O rinominando "parti" in "zparts"? I documenti di ibernazione dicono solo che l'ordine non è garantito. Mi piacerebbe un bug in ibernazione per consentire di far rispettare questo ordine ...

Un'altra soluzione che potrebbe essere più facile:

class Part { 
    public int hashCode() { 
    //don't include getCompound().hashCode() 
    return getSomeUniqueName() == null ? 0 : getSomeUniqueName().hashCode(); 
    } 

    public boolean equals(Object o) 
    { 
    if (this == o) return true; 
    if (!o instanceof Part) return false; 

    Part part = (Part) o; 

    if (getCompound() != null ? !getCompound().equals(part.getCompound()) : part.getCompound()!= null) 
     return false; 
    if (getSomeUniqueName()!= null ? !getSomeUniqueName().equals(part.getSomeUniqueName()) : part.getSomeUniqueName()!= null) 
     return false; 

    return true; 
    } 
} 

Negli Compound.equals() Assicurarsi che inizia anche con

public boolean equals(Object o) 
{ 
    if (this == o) return true; 

Questo dovrebbe evitare il problema che si sta avendo ora.

Ogni proprietà nel metodo hashCode() deve essere nel metodo equals(), ma non necessariamente nel contrario.

+0

Mi rendo conto che è l'opposto del titolo della domanda che era: "Delega della funzione hash" :-( – greyfairer

+0

Ho provato a riordinare le proprietà e anche a rinominarle .. non ho cambiato nulla.Ho la sensazione che le collezioni siano caricate prima di qualsiasi altra proprietà. 'equals()' inizia effettivamente con il taglio che hai dato, tuttavia non vedo come questo potrebbe aiutare il mio problema? – roesslerj

+0

Ho gettato getCompound () .hashCode() fuori da hashCode() della parte, quindi non sta più delegando :-). Questo dovrebbe evitare il tuo problema. L'ho tenuto in eguale in modo che la logica aziendale non si rompa. – greyfairer

0

Una possibilità ho pensato stava seguendo i consigli come indicato nella this article. Sostanzialmente, propongono di non utilizzare l'ibernazione (o piuttosto il database) per generare ID, ma di utilizzare una libreria UUID per generare i propri ID e quindi utilizzare questi ID per equals() e hashCode(). Oltre ai problemi menzionati in questo articolo, ha alcuni seri inconvenienti per la mia attuale implementazione: romperà il mio codice esistente!Ogni volta che creerei un Part - istanza prima dovrei interrogare se esiste già nel database e recuperarlo in caso affermativo. Nella mia attuale implementazione creo semplicemente Parts come preferisco e semplicemente li aggiungo allo Compound. Se lo Compound ha già una tale parte, tutto funziona in modo automatico ...

0

Ho trovato una domanda correlata here. L'idea di base della soluzione è che l'intero problema scompare non appena non si prelevano con impazienza le parti. Quindi il composto è già completamente inizializzato quando le parti vengono caricate. Tuttavia questo apre un problema completamente diverso quando si lavora fuori dalle sessioni con oggetti distaccati ...

0

Non sono completamente sicuro, ma puoi provare a controllare se l'Oggetto che Hibernate restituisce è davvero un Oggetto che stai cercando (. classe), instanceof in questo caso non sarà un buon candidato perché il proxy è una sottoclasse. Hibernate restituirà un proxy se non tutti i membri vengono risolti e quindi l'hashcode/equals si interromperà di sicuro.

Cheers, Eugene

+0

Grazie per l'aiuto. Purtroppo questo non è il mio problema in questo caso. – roesslerj

2

Dalla tua domanda ho capito che tutte le proprietà modello che mai che partecipano a hashCode() metodo non sono stati caricati per impostazione predefinita. In tal caso, se vuoi che tutte le tue proprietà vengano caricate, puoi seguire le istruzioni.

  1. Chiamando i metodi getter in hashCode() della vostra classe del modello in quanto è inizializza/carichi tutte le proprietà del modello.
  2. Utilizzando sesstion.get() anziché il metodo session.load(), poiché non creerà alcun proxy e caricherà tutte le proprietà del modello.
  3. Impostando lazy="false" per tutte le proprietà nella mappatura.

Spero che questo possa risolvere il tuo problema!

+0

Grazie per l'aiuto. Per quanto riguarda 1: è la stessa proposta da abahgat e purtroppo non aiuta. Per 2: utilizzo 'query.list()' per recuperare l'oggetto problematico tramite query. E per 3: questo è quello che sto facendo attualmente e in realtà parte del problema ... – roesslerj

+0

Mi dispiace, forse sono stato ingannevole ... le proprietà che partecipano a 'hashCode()' SONO caricate, ma solo dopo che hashCode ha stato calcolato ... – roesslerj

2

Ho letto in uno dei commenti alla domanda che stai permettendo a Eclipse di generare i metodi equals e hashCode.

Lo hai fatto per tutte le entità (Part e Compound)? Sto chiedendo perché se questo è il caso, quei metodi generalmente accedono direttamente alle proprietà dell'oggetto (cioè senza chiamare i metodi getter). Sembrano i seguenti.

@Override 
public int hashCode() { 
    final int prime = 31; 
    int result = 1; 
    result = prime * result + ((prop == null) ? 0 : prop.hashCode()); 
    return result; 
} 

Quando si utilizza Hibernate, che spesso porta a problemi come quello che si sta descrivendo, perché le proprietà non inizializzate hanno valori di default (null per gli oggetti, 0 per int s, e così via) fino a quando il metodo get appropriata è chiamato, che fa sì che il proxy di ibernazione acceda al database e carichi i valori necessari per calcolare i valori corretti per i metodi.

È possibile individuare facilmente il problema se si attiva un debugger e si ispezionano le proprietà alla prima chiamata su hashCode().

Se ciò accade, il modo più semplice per risolvere questo problema è quello di modificare i vostri metodi per utilizzare i metodi get, come qui:

@Override 
public int hashCode() { 
    final int prime = 31; 
    int result = 1; 
    result = prime * result + ((getProp() == null) ? 0 : getProp().hashCode()); 
    return result; 
} 

Un altro punto degno di nota: l'Eclipse-generato uguale metodo contiene esegue questo controllo getClass() != obj.getClass() che non è appropriato per le entità di Hibernate che vengono estese dai proxy di Hibernate. Lo sostituirò con un controllo instanceof.

+1

+1 per il suggerimento con l'istanza di controllo. Anche l'altra cosa sembra ragionevole - controllerò. Grazie comunque! – roesslerj

+0

Mi dispiace segnalare che questo non ha risolto il mio problema. Sono entro i limiti di una sessione di ibernazione e chiamo il metodo getter - e otteniamo comunque null invece del valore effettivo dell'attributo. C'è qualcos'altro che potrei fare di sbagliato in questo senso? – roesslerj

+0

Ottieni nulla su tutti gli attributi o solo su alcuni di essi? Forse potrebbe essere utile se hai pubblicato il codice effettivo per i tuoi metodi hashcode e equals. Tiravo a indovinare. – abahgat

2

Quindi perché non utilizzare un elenco, anziché un set? So che sarebbe una soluzione alternativa piuttosto che una soluzione adeguata, ma non dovresti assolutamente giocare con gli hashcode.

+0

Grazie per l'idea. Ma in quella specifica situazione, l'implementazione di 'hashCode()' e 'equals()' mi permette di aggiungere qualsiasi oggetto appena creato al set, e se è già contenuto, non succede nulla e non è persistente. Questo mi ha permesso di avere un sacco di codice extra e/o query di database. Una soluzione migliore sarebbe quella di restituire '0' come' hashCode() 'e lasciare' equals() 'così com'è, risultando come un elenco in cui i valori uguali non vengono sostituiti all'inserimento. Tuttavia la soluzione suggerita da greyfairer è migliore ... – roesslerj

+0

Abbastanza giusto. Ti preghiamo di notare che non devi calcolare hashCode() ed equals() su campi che non sono effettivamente definitivi. Inoltre non penso che dovresti includere il composto quando calcoli hashCode() o equals() per le parti, dato che questo è solo il riferimento genitore, non qualcosa da cui sono create le parti. – rmaruszewski

+0

È vero che le parti non sono costruite da componenti, tuttavia potrebbe accadere che due parti con gli stessi attributi appartengano a due componenti diversi, in cui la componente è l'unico modo per distinguerli. Nella mia situazione (che è abbastanza comune, credo), le parti non sono uniche rispetto all '"universo", ma solo rispetto al "loro mondo" che è il composto. – roesslerj

Problemi correlati