Qual è il modo corretto di ricollegare oggetti staccati in Hibernate?


186

Ho una situazione in cui ho bisogno di ricollegare oggetti staccati a una sessione di ibernazione, anche se nella sessione può già esistere un oggetto con la stessa identità, che causerà errori.

In questo momento, posso fare una delle due cose.

  1. getHibernateTemplate().update( obj ) Funziona se e solo se un oggetto non esiste già nella sessione di ibernazione. Vengono generate eccezioni affermando che un oggetto con l'identificatore specificato esiste già nella sessione quando ne avrò bisogno in seguito.

  2. getHibernateTemplate().merge( obj ) Funziona se e solo se esiste un oggetto nella sessione di ibernazione. Le eccezioni vengono generate quando ho bisogno che l'oggetto sia in una sessione in seguito se lo uso.

Dati questi due scenari, come posso collegare genericamente sessioni agli oggetti? Non voglio usare eccezioni per controllare il flusso della soluzione di questo problema, in quanto deve esserci una soluzione più elegante ...

Risposte:


181

Quindi sembra che non ci sia modo di ricollegare un'entità staccata non aggiornata in JPA.

merge() invierà lo stato non aggiornato al DB e sovrascriverà eventuali aggiornamenti intermedi.

refresh() non può essere chiamato su un'entità staccata.

lock() non può essere chiamato su un'entità staccata, e anche se potesse, e ha ricollegato l'entità, chiamando 'lock' con l'argomento 'LockMode.NONE' che implica che stai bloccando, ma non bloccando, è il pezzo più controintuitivo del design dell'API Che abbia mai visto.

Quindi sei bloccato. C'è un detach()metodo, ma no attach()o reattach(). Un passaggio ovvio nel ciclo di vita dell'oggetto non è disponibile.

A giudicare dal numero di domande simili sull'APP, sembra che anche se l'APP afferma di avere un modello coerente, sicuramente non corrisponde al modello mentale della maggior parte dei programmatori, che sono stati maledetti per sprecare molte ore cercando di capire come ottenere JPA per fare le cose più semplici e finire con il codice di gestione della cache in tutte le loro applicazioni.

Sembra che l'unico modo per farlo sia scartare l'entità staccata stantia e fare una query di ricerca con lo stesso ID, che colpirà L2 o il DB.

Mik


1
Mi chiedo se c'è un motivo che la specifica JPA non consente refresh()alle entità distaccate? Guardando attraverso le specifiche 2.0 non vedo alcuna giustificazione; solo che non è permesso.
FGreg

11
Questo NON è assolutamente preciso. Da JPwH: *Reattaching a modified detached instance* A detached instance may be reattached to a new Session (and managed by this new persistence context) by calling update() on the detached object. In our experience, it may be easier for you to understand the following code if you rename the update() method in your mind to reattach()—however, there is a good reason it’s called updating.Altre informazioni sono disponibili nella sezione 9.3.2
cwash del

Gli oggetti persistenti funzionano alla grande, il flag sporco è impostato in base al delta tra il carico iniziale e i valori al momento del flush (). Gli oggetti distaccati sono necessari e al momento non dispongono di questa funzionalità. Il modo per farlo ibernare è quello di aggiungere un hash / id aggiuntivo per gli oggetti staccati. E mantieni un'istantanea dell'ultimo stato dell'oggetto staccato disponibile, proprio come fanno per gli oggetti persistenti. In questo modo possono sfruttare tutto quel codice esistente e farlo funzionare per oggetti separati. In questo modo, come notato da @mikhailfranco, non "sposteremo lo stato non aggiornato al DB e sovrascriveremo eventuali aggiornamenti"
tom

2
Secondo Hibernate javadoc (ma non JPA), lock(LockMode.NONE)in effetti può essere chiamato su un oggetto transitorio e ricollega l'entità alla sessione. Vedere stackoverflow.com/a/3683370/14379
seanf

lock non ha funzionato per me: java.lang.IllegalArgumentException: entità non nel contesto di persistenza su org.hibernate.internal.SessionImpl.lock (SessionImpl.java:3491) su org.hibernate.internal.SessionImpl.lock (SessionImpl. java: 3482) su com.github.vok.framework.DisableTransactionControlEMDelegate.lock (DB.kt)
Martin Vysny

32

Tutte queste risposte mancano di un'importante distinzione. update () viene utilizzato per (ri) collegare il grafico dell'oggetto a una sessione. Gli oggetti che gli passano sono quelli che sono stati gestiti.

merge () non è in realtà un'API di (ri) allegato. Avviso merge () ha un valore di ritorno? Questo perché ti restituisce il grafico gestito, che potrebbe non essere il grafico che hai passato. merge () è un'API JPA e il suo comportamento è regolato dalle specifiche JPA. Se l'oggetto che passi a merge () è già gestito (già associato alla Sessione), allora è il grafico con cui funziona Hibernate; l'oggetto passato è lo stesso oggetto restituito da merge (). Se, tuttavia, l'oggetto che passi in merge () è staccato, Hibernate crea un nuovo grafico a oggetti gestito e copia lo stato dal tuo grafico staccato sul nuovo grafico gestito. Ancora una volta, tutto ciò è dettato e regolato dalle specifiche dell'APP.

In termini di una strategia generica per "assicurarsi che questa entità sia gestita, o renderla gestita", dipende dal fatto che si desideri tenere conto anche di dati non ancora inseriti. Supponendo di fare, usa qualcosa di simile

if ( session.contains( myEntity ) ) {
    // nothing to do... myEntity is already associated with the session
}
else {
    session.saveOrUpdate( myEntity );
}

Si noti che ho usato saveOrUpdate () anziché update (). Se non si desidera gestire qui i dati non ancora inseriti, utilizzare invece update () ...


3
Questa è la risposta giusta a questa domanda - caso chiuso!
Cwash

2
Session.contains(Object)controlli per riferimento. Se esiste già un'altra Entità che rappresenta la stessa riga nella sessione e si passa un'istanza separata, si otterrà un'eccezione.
djmj,

Come Session.contains(Object)controllo per riferimento, se esiste un'altra Entità che rappresenta la stessa riga nella sessione, restituirà false e lo aggiornerà.
AxelWass,

19

Risposta non diplomatica: probabilmente stai cercando un contesto di persistenza esteso. Questo è uno dei motivi principali alla base di Seam Framework ... Se stai lottando per usare Hibernate in primavera in particolare, dai un'occhiata a questo documento di Seam.

Risposta diplomatica: questo è descritto nei documenti di Hibernate . Se hai bisogno di ulteriori chiarimenti, dai un'occhiata alla Sezione 9.3.2 di Java Persistence with Hibernate chiamata "Lavorare con oggetti distaccati". Consiglio vivamente di ottenere questo libro se stai facendo qualcosa di più di CRUD con Hibernate.


5
Da seamframework.org : "Lo sviluppo attivo di Seam 3 è stato interrotto da Red Hat." Anche il collegamento "questo pezzo di documenti di Seam" è morto.
badbishop

14

Se sei sicuro che la tua entità non è stata modificata (o se accetti che qualsiasi modifica andrà persa), puoi ricollegarla alla sessione con lock.

session.lock(entity, LockMode.NONE);

Non bloccherà nulla, ma otterrà l'entità dalla cache della sessione o (se non la trova lì) la leggerà dal DB.

È molto utile prevenire LazyInitException quando si navigano le relazioni da entità "vecchie" (ad esempio da HttpSession). Prima devi "ricollegare" l'entità.

Anche l'utilizzo di get può funzionare, tranne quando si ottiene l'ereditarietà mappata (che genererà già un'eccezione su getId ()).

entity = session.get(entity.getClass(), entity.getId());

2
Vorrei associare nuovamente un'entità alla sessione. Sfortunatamente, Session.lock(entity, LockMode.NONE)fallisce con l'eccezione che dice: non è possibile riassociare la raccolta transitoria non inizializzata. Come può superare questo?
dma_k,

1
In effetti non avevo completamente ragione. L'uso di lock () ricollega la tua entità, ma non le altre entità associate ad essa. Quindi, se si esegue entity.getOtherEntity (). GetYetAnotherEntity (), si potrebbe avere un'eccezione LazyInit. L'unico modo che conosco per superare è usare find. entity = em.find (entity.getClass (), entity.getId ();
John Rizzo

Non esiste un Session.find()metodo API. Forse vuoi dire Session.load(Object object, Serializable id).
dma_k,

11

Poiché questa è una domanda molto comune, ho scritto questo articolo , su cui si basa questa risposta.

Stati delle entità

L'APP definisce i seguenti stati di entità:

Nuovo (Transitorio)

Un oggetto appena creato che non è mai stato associato a un ibernazione Session(aka Persistence Context) e non è mappato su nessuna riga della tabella del database è considerato nello stato Nuovo (Transitorio).

Per perseverare, è necessario chiamare esplicitamente il EntityManager#persistmetodo o utilizzare il meccanismo di persistenza transitiva.

Persistente (gestito)

Un'entità persistente è stata associata a una riga della tabella del database ed è gestita dal contesto di persistenza attualmente in esecuzione. Qualsiasi modifica apportata a tale entità verrà rilevata e propagata al database (durante il flush-time della sessione).

Con Hibernate, non è più necessario eseguire le istruzioni INSERT / UPDATE / DELETE. Hibernate utilizza uno stile di lavoro transattivo write-behind e le modifiche sono sincronizzate all'ultimo momento responsabile, durante l'attuale Sessionflush-time.

Distaccato

Una volta chiuso il contesto di persistenza attualmente in esecuzione, tutte le entità gestite in precedenza vengono staccate. Le modifiche successive non verranno più monitorate e non verrà eseguita alcuna sincronizzazione automatica del database.

Transizioni dello stato delle entità

È possibile modificare lo stato dell'entità utilizzando vari metodi definiti EntityManagerdall'interfaccia.

Per comprendere meglio le transizioni di stato dell'entità JPA, considerare il diagramma seguente:

Transizioni di stato dell'entità JPA

Quando si utilizza JPA, per riassociare un'entità staccata a un'attiva EntityManager, è possibile utilizzare l' operazione di unione .

Quando si utilizza l'API Hibernate nativa, a parte merge, è possibile ricollegare un'entità staccata a una Sessione di ibernazione attiva utilizzando i metodi di aggiornamento, come dimostrato dal diagramma seguente:

Sospensione delle transizioni di stato delle entità

Unione di un'entità distaccata

L'unione sta per copiare lo stato di entità staccata (origine) in un'istanza di entità gestita (destinazione).

Considera che abbiamo persistito la seguente Bookentità, e ora l'entità è staccata come EntityManagerquella usata per persistere l'entità è stata chiusa:

Book _book = doInJPA(entityManager -> {
    Book book = new Book()
    .setIsbn("978-9730228236")
    .setTitle("High-Performance Java Persistence")
    .setAuthor("Vlad Mihalcea");

    entityManager.persist(book);

    return book;
});

Mentre l'entità è nello stato distaccato, la modifichiamo come segue:

_book.setTitle(
    "High-Performance Java Persistence, 2nd edition"
);

Ora, vogliamo propagare le modifiche al database, in modo da poter chiamare il mergemetodo:

doInJPA(entityManager -> {
    Book book = entityManager.merge(_book);

    LOGGER.info("Merging the Book entity");

    assertFalse(book == _book);
});

E Hibernate eseguirà le seguenti istruzioni SQL:

SELECT
    b.id,
    b.author AS author2_0_,
    b.isbn AS isbn3_0_,
    b.title AS title4_0_
FROM
    book b
WHERE
    b.id = 1

-- Merging the Book entity

UPDATE
    book
SET
    author = 'Vlad Mihalcea',
    isbn = '978-9730228236',
    title = 'High-Performance Java Persistence, 2nd edition'
WHERE
    id = 1

Se l'entità risultante dalla fusione non ha equivalenti nella corrente EntityManager, verrà recuperata una nuova istantanea dell'entità dal database.

Una volta che esiste un'entità gestita, JPA copia lo stato dell'entità staccata su quella attualmente gestita e durante il contesto di persistenzaflush , verrà generato un AGGIORNAMENTO se il meccanismo di controllo sporco rileva che l'entità gestita è cambiata.

Pertanto, quando si utilizza merge, l'istanza dell'oggetto disconnesso continuerà a rimanere staccata anche dopo l'operazione di unione.

Ricollegare un'entità staccata

Ibernazione, ma non JPA supporta il ricollegamento tramite il updatemetodo.

Un Hibernate Sessionpuò associare un solo oggetto entità per una determinata riga del database. Questo perché il contesto di persistenza funge da cache in memoria (cache di primo livello) e solo un valore (entità) è associato a una determinata chiave (tipo di entità e identificativo del database).

Un'entità può essere ricollegata solo se non esiste nessun altro oggetto JVM (corrispondente alla stessa riga del database) già associato all'ibernazione corrente Session.

Considerando che abbiamo persistito l' Bookentità e che l'abbiamo modificata quando l' Bookentità era nello stato distaccato:

Book _book = doInJPA(entityManager -> {
    Book book = new Book()
    .setIsbn("978-9730228236")
    .setTitle("High-Performance Java Persistence")
    .setAuthor("Vlad Mihalcea");

    entityManager.persist(book);

    return book;
});

_book.setTitle(
    "High-Performance Java Persistence, 2nd edition"
);

Possiamo ricollegare l'entità staccata in questo modo:

doInJPA(entityManager -> {
    Session session = entityManager.unwrap(Session.class);

    session.update(_book);

    LOGGER.info("Updating the Book entity");
});

E Hibernate eseguirà la seguente istruzione SQL:

-- Updating the Book entity

UPDATE
    book
SET
    author = 'Vlad Mihalcea',
    isbn = '978-9730228236',
    title = 'High-Performance Java Persistence, 2nd edition'
WHERE
    id = 1

Il updatemetodo richiede per unwrapla EntityManagera un Hibernate Session.

Diversamente merge, l'entità staccata fornita verrà riassociata al contesto di persistenza corrente e durante l'aggiornamento viene pianificato un AGGIORNAMENTO indipendentemente dal fatto che l'entità abbia modificato o meno.

Per evitare ciò, è possibile utilizzare l' @SelectBeforeUpdateannotazione Hibernate che attiverà un'istruzione SELECT che ha recuperato lo stato caricato che viene quindi utilizzato dal meccanismo di controllo sporco.

@Entity(name = "Book")
@Table(name = "book")
@SelectBeforeUpdate
public class Book {

    //Code omitted for brevity
}

Fai attenzione a NonUniqueObjectException

Un problema che può verificarsi updateè se il contesto di persistenza contiene già un riferimento di entità con lo stesso ID e dello stesso tipo dell'esempio seguente:

Book _book = doInJPA(entityManager -> {
    Book book = new Book()
    .setIsbn("978-9730228236")
    .setTitle("High-Performance Java Persistence")
    .setAuthor("Vlad Mihalcea");

    Session session = entityManager.unwrap(Session.class);
    session.saveOrUpdate(book);

    return book;
});

_book.setTitle(
    "High-Performance Java Persistence, 2nd edition"
);

try {
    doInJPA(entityManager -> {
        Book book = entityManager.find(
            Book.class,
            _book.getId()
        );

        Session session = entityManager.unwrap(Session.class);
        session.saveOrUpdate(_book);
    });
} catch (NonUniqueObjectException e) {
    LOGGER.error(
        "The Persistence Context cannot hold " +
        "two representations of the same entity",
        e
    );
}

Ora, quando si esegue il test case sopra, Hibernate lancerà un NonUniqueObjectExceptionperché il secondo EntityManagercontiene già Bookun'entità con lo stesso identificativo di quello a cui passiamo updatee il contesto di persistenza non può contenere due rappresentazioni della stessa entità.

org.hibernate.NonUniqueObjectException:
    A different object with the same identifier value was already associated with the session : [com.vladmihalcea.book.hpjp.hibernate.pc.Book#1]
    at org.hibernate.engine.internal.StatefulPersistenceContext.checkUniqueness(StatefulPersistenceContext.java:651)
    at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.performUpdate(DefaultSaveOrUpdateEventListener.java:284)
    at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.entityIsDetached(DefaultSaveOrUpdateEventListener.java:227)
    at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.performSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:92)
    at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.onSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:73)
    at org.hibernate.internal.SessionImpl.fireSaveOrUpdate(SessionImpl.java:682)
    at org.hibernate.internal.SessionImpl.saveOrUpdate(SessionImpl.java:674)

Conclusione

Il mergemetodo è preferibile se si utilizza il blocco ottimistico in quanto consente di evitare aggiornamenti persi. Per maggiori dettagli su questo argomento, consulta questo articolo .

Il updateè buono per aggiornamenti batch quanto esso può evitare SELECT aggiuntivo generato dal mergefunzionamento, riducendo quindi il tempo di aggiornamento di esecuzione batch.


Bella risposta. Mi stavo chiedendo @SelectBeforeUpdatedell'annotazione però. Quando viene attivata la selezione? Al momento della chiamata update, prima di svuotare o non importa (potrebbe importare se l'ibernazione ha recuperato tutte le entità annotate in una chiamata prima di svuotare)?
Andronico,

Il @SelectBeforeUpdateinnesca SELECT durante il contesto di persistenza flushoperazione. Controlla il getDatabaseSnapshotmetodo inDefaultFlushEntityEventListener per maggiori dettagli.
Vlad Mihalcea,

10

Sono tornato a JavaDoc per org.hibernate.Sessione ho trovato quanto segue:

Le istanze temporanee possono essere rese persistenti chiamando save(), persist()o saveOrUpdate(). Le istanze persistenti possono essere rese transitorie chiamando delete(). Qualsiasi istanza restituita da un metodo get()o load()è persistente. Istanze indipendenti possono essere effettuate persistenti chiamando update(), saveOrUpdate(), lock()o replicate(). Lo stato di un'istanza temporanea o distaccata può anche essere reso persistente come nuova istanza persistente chiamando merge().

Così update(), saveOrUpdate(), lock(), replicate()e merge()sono le opzioni candidati.

update(): Genererà un'eccezione se esiste un'istanza persistente con lo stesso identificatore.

saveOrUpdate(): Salva o aggiorna

lock(): Obsoleto

replicate(): Persiste lo stato dell'istanza staccata fornita, riutilizzando il valore dell'identificatore corrente.

merge(): Restituisce un oggetto persistente con lo stesso identificatore. L'istanza fornita non viene associata alla sessione.

Pertanto, lock()non deve essere utilizzato immediatamente e in base al requisito funzionale è possibile sceglierne uno o più.


7

L'ho fatto così in C # con NHibernate, ma dovrebbe funzionare allo stesso modo in Java:

public virtual void Attach()
{
    if (!HibernateSessionManager.Instance.GetSession().Contains(this))
    {
        ISession session = HibernateSessionManager.Instance.GetSession();
        using (ITransaction t = session.BeginTransaction())
        {
            session.Lock(this, NHibernate.LockMode.None);
            t.Commit();
        }
    }
}

First Lock è stato chiamato su ogni oggetto perché Contains era sempre falso. Il problema è che NHibernate confronta gli oggetti per ID e tipo di database. Contiene utilizza il equalsmetodo, che confronta per riferimento se non viene sovrascritto. Con quel equalsmetodo funziona senza eccezioni:

public override bool Equals(object obj)
{
    if (this == obj) { 
        return true;
    } 
    if (GetType() != obj.GetType()) {
        return false;
    }
    if (Id != ((BaseObject)obj).Id)
    {
        return false;
    }
    return true;
}

4

Session.contains(Object obj) controlla il riferimento e non rileverà un'istanza diversa che rappresenta la stessa riga ed è già collegata ad essa.

Qui la mia soluzione generica per Entità con una proprietà identificatore.

public static void update(final Session session, final Object entity)
{
    // if the given instance is in session, nothing to do
    if (session.contains(entity))
        return;

    // check if there is already a different attached instance representing the same row
    final ClassMetadata classMetadata = session.getSessionFactory().getClassMetadata(entity.getClass());
    final Serializable identifier = classMetadata.getIdentifier(entity, (SessionImplementor) session);

    final Object sessionEntity = session.load(entity.getClass(), identifier);
    // override changes, last call to update wins
    if (sessionEntity != null)
        session.evict(sessionEntity);
    session.update(entity);
}

Questo è uno dei pochi aspetti di .Net EntityFramework che mi piace, le diverse opzioni di collegamento relative alle entità modificate e alle loro proprietà.


3

Ho trovato una soluzione per "aggiornare" un oggetto dall'archivio di persistenza che terrà conto di altri oggetti che potrebbero già essere collegati alla sessione:

public void refreshDetached(T entity, Long id)
{
    // Check for any OTHER instances already attached to the session since
    // refresh will not work if there are any.
    T attached = (T) session.load(getPersistentClass(), id);
    if (attached != entity)
    {
        session.evict(attached);
        session.lock(entity, LockMode.NONE);
    }
    session.refresh(entity);
}

2

Spiacenti, non riesci ad aggiungere commenti (ancora?).

Utilizzo di Hibernate 3.5.0-Final

Considerando che il Session#lockmetodo di questo deprecato, il javadoc non suggerisce usando Session#buildLockRequest(LockOptions)#lock(entity)e se si assicurarsi che le associazioni hanno cascade=lock, i pigri di carico non è un problema neanche.

Quindi, il mio metodo di collegamento sembra un po 'simile

MyEntity attach(MyEntity entity) {
    if(getSession().contains(entity)) return entity;
    getSession().buildLockRequest(LockOptions.NONE).lock(entity);
    return entity;

I test iniziali suggeriscono che funziona a meraviglia.


2

Forse si comporta in modo leggermente diverso su Eclipselink. Per ricollegare oggetti staccati senza ottenere dati non aggiornati, di solito faccio:

Object obj = em.find(obj.getClass(), id);

e come optional un secondo passaggio (per invalidare la cache):

em.refresh(obj)

1

provare getHibernateTemplate (). replicate (entità, ReplicationMode.LATEST_VERSION)


1

Nel post originale, ci sono due metodi, update(obj)e merge(obj)che sono menzionati al lavoro, ma succede il contrario. Se questo è veramente vero, allora perché non testare per vedere se l'oggetto è già prima nella sessione, quindi chiamare update(obj)se lo è, altrimenti chiamare merge(obj).

Il test per l'esistenza nella sessione è session.contains(obj). Pertanto, penso che il seguente pseudo-codice funzionerebbe:

if (session.contains(obj))
{
    session.update(obj);
}
else 
{
    session.merge(obj);
}

2
contiene () verifica i confronti per riferimento, ma le funzioni di ibernazione funzionano in base all'ID del database. session.merge non verrà mai chiamato nel tuo codice.
Verena Haunschmid,

1

per ricollegare questo oggetto, è necessario utilizzare merge ();

questo metodo accetta nel parametro la tua entità distaccata e restituisce un'entità verrà collegata e ricaricata dal database.

Example :
    Lot objAttach = em.merge(oldObjDetached);
    objAttach.setEtat(...);
    em.persist(objAttach);

0

chiamando prima merge () (per aggiornare l'istanza persistente), quindi lock (LockMode.NONE) (per collegare l'istanza corrente, non quella restituita da merge ()) sembra funzionare per alcuni casi d'uso.


0

La proprietà ha hibernate.allow_refresh_detached_entityfatto il trucco per me. Ma è una regola generale, quindi non è molto adatto se si desidera farlo solo in alcuni casi. Spero possa essere d'aiuto.

Testato su Hibernate 5.4.9

SessionFactoryOptionsBuilder



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.