2015-11-30 9 views
20

Esecuzione di un esempio molto semplice di relazione uno-a-molti (stato -> Paese).Impedisci l'ibernazione dall'eliminazione di entità orfane durante la fusione di un'entità con associazioni di entità con orphanRemoval impostato su true

Paese (lato inverso):

@OneToMany(mappedBy = "country", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) 
private List<StateTable> stateTableList=new ArrayList<StateTable>(0); 

StateTable (lato possedere):

@JoinColumn(name = "country_id", referencedColumnName = "country_id") 
@ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH, CascadeType.DETACH}) 
private Country country; 

Il metodo si tenta di aggiornare una dotazione (monofamiliare) StateTable entità all'interno di una transazione di database attivo (JTA o risorsa locale):

public StateTable update(StateTable stateTable) { 

    // Getting the original state entity from the database. 
    StateTable oldState = entityManager.find(StateTable.class, stateTable.getStateId()); 
    // Get hold of the original country (with countryId = 67, for example). 
    Country oldCountry = oldState.getCountry(); 
    // Getting a new country entity (with countryId = 68) supplied by the client application which is responsible for modifying the StateTable entity. 
    // Country has been changed from 67 to 68 in the StateTable entity using for example, a drop-down list. 
    Country newCountry = entityManager.find(Country.class, stateTable.getCountry().getCountryId()); 
    // Attaching a managed instance to StateTable. 
    stateTable.setCountry(newCountry); 

    // Check whether the supplied country and the original country entities are equal. 
    // (Both not null and not equal - http://stackoverflow.com/a/31761967/1391249) 
    if (ObjectUtils.notEquals(newCountry, oldCountry)) { 
     // Remove the state entity from the inverse collection held by the original country entity. 
     oldCountry.remove(oldState); 
     // Add the state entity to the inverse collection held by the newly supplied country entity 
     newCountry.add(stateTable); 
    } 

    return entityManager.merge(stateTable); 
} 

Va notato che orphanRemoval è impostato su true. L'entità StateTable viene fornita da un'applicazione client che è interessata a modificare l'associazione di entità Country (countryId = 67) in StateTable in qualcos'altro (countryId = 68) (quindi sul lato opposto in JPA, migrando un'entità figlio dalla sua genitrice (raccolta) a un'altra genitore (raccolta) che a sua volta orphanRemoval=true si oppone).

Il provider di Hibernate emette un'istruzione DML DELETE che provoca la rimozione della riga corrispondente all'entità StateTable dalla tabella del database sottostante.

Nonostante il fatto che orphanRemoval è impostato su true, mi aspetto Hibernate ad emettere un regolare UPDATE DML dichiarazione provocando l'effetto di orphanRemoval essere sospesa nella sua interezza perché il collegamento rapporto è migrato (non eliminati semplicemente).

EclipseLink fa esattamente questo lavoro. Emette un'istruzione UPDATE nello scenario indicato (con la stessa relazione con orphanRemoval impostata su true).

Quale si sta comportando in base alle specifiche? È possibile rendere il problema di Hibernate un'istruzione UPDATE in questo caso diversa dalla rimozione di orphanRemoval dal lato inverso?


Questo è solo un tentativo di fare una relazione bidirezionale più coerente su entrambi i lati.

Le modalità di gestione collegamento difensive cioè add() e remove() utilizzato nel frammento di sopra, se necessario, sono definiti nell'entità Country come segue.

public void add(StateTable stateTable) { 
    List<StateTable> newStateTableList = getStateTableList(); 

    if (!newStateTableList.contains(stateTable)) { 
     newStateTableList.add(stateTable); 
    } 

    if (stateTable.getCountry() != this) { 
     stateTable.setCountry(this); 
    } 
} 

public void remove(StateTable stateTable) { 
    List<StateTable> newStateTableList = getStateTableList(); 

    if (newStateTableList.contains(stateTable)) { 
     newStateTableList.remove(stateTable); 
    } 
} 


Aggiornamento:

Hibernate può solo rilasciare un UPDATE DML dichiarazione previsto, se il codice data si modifica nel modo seguente.

public StateTable update(StateTable stateTable) { 
    StateTable oldState = entityManager.find(StateTable.class, stateTable.getStateId()); 
    Country oldCountry = oldState.getCountry(); 
    // DELETE is issued, if getReference() is replaced by find(). 
    Country newCountry = entityManager.getReference(Country.class, stateTable.getCountry().getCountryId()); 

    // The following line is never expected as Country is already retrieved 
    // and assigned to oldCountry above. 
    // Thus, oldState.getCountry() is no longer an uninitialized proxy. 
    oldState.getCountry().hashCode(); // DELETE is issued, if removed. 
    stateTable.setCountry(newCountry); 

    if (ObjectUtils.notEquals(newCountry, oldCountry)) { 
     oldCountry.remove(oldState); 
     newCountry.add(stateTable); 
    } 

    return entityManager.merge(stateTable); 
} 

Osservare le seguenti due righe nella versione più recente del codice.

// Previously it was EntityManager#find() 
Country newCountry = entityManager.getReference(Country.class, stateTable.getCountry().getCountryId()); 
// Previously it was absent. 
oldState.getCountry().hashCode(); 

Se sia l'ultima riga è assente o EntityManager#getReference() è sostituito dal EntityManager#find(), poi una dichiarazione DELETE DML è inaspettatamente rilasciato.

Quindi, cosa sta succedendo qui? Soprattutto, sottolineo la portabilità. Non trasferire questo tipo di funzionalità di base di base tra diversi provider JPA impedisce gravemente l'utilizzo dei framework ORM.

Capisco la differenza di base tra EntityManager#getReference() e EntityManager#find().

+0

Solo un pensiero ma fa alcuna differenza se si fa l'add prima della rimozione? –

+0

Cambiare l'ordine di quelle due linee non fa alcuna differenza. Ho verificato in anticipo tali possibilità :) – Tiny

+0

Sembra un bug Hibernate. Solo un'ipotesi, fondere esplicitamente il nuovo paese e quindi il vecchio paese prima dello stato. Se necessario, richiamare il colore intermedio. – BalusC

risposta

9

In primo luogo, cerchiamo di modificare il codice originale per una forma più semplice:

StateTable oldState = entityManager.find(StateTable.class, stateTable.getStateId()); 
Country oldCountry = oldState.getCountry(); 
oldState.getCountry().hashCode(); // DELETE is issued, if removed. 

Country newCountry = entityManager.find(Country.class, stateTable.getCountry().getCountryId()); 
stateTable.setCountry(newCountry); 

if (ObjectUtils.notEquals(newCountry, oldCountry)) { 
    oldCountry.remove(oldState); 
    newCountry.add(stateTable); 
} 

entityManager.merge(stateTable); 

Si noti che ho aggiunto solo oldState.getCountry().hashCode() nella terza riga. Ora puoi riprodurre il problema rimuovendo solo questa linea.

Prima di spiegare cosa sta succedendo qui, in primo luogo alcuni estratti dallo JPA 2.1 specification.

sezione 3.2.4 :

La semantica dell'operazione filo, applicato ad un'entità X sono come segue:

  • Se X è un'entità gestita, è sincronizzato al database.
    • Per tutte le entità Y riferimento un rapporto da X, se il rapporto di Y è stato annotato con il valore dell'elemento cascade cascade = persistono o cascade = ALL, l'operazione persistere viene applicata a Y

Sezione 3.2.2:

La semantica dell'operazione persistono, applicato ad un'entità X sono come segue:

  • Se X è un'entità rimosso, esso diventa gestito.

orphanRemoval JPA javadoc:

(opzionale) se si deve applicare l'operazione di rimozione a soggetti che sono stati rimossi dal rapporto e a cascata l'operazione di rimozione a quelle entità.

Come possiamo vedere, orphanRemoval è definito in termini di remove funzionamento, quindi tutte le regole che si applicano per removedeve richiedere orphanRemoval pure.

In secondo luogo, come spiegato in this answer, l'ordine degli aggiornamenti eseguiti da Hibernate è l'ordine in cui le entità vengono caricate nel contesto di persistenza. Per essere più precisi, aggiornare un'entità significa sincronizzare lo stato corrente (controllo sporco) con il database e collegare in cascata l'operazione PERSIST alle sue associazioni.

Ora, questo è ciò che sta accadendo nel tuo caso. Alla fine della transazione, Hibernate sincronizza il contesto di persistenza con il database. Abbiamo due scenari:

  1. Quando è presente la linea extra (hashCode):

    1. Hibernate sincronizza oldCountry con DB. Lo fa prima di gestire newCountry, perché oldCountry è stato caricato per primo (inizializzazione del proxy forzata chiamando hashCode).
    2. Hibernate vede che un'istanza StateTable è stata rimossa dalla collezione oldCountry, contrassegnando così l'istanza StateTable come rimossa.
    3. Hibernate sincronizza newCountry con il DB. L'operazione PERSIST passa a cascata allo stateTableList che ora contiene l'istanza dell'entità StateTable rimossa.
    4. L'istanza StateTable rimossa è ora gestita nuovamente (sezione 3.2.2 delle specifiche JPA citate sopra).
  2. Quando la linea extra (hashCode) è assente:

    1. Hibernate sincronizza newCountry con DB. Lo fa prima di gestire oldCountry, perché newCountry è stato caricato per primo (con entityManager.find).
    2. Hibernate sincronizza oldCountry con il DB.
    3. Hibernate vede che un'istanza StateTable è stata rimossa dalla collezione oldCountry, contrassegnando così l'istanza StateTable come rimossa.
    4. La rimozione dell'istanza StateTable è sincronizzata con il database.

L'ordine degli aggiornamenti spiega anche le tue scoperte in cui è praticamente costretti oldCountry inizializzazione del proxy per accadere prima di caricare newCountry dal DB.

Quindi, questo è conforme alle specifiche JPA? Ovviamente sì, nessuna regola delle specifiche JPA è stata interrotta.

Perché non è portatile?

La specifica JPA (come qualsiasi altra specifica dopo tutto) offre ai provider la libertà di definire molti dettagli non coperti dalle specifiche.

Inoltre, ciò dipende dalla vostra visione della "portabilità". La funzionalità e qualsiasi altra funzionalità JPA sono portatili quando si tratta delle loro definizioni formali. Tuttavia, dipende da come li usi in combinazione con le specifiche del tuo provider JPA.

proposito, sezione 2.9 della specifica raccomanda (ma non definisce chiaramente) per la orphanRemoval:

applicazioni

mobili non devono tuttavia dipendere un ordine specifico di rimozione, e non deve riassegnare un'entità che è stata resa orfana a un'altra relazione o tentare in altro modo di mantenerla.

Ma questo è solo un esempio di raccomandazioni vaghe o non ben definite nelle specifiche, poiché la persistenza di entità rimosse è consentita da altre dichiarazioni nella specifica.

+0

Ora un po 'di più: facendo 'Country oldCountry = entityManager.find (Country.class, oldState.getCountry(). GetCountryId());' elimina la necessità di aggiungere questa riga 'oldState.getCountry(). HashCode(); 'ma per me, anche' EntityManager # find() 'non dovrebbe essere necessario.Questo 'Country oldCountry = oldState.getCountry();' dovrebbe recuperare un'entità pienamente qualificata mentre il metodo viene eseguito all'interno di una transazione JTA attiva. 'OldState.getCountry()' non è un proxy non inizializzata come restituito dal 'EntityManager # getReference()', che non possono essere caricati se non esplicitamente caricati facendo 'oldState.getCountry() hashCode();.' – Tiny

+0

Forse, potrebbe funzionare con '' ma non sono interessato a usarlo in quanto non può essere usato senza seria attenzione. – Tiny

+0

@Tiny 'oldState.getCountry()' restituisce un proxy non inizializzato (l'associazione è pigro). 'hibernate.enable_lazy_load_no_trans' non ha nulla a che fare con il tuo problema (non aiuterà né danneggia). –

3

Non appena l'entità di riferimento può essere utilizzata in altri genitori, si complica comunque. Per renderlo veramente pulito, l'ORM ha dovuto cercare nel database qualsiasi altro utilizzo dell'entità rimossa prima di eliminarlo (garbage collection persistente). Questo richiede molto tempo e quindi non è veramente utile e quindi non implementato in Hibernate.

Elimina orfani funziona solo se il bambino è utilizzato per un genitore single e non riutilizzato altrove. Potresti persino ottenere un'eccezione quando provi a riutilizzarlo per rilevare meglio l'uso improprio di questa funzione.

Decidere se si desidera mantenere l'eliminazione di orfani o meno. Se vuoi mantenerlo, devi creare un nuovo figlio per il nuovo genitore invece di spostarlo.

Se si abbandona l'eliminazione di orfani, è necessario eliminare i bambini non appena non vengono più referenziati.

+0

Dio! Questo è un problema di portabilità per questo tipo di funzionalità rudimentale. Dovrebbe essere chiaramente indicato nelle specifiche JPA. – Tiny

+0

Come ho cercato di sottolineare: non è così rudimentale non appena si inizia a pensare a possibili scenari in cui non tutto è in memoria. Ad ogni modo, c'è una possibilità che io abbia torto, perché lo so da NHibernate. Ho appena pensato che fosse lo stesso lì. –

Problemi correlati