Appunti avanzati di Hibernate

II parte: I metodi di sessionedi

Che cosa sono i metodi phe portano a variazione di stato degli oggetti? Nell‘articolo affrontiamo i principali metodi forniti dalla Sessione Hibernate (o EntityManager) che portano a variazione di stato cercando di capire quali sono i diversi comportamenti strutturali. Questo ci consentirà di non incorrere in errori tipici e di sfruttare al meglio Hibernate.

Come previsto, affrontiamo in questo articolo il fondamentale argomento degli stati di sessione. Riporteremo degli esempi i quali valgono comunque anche per JPA e EntityManager come "interfaccia" del Persistence Context.

Scope Object Identity

Come ben spiegato in "Java persistence with Hibernate" [1], partendo dai tipi di relazione di uguaglianza tra oggetti:

Java object identity: objA == objB
Database identity: objA.getId().equals(objB.getId)

si definisce "scope object identity" l'insieme delle condizioni, in un determinato contesto, che garantiscono sempre l'equivalenza delle due espressioni logiche. Hibernate garantisce lo "scope object identity" all'interno del persistence-context (quindi per gli oggetti in stato "Persistence") ovvero all'interno della sessione Hibernate può esistere solo e una sola instanza di una certa Classe che rappresenta lo stato di una particolare riga sul database; tale politica è definita in letteratura "persistence context-scoped identiy".

Partiamo subito con un esempio prendendo come domain model un sottoinsieme di quello sviluppato negli articoli della serie "Il programmatore e le sue API" di Giovanni Puliti e Alfredo Larotonda, pubblicati già da tempo su MokaByte, e riportiamo nelle figure 1 e 2 la rappresentazione delle classi e il contenuto dei relativi file di mapping.

    Integer idRegina =  new Integer(1);      
    
    Session session1 = hibernateUtil.getSession();
    Transaction transaction1 = session1.beginTransaction();
    
    //Viene caricato il record dalla tabella REGINA con ID = 1
    Regina reginaA = (Regina) session1.get(Regina.class, new Integer(idRegina));
    Regina reginaB = (Regina) session1.get(Regina.class, new Integer(idRegina));
              reginaA == reginaB; //TRUE - reginaA e ReginaB puntano alla stessa 
                                  //istanza di tipo REGINA

              transaction1.commit();
              session1.close();

              Session session2 = hibernateUtil.getSession();
              Transaction transaction2 = session2.beginTransaction();

              Regina reginaC = (Regina) session2.get(Regina.class, new Integer(idRegina));
              reginaC == reginaB; //FALSE - reginaC e reginaB puntano ad 
                                  //istanze di tipo REGINA diverse
              reginaC.getId().equals(reginaB.getId);//TRUE - Entrambe le istanze puntate  
                                                    //da  reginaC e reginaB rappresentano 
                                                    //l'immagine del record 
                                                    //nella tabella REGINA con ID = 1
              transaction2.commit();
              session2.close();

Dall'esempio e dai test internamente effettuati si potrà subito notare come il principio del "persistence context-scoped identiy" viene rispettato all'interno del persistence context, della prima sessione aperta, tra gli oggetti reginaA e reginaB, mentre nel confronto tra gli oggetti reginaC e reginaB, rispettivamente nello stato Persistent e Detached, non si rispetta la"Java object identity";  al contrario, risulta vero il controllo sulla "Database identity" visto che le due istanze rappresentano lo stesso record del data base.
Il principio del "persistence context-scoped identiy" è anche la base della cache di primo livello; tornando infatti all'esempio, se si tracciassero le query di SELECT generate dai metodi get(...) per recuperare le informazioni degli stati di reginaA e reginaB, risulterebbe essere eseguita una sola query necessaria per creare l'instanza dell'oggetto di tipo Regina.  Alla seconda chiamata del metodo get(...) Hibernate verifica, nella working memory della sessione, l'esistenza di un'istanza che abbia la proprietà identificante uguale all'id cercato e, in caso affermativo, ne ritorna il riferimento evitando di accedere nuovamente al data base.


Figura 1 - La classe Regina

 

 


Figura 2 - Il Domain Model

Talvolta il principio del "persistence context-scoped identiy" può generare eccezioni di tipo org.hibernate.NonUniqueObjectException con messaggio d'errore associato "a different object with the same identifier value was already associated with the session...", inaspettato soprattutto per i neofiti di Hibernate. Di seguito riportiamo un esempio basato sul domain model di figura 2 che genera appunto un' eccezione org.hibernate.NonUniqueObjectException:

    ...................................................................
    //Nuovo Oggetto di tipo Apiario
    Apiario apiario = new Apiario(); //Oggetto in stato Transient
    apiario.setDimensione(1);
    //Seguono set(...) e creazione classi associate
    ...................................................................
    ...................................................................
    //Apertura Sessione Hibernate
    Session session = HibernateUtil.getSessionFactory().openSession();
    //Apertura Transazione
    Transaction transaction = session.beginTransaction();

    session.save(apiario); //Viene generata immediatamente una query di INSERT
                        //sulla tabella APIARIO per ottenere automaticamente
                        //la chiave primaria d'associare alla proprietà id. 
                        //L'oggetto passo allo stato Persistent

    //Commit Transazione
    transaction.commit();
    //Chiusura Sessione Hibernate - L'istanza di APIARIO passa da Peristent a Detached
    session.close();
 
    Integer idApiario = apiario.getId(); //Viene estrapolata la proprietà 
                                         //id dell'oggetto detached Apiario
    ...................................................................
    ...................................................................
    //Nuovo Oggetto di tipo FAMIGLIA
    Famiglia famiglia = new Famiglia();
    famiglia.setCodice("RED1-HONEY");
    //Seguono set() e creazione oggetti associati/
    ...................................................................
    ...................................................................
    //Viene associato l'oggetto apiario in stato Detached 
    //all'oggetto famiglia in stato Transient
    famiglia.setApiario(apiario);

    //Apertura nuova Sessione HIBERNATE
    Session session2 = HibernateUtil.getSessionFactory().openSession();
    Transaction transaction2 = session2.beginTransaction();
    
    //Viene caricato un instanza Persistent di Apiario tramite idApiario  
    Apiario  apiario2 = (Apiario) session.load(Apiario.class, idApiario);
    //Viene creata una nuova istanza di tipo Apiario
    //con id uguale a quella Detached referenziata da apiario

    //La relazione bi-direzionale APIARIO  FAMIGLIA impone per convenzione
    //HIBERNATE la valorizzazione dell'associazione FAMIGLIA --> APIARIO e
    //APIARIO --> FAMIGLIA

    apiario2.getFamiglie().add(famiglia);//Questa chiamata scatena il lancio dell'eccezione
                                        //ERRORE org.hibernate.NonUniqueObjectException
                                        //causato dall'istanza di tipo Apiario associata 
                                        //precedentementeall'oggetto Famiglia 
                                        //in quanto (apiario != apiario2) è uguale a FALSE

    Transaction2.commit();
    session2.close();
    ...................................................................

L'errore è generato dal passaggio di stato dell'oggetto famiglia da Transient a Persistent quando viene associato all'oggetto apiario2, in particolar modo l'eccezione scatta quando viene "trattato" il passaggio di stato dell'oggetto detached di tipo Apiario incapsulato in famiglia e associato con la chiamata famiglia.setApiario(apiario); la presenza dell'oggetto Persistent apiario2 con lo stesso id dell'istanza incapsulata in famiglia non soddisfa le condizioni della "persistence context-scoped identiy".

Una possibile soluzione, anche se "leggermente" contorta, potrebbe essere quella di rendere Persistent l'oggetto Detached apiario in modo tale che apiario2 e il puntatore incapsulato in famiglia facciano riferimento alla stessa istanza Persistent:

    ...................................................................
    //Apertura nuova Sessione HIBERNATE
    Session session2 = HibernateUtil.getSessionFactory().openSession();
    Transaction transaction2 = session2.beginTransaction(); 

    session.save(apiario);
    //Viene caricata u' instanza Persistent di Apiario tramite idApiario  
    Apiario  apiario2 = (Apiario) session.load(Apiario.class, idApiario);

    apiario2.getFamiglie().add(famiglia);//OK
    transaction2.commit();
    session2.close();
    ...................................................................

Infatti la chiamata session.load(Apiario.class, idApiario) ritorna la stessa istanza referenziata da apiario già presente nel persistence context.

Ovveride di metodi equals(...) e hashcode(...)

Spesso la scelta se implementare o meno l'ovveride dei metodi equals(...) e hashcode(...) è argomento di lunghe discussioni dovute a svariate problematiche riscontrabili, ad esempio, sul sito Hibernate [5]. Nel libro "Java persistence with Hibernate" [1] vengono dati ottimi spunti sull'argomento, focalizzandosi sui problemi dovuti a scelte sbagliate o non fatte. Partendo proprio dalla "bibbia" di Hibernate cerchiamo di venire al punto del problema.

Quando si confrontano, tramite il metodo equals(...), istanze di classi che non implementano l'ovveride dei metodi in questione per default la JVM utilizza la "Java object identity"; questo modo di procedere, insieme all'"Identity scope" utilizzato da Hibernate, sarabbe assolutamente sufficiente quando:

  • non si prevedono confronti tra oggetti in stato Detached;
  • non si prevede di associare nessun oggetto in stato Detached/Transient in strutture List, Map o Set dove è richiesto da "contratto", specificato nelle rispettive documentazioni, l'implementazione di equals(...)/hashCode(...).

Chiariamo subito con un esempio un possibile problema inerente all'inserimento di oggetti Transient in un Set prendendo ancora come spunto il domain model della figura 2, dove la proprietà famiglie della classe Apiario, derivata dall'aggregazione con la classe Famiglia, è di tipo java.util.HashSet:

    ...................................................................
    //Nuovo Oggetto di tipo Apiario
    Apiario apiario = new Apiario(); //Oggetto in stato Transient
    apiario.setDimensione(3);
    //Seguono set(...) e creazione classi associate
    ...................................................................
    ...................................................................
    //Apertura Sessione Hibernate
    Session session = HibernateUtil.getSessionFactory().openSession();
    //Apertura Transazione
    Transaction transaction = session.beginTransaction();
    session.save(apiario); //Viene generata subito query di INSERT sulla tabella APIARIO
                        //per generazione automatica della PK e valorizzazione della proprietà
                        //identificante id. L'oggetto passo allo stato Persistent

    //Commit Transazione
    transaction.commit();
    //Chiusura Sessione Hibernate - L'istanza di APIARIO passa da Peristent a Detached
    session.close();
    ...................................................................
    ...................................................................
    //Nuovo Oggetto di tipo FAMIGLIA
    Famiglia famiglia = new Famiglia();
    famiglia.setCodice("RED1-HONEY");
    //Seguono set() e creazione oggetti associati/
    ...................................................................
    ...................................................................
    //Viene associato l'oggetto apiario in stato Detached all'oggetto 
    //famiglia in stato Transient
    famiglia.setApiario(apiario);
    //Nuovo Oggetto di tipo FAMIGLIA
    Famiglia famiglia2 = new Famiglia();
    famiglia2.setCodice("BROWN1-HONEY");
   //Seguono set() e creazione oggetti associati/
    ...................................................................
    //Nuovo Oggetto di tipo FAMIGLIA - Duplicazione di stato
    //dell'istanza famiglia3 (esclusa naturalmente la proprietà id non valorizzata)
    Famiglia famiglia3 = new Famiglia();
    famiglia3.setCodice("BROWN1-HONEY");
    //Seguono set() e creazione oggetti associati/
    ...................................................................
    ...................................................................
    //Apertura nuova Sessione HIBERNATE
    Session session2 = HibernateUtil.getSessionFactory().openSession();
    Transaction transaction2 = session2.beginTransaction();
    apiario = (Apiario) session.load(Apiario.class, apiario.getId());
    //La relazione bi-direzionale APIARIO  FAMIGLIA impone per convenzione
    //HIBERNATE la valorizzazione dell'associazione FAMIGLIA --> APIARIO e
    //APIARIO --> FAMIGLIA
    famiglia.setApiario(apiario);
    famiglia2.setApiario(apiario);
    famiglia3.setApiario(apiario);
    apiario.getFamiglie().add(famiglia2);
    apiario.getFamiglie().add(famiglia);
    apiario.getFamiglie().add(famiglia3);
    transaction2.commit();
    session2.close();
    ...................................................................

Nella collection famiglie vengono inseriti in maniera non corretta tutti i 3 oggetti Transient di tipo Famiglia e l'operazione implicita di flush(), eseguita da Hibernate prima della transaction.commit(), porta al lancio dell'eccezione org.hibernate.exception.ConstraintViolationException a causa del constraint univoco sulla proprietà codice della classe Famiglia ( ); il problema nasce proprio dalla non implementazione dei metodi equals()/hashcode() nella classe di tipo Famiglia e l'inserimento nella collection HashSet è sempre ammesso perche' i tre puntatori si riferiscono a istanze diverse anche se lo stato di famiglia2 e famiglia3 è uguale.

Il primo errore che si potrebbe commettere cercando di risolvere questo problema potrebbe essere quello di implementare i metodi equals(...)/hashCode(...) basandosi sulla proprieta identificante id:

...................................................................
//Librerie Apache Commons Lang
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
public class Famiglia {
    ...................................................................
    ...................................................................
    @Override
    public int hashCode() {
        return new HashCodeBuilder(17, 37).append(getId()).toHashCode();
    }
    
    @Override
    public boolean equals(Object obj) {
        
        if (obj == null) return false;
        if (!(obj instanceof Famiglia))  return false;
        if (obj.getId() == null || obj.getId().intValue == 0);
        Famiglia reg = (Famiglia) obj;
        
        return new EqualsBuilder().append(getId(), reg.getId()).isEquals();
    }

Anche questa soluzione porta allo stesso identico errore in quanto le istanze Transient hanno la proprietà id uguale a null (oppure unsaved_value se considerato) e quindi, tornando all'esempio, i 3 oggetti vengono inseriti nella proprietà famiglie generando la stessa eccezione di univocità. D'altro canto, se ci limitassimo ad eseguire solo un controllo sull'id implementando la equals(...) della classe Famiglia in questo modo:

...................................................................
//Librerie Apache Commons Lang
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;

    public class Famiglia {
    ...................................................................
    ...................................................................
    @Override
    public int hashCode() {
        return new HashCodeBuilder(17, 37).append(getId()).toHashCode();
    }
    
    @Override
    public boolean equals(Object obj) {
    
        if (obj == null) return false;
        if (!(obj instanceof Famiglia))  return false;
        return getId().equals(reg.getId());
    }

si otterebbe l'effetto contrario, ovvero tutti e 3 gli oggetti verrebbero considerati "uguali" con la conseguenza che nella proprietà famiglie di Apiario verrebbe inserita una sola instanza.

Una possibile soluzione è quella di implementare le classi equals(...)/hashCode(...) sfruttando la Business Key codice della classe Famiglia:

...................................................................
//Librerie Apache Commons Lang
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
public class Famiglia {
    ...................................................................
    ...................................................................
    @Override
    public int hashCode() {
        return new HashCodeBuilder(17, 37).append(getCodice()).toHashCode();
    }
    
    @Override
    public boolean equals(Object obj) {
    
        if (obj == null) return false;
        if (this == obj) return true;
        if (!(obj instanceof Famiglia)) return false;
    
        Famiglia reg = (Famiglia) obj;
    
        return new EqualsBuilder().append(getCodice(), reg.getCodice()).isEquals();
    }

Quest'ultima implementazione dei due metodi equals(...)/hashCode(...) risolve il problema dello scarto dei giusti duplicati operato dalla Collection HashSet e nella proprietà famiglie verranno inserite le instanze con la proprietà codice uguale a "RED1-HONEY" e solo una con codice uguale a "BROWN1-HONEY" ossia la prima presa in considerazione.

Spesso non esistono soluzioni "universali" per le scelte implementative o per candidare una Business Key ma, come spesso capita nell'IT, le problematiche vanno valutate caso per caso. Noi ci limiteremo a elencare alcuni suggerimenti in funzione dell'utilizzo Hibernate:

  • La definzione dei metodi equals(...) e hashCode(...) devono essere implementate rispettando le condizioni di Riflessività, Simmetria, Transitività e Consistenza ben illustrate da Joshua Block nel suo libro "Effective Java - Second Edition" [2].
  • L'implementazione dei metodi equals(...) e hashCode(...) per le classi Hibernate devono utilizzare sempre i metodi getter per il confronto dei valori e il controllo instanceof per il confronto delle istanze perchè spesso vengono manipolati in modo trasparenti le classi Proxy (o Placeholder) e non quelle effettive e si veda a tal proposito il metodo session.load(...). Tratteremo adeguatamente il problema dei Proxy e del "Lazy loading" nei prossimi articoli.
  • La candidatura di una Business Key dovrebbe tenere conto del livello di immutabilità, del fatto che siano definiti dei constraint di univocità sul DB, etc.

In che modo Hibernate utilizza le "nostre" implementazioni dei metodi equals(...)/hashCode(...) all'interno del "Persistence Context" quando deve eseguire "internamente" confronti tra oggetti dello stesso tipo in stato Persistent? La risposta è che non le considera per garantire sempre il "persistence context-scoped identiy". Facciamo un esempio prendendo la classe Famiglia con l'implementazione corretta della equals(....)/hashCode(...) basata sulla proprietà codice:

    ...................................................................
    //Nuovo Oggetto di tipo FAMIGLIA
    Famiglia famiglia2 = new Famiglia();
    famiglia2.setCodice("BROWN1-HONEY");
    //Seguono set() e creazione oggetti associati/
    ...................................................................
    
    //Nuovo Oggetto di tipo FAMIGLIA - Duplicazione di stato dell'istanza
    //famiglia3 (esclusa naturalmente la proprietà id non valorizzata)
    Famiglia famiglia3 = new Famiglia();
    famiglia3.setCodice("BROWN1-HONEY");
    //Seguono set() e creazione oggetti associati/
    ...................................................................
    ...................................................................
 
    //Apertura nuova Sessione HIBERNATE
    Session session2 = HibernateUtil.getSessionFactory().openSession();
    Transaction transaction2 = session2.beginTransaction();
    
    (famiglia2.equals(famiglia3)); //TRUE
    
    session2.save(famiglia2);
    session2.save(famiglia3);
    transaction2.commit();
    session2.close();
    ...................................................................

Ci si potrebbe aspettare che venga eseguito un controllo di uguaglianza che "impedisca" il passaggio dell'oggetto famiglia3 da Transient a Persistent perchè esiste nel "persistence context" un oggetto "uguale", in funzione della equals() da noi implementata, ma in realtà alla chiamata session2.save(famiglia3) vengono applicati solo i controlli di uguaglianza della "persistence context-scoped identiy" e quindi, essendo famiglia3 Transient, il passaggio di stato va a buon fine generando errore di "unique constraint" sulla INSERT relativa; se avessimo copiato il valore dell'id generato al salvataggio di stato dell'oggetto Persistent famiglia2 in famiglia3 prima della save(...) sarebbe stata lanciata eccezione org.hibernate.NonUniqueObjectException alla chiamata session2.save(famiglia3).

Caricare un oggetto Persistent con Load/Get

I metodi get(...) e load(...) esposti dalla Session Hibernate permettono di caricare direttamente nel "persistence context" oggetti in Stato Persistent tramite l'id, come nell'esempio di cui sotto:

    .............................................................................
    Session session = hibernateUtil.getSession();
    Transaction transaction = session.beginTransaction();
    
    Regina regina = (Regina) session.get(Regina.class, new Integer(8));
    Regina regina2 = (Regina) session.load(Regina.class, new Integer(9));
    
    transaction.commit();
    session.close();
    .............................................................................

dove entrambe le chiamate, get(...) e load(...), creano due istanze Persistence della classe Regina.

La differenza sostanziale tra i due metodi è:

  • Il metodo get(...) crea l'istanza richiesta accedendo al database, se naturalmente non è presente un'istanza Persistence con lo stesso identificativo, per valorizzare lo stato dell'oggetto. Se l'id non ha corrispondenza nella tabella di riferimento della classe viene restituito, dal metodo in questione, null.
  • Il metodo load(...) restituisce l'istanza del proxy di riferimento della classe senza accedere al data base. Come spiegato di seguito, solo quando si accede all'istanza tramite i metodi esposti viene eseguito un accesso al data base per recuperare lo stato dell'oggetto e se l'id (che per inciso è l'unica informazione presente se non si accede all'istanza) non ha corrispondenza nella tabella di riferimento viene lanciata l'eccezione ObjectNotFoundException. Anche in questo caso vale il discorso della get(...) ovvero se esiste un'istanza Persistence con lo stesso identificativo viene ritornato il direttamente il suo riferimento (Cache di primo livello).

Per default Hibernate crea dinamicamente una classe "proxy" per ogni classe mappata usando CGLIB. Una classe proxy è una sottoclasse della classe di riferimento. La sottoclasse generata contiene tutti i metodi della superclasse e quando si accede ad uno di essi il proxy si occupa di recuperare il valore delle proprietà associata, se non è presente nella superclasse, direttamente dal Data Base utilizzando il codice d'invocazione JDBC presente in essa.

Rimandiamo ai prossimi articoli la spiegazione del modo in cui sia possibile creare e gestire classi proxy personalizzate e quali siano le problematiche riccorenti nell'utilizzare i proxy nelle gerarchie e come si gestisce il "lazy loading" tramite Hibernate.

Identificazione di Oggetto New - Old

Abbiamo più volte sottolineato che un oggetto Transient può considerarsi tale se la proprietà identificante non è valorizzata al contrario di un oggetto Detached, ma come è ben illustrato nel libro "Java persistence with Hibernate" [1] vengono sfruttati anche i seguenti criteri per determinare lo stato Transient di un oggetto:

  1. La proprietà identificante id è null;
  2. Il campo di Versionamento (se è previsto) è null;
  3. Una nuova istanza della stessa classe, creata internamente da Hibernate, ha lo stesso valore della proprietà identificante dell'istanza data;
  4. Si specifica il valore di "unsaved-value" (ad esempio il valore 0) nel file di mapping e il valore della proprietà identificante id valorizzata coincide con esso. Vedremo che unsaved-value viene utilizzato con la stessa logica anche per il versionamento.
  5. La proprietà identificante id valorizzata non permette di recuperare un istanza dello stesso tipo e con lo stesso id nella cache di secondo livello;
  6. È stata implementata l'interfaccia org.hibernate.Interceptor e forzando un ritorno a Boolean.TRUE nel metodo isUnsaved();

Anticipazione gestione Transazioni

Prima di inoltrarci nelle varie metodologie per il passaggio di stato degli oggetti è bene ricordare che in tutti gli esempi abbiamo indicato e indicheremo esplicitamente sempre:

    ...................................................................
    Transaction transaction = session.beginTransaction();
    
    ...................................................................
    
    transaction.commit();
    ...................................................................

ossia dell'apertura e chiusura della transazione utilizzata dal Persistence Context. Illustreremo nei prossimi articoli le varie tecniche per la gestione delle transazioni ma dobbiamo tuttavia anticipare che se viene generato un errore che porta a Rollback della transazione, Hibernate, per scelta implementativa, non ripristina mai lo stato degli oggetti in stato Persistent coinvolti, a differenza dei DBMS che riportano sempre in una stato di "congruenza" il DB (vedere definizione di ACID).
Nel primo articolo della serie avevamo accennato che tramite la gestione del dirty checking (ovvero mantenimento di uno snapshot con lo stato originale dell'oggetto quando diventa Persistent) Hibernate riconosce quando e quali proprietà sono coinvolte nell'aggiornamento del DB ma in caso di Rollback della transazione i "vecchi valori" contenuti nello snapshot dell'oggetto modificato non vengono utilizzati per allineare allo stato precedente l'oggetto.

Occorre fare attenzione perchè l'oggetto "dirty", la cui istanza diventa Detached dopo il Rollback, potrebbe essere riutilizzato in una nuova sessione con la possibilità di ri-generare errore quando ri-diventa Persistent. Riportiamo un piccolo esempio per cercare di chiarire:

    ...................................................................
    Session session = hibernateUtil.getSession();
    Transaction transaction = session.beginTransaction();
    
    Regina regina = (Regina) session.get(Regina.class, new Integer(8));
    regina.getColore.equals("giallo"); //TRUE    
    regina.setColore(null); //Ipotiziamo di aver aggiunto alla proprietà 
                            //colore <not-null="true">
 
    transaction.commit();//La query di UPDATE generata dalla flush() lanciata
                         //da Hibernate manda il ROLLBACK la transazione
    session.close();
    ...................................................................
    // regina punta ad un istanza detached  in cui lo stato contiene
    //la proprieta colore = null  e NON  il valore originale
    // recuperato con la get()
    Session session2 = hibernateUtil.getSession();
    Transaction transaction2 = session2.beginTransaction();
    
    session2.update(regina);  
    
    transaction2.commit(); //La query di UPDATE generata dalla flush() lanciata
                           //da Hibernate manda il ROLLBACK la transazione  
    session2.close();
    ...................................................................

In questo caso , l'oggetto regina in stato Persistent viene modificato rendendo null la proprietà colore e tale modifica lo rende "dirty" (diverso dal valore della proprietà colore sullo snapshot che rimane "giallo") e quindi prima della  transaction.commit() viene lanciata una query di UPDATE scatenando un eccezione di obbligatorietà a causa del null e la conseguente esecuzione di una RollBack; alla chiusura della session l'oggetto regina passa in stato Detached con la proprietà colore uguale a null ed è evidente che il tentativo di rendere Persistent l'oggetto potrebbe portare allo stesso identico errore.

 

 

Figura 3 - Stato oggetti e variazioni

 

Passaggio da Transient a Persistent

Come si può vedere dal diagramma della Figura 3, già trattato nel primo articolo, il passaggio di stato da Transient a Persistent può avvenire chiamando i metodi diretti della Sessione Hibernate save(), saveOrUpdate(), merge() oppure associando l'oggetto Transient ad un oggetto Persistent.

Metodo save()

Il metodo save(obj) permette di eseguire il passaggio di stato di un oggetto Transient a Persistent indicando ad Hibernate la necessità di salvare lo stato dell'oggetto generando sempre e comunque una query di INSERT. Riportiamo un semplice esempio sempre basandoci sul domain model della figura 1:

    ...................................................................
    //Nuovo oggetto in stato Transient  
    Regina regina = new Regina();    
    regina.setDataNascita(new Date());
    regina.setColore("rosso");
    regina.setLineaGenetica("B112");
    regina.setNote("regina allevamento il Pungiglione");
    
    Session session = hibernateUtil.getSession();
    Transaction transaction = session.beginTransaction();
    
    session.save(regina);    //L'oggetto regina passa da stato Transient a Persistent -
                        //Richiede INSERT immediata per generazione automatica
                        //della chiave primaria
    
    regina.setColore("nero"); //Pone l'oggetto Persistent in stato dirty.
                              //Richiede una query di UPDATE per allineare 
                              //il suo nuovo stato con il database
                              //Tale query viene "ritardata" fino alla flush() 
                              //gestita in modo trasparente da Hibernate
    
    transaction.commit();
    session.close();
    ...................................................................

in questo particolare caso quando l'oggetto regina diventa Persistent viene subito generata una query di INSERT per poter valorizzare il valore della proprietà identificante richiesta dalla registrazione:

.....

    
 
.....

di conseguenza la variazione della proprietà colore dell'oggetto regina nello stato Persistent richiede, come vedremo più avanti, una query di UPDATE per aggiornare lo stato prima della commit proprio perchè l'oggetto Persistent è "dirty". Nella figura 4. riportiamo lo schema (preso da "Java persistence with Hibernate" [1]) che riassume i passaggi di stato di un oggetto da Transient (T) a Persistent (P) fino a Detached (D) tramite il metodo save(obj) e la sequenza temporale con cui avvengono solitamente le operazioni; specifichiamo che, se non chiamato esplicitamente, il metodo flush() viene gestito automaticamente da Hibernate (come indicato nella figura 4).

 

Figura 4 - Metodo save(obj)

Il metodo save(...) fallisce se lo stato dell'oggetto è già stato reso persistente sul DataBase precedentemente: questo è ovvio visto che genera sempre una query di INSERT e quindi si avrebbe un eccezione di "chiave duplicata".

Associazione oggetto Transient ad Oggetto Persistent

Abbiamo visto negli esempi precedenti, vedi Apiario e Famiglia, che l'associazione di un oggetto B in stato Transient a un oggetto A in stato Persistent rende B automaticamente Persistent. Nello schema di figura 5 abbiamo riportato un esempio di associazione in cui all'oggetto caricato tramite una load(id) in stato Persistent Pa (dove gli indici "a" e "b" servono solo per differenziare gli stati delle istanze coinvolte nello schema) viene associato un oggetto Transient creato in [1] tramite il metodo setter relativo; tale operazione porta il nuovo oggetto ad uno stato Persistent (Pb), generando una query di INSERT per il salvataggio del suo stato e una di UPDATE per l'oggetto in stato Pa per valorizzare la FK di associazione con la nuova PK generata dalla INSERT.

 

 

Figura 5 - Variazione di stato da Transient a Persistent tramite associazione.

 

Metodo saveOrUpdate()

A differenza del metodo save(...) il metodo saveOrUpdate() esegue un controllo sullo stato dell'oggetto ossia applica le tecniche elencate nel paragrafo "Identificazione di Oggetto New - Old" per identificare lo stato Persistence o Detached dell'oggetto. Se l'oggetto è in stato Persistence, esso ha lo stesso identico comportamento del metodo save(...) altrimenti applica la stessa logica del metodo update(...).

Ricapitoliamo sintatticamente il comportamento:

  1. se l'oggetto è già in stato Persistent non fa niente;
  2. se esiste un'altra istanza con la stessa proprietà identificante, Hibernate lancia un'eccezione per il principio del persistence context-scoped identiy;
  3. se l'oggetto è considerato "New" sotto le condizioni riportate precedentemente, esegue una save(...);
  4. altrimenti esegue update(...).

Modificare un oggetto Persistent

Abbiamo più volte visto che quando viene caricato in sessione un oggetto, tramite i metodi get(...) o load(...) oppure tramite query HQL o criteria, questo "passa" direttamente in stato Persistent. In funzione dei metodi utilizzati abbiamo visto che Hibernate crea un'istanza della classe passata come parametro ai metodi oppure un proxy (come brevemente illustrato nel precedenti paragrafi). Insieme all'istanza abbiamo anche visto che Hibernate crea sempre, se non esplicitato tramite il metodo session.setReadOnly(object, true), uno snapshot per il controllo del dirty checking.
Ci preme nuovamente sottolineare che ogni modifica apportata allo stato dell'oggetto Persistent, come ad esempio i punti n. 3 e n. 4 della figura 6, rendono l'oggetto "dirty" e per il principio del "trasparent transaction-level write-behind", del quale abbiamo parlato nell'articolo precedente [4], le query di aggiornamento vengono ritardate il più possibile fino alla chiusura della sessione; nella figura 6, ad esempio, all'oggetto in stato Persistent vengono apportate due modifiche successive di stato ma la query "comulativa" di UPDATE viene ritardata fino alla chiamata del metodo flush() (punto n. 5 dell'immagine).

 

 

 

Figura 6 - Update ritardato con doppia modifica.

 

Modificare un oggetto Detached

Metodo update(....)

Il metodo update(Obj), come illustrato nello schema di figura 7, porta un oggetto dallo stato Detached a uno stato Persistent  ri-agganciandolo alla Sessione. Sempre riferendoci al domain model di figura 1 riportiamo un semplice esempio:

    ...................................................................
    Session session = hibernateUtil.getSession();
    Transaction transaction = session.beginTransaction();
    
    Regina regina = (Regina) session.get(Regina.class, new Integer(8));    
    
    transaction.commit();
    session.close();
    ...................................................................
    ...................................................................
    regina.setColore("nero"); //Modifica di oggetto Deatache
    
    Session session2 = hibernateUtil.getSession();
    Transaction transaction2 = session2.beginTransaction();
    
    session2.update(regina); //L'oggetto regina passa da statoDeatache a Persistent
    
    transaction2.commit(); //Viene eseguita la flush() per generare
                           //la query di UPDATE per la modifica del colore
    session2.close();
    ...................................................................

La chiamata session2.update(regina) mette in stato Persistent l'oggetto regina e si richiede esplicitamente una query di UPDATE per l'aggiornamento dello stato eseguita nella flush() gestita da Hibernate. Occorre sottolinerare che la query di UPDATE viene eseguita anche se non sono state apportate modifiche prima dell'"aggancio" al persistence context, ovviamente Hibernate non può sapere se l'oggetto Detached abbia subito modifiche e quindi avere un controllo di dirty checking attuabile solo sulle modifiche apportate dopo il passaggio di stato in Persistence.

Per evitare una query di UPDATE "inutile" è possibile configurare nel file di mapping della classe Regina l'attributo select-before-update="true" in modo da forzare una query di SELECT per eseguire un confronto sullo stato dell'oggetto e la sua immagine sul DB e "simulare" un dirty checking.

Figura 7 - Riaggancio di oggetto Detached.

Metodo merge(....)

Il metodo merge(...) permette di eseguire un'operazione di merge tra lo stato di un oggetto in stato Detached con lo stato di un'istanza Persistence che ha lo stesso valore Id. Riportiamo un esempio sempre utilizzando il domain model di Figura 1:

    ...................................................................
    Session session = hibernateUtil.getSession();
    Transaction transaction = session.beginTransaction();
    
    Regina regina = (Regina) session.get(Regina.class, new Integer(8));
    
    transaction.commit();
    session.close();
    ...................................................................
    ...................................................................
    regina.setColore("nero"); //Modifica di oggetto Deatache
    
    Session session2 = hibernateUtil.getSession();
    Transaction transaction2 = session2.beginTransaction();
    
    Regina regina2 = (Regina) session.get(Regina.class, new Integer(8));
    
    regina2 = session2.merge(regina); //Lo stato dell'oggetto regina2 viene 
                                      //variato con la proprietà colore a "nero"
    
    transaction2.commit(); //Viene eseguita la flush() per generare la query 
                           //di UPDATE per la modifica del colore
                           //per il Dirty Cheking sulla classe regina2
    session2.close();
    ...................................................................

Nella chiamata session2.merge(regina) viene eseguita una copia dello stato dell'oggetto Detached regina su l'oggetto Persistence regina2, se questa operazione, dopo il controllo del dirty checking porta l'oggetto regina2 in stato"dirty" al flush() viene generato una query di UPDATE.

Nella figura 8 riportiamo sintatticamente lo schema comportamentale del metodo merge(...) dove abbiamo indicato che l'oggetto Detached rimane tale anche dopo la chiamata a merge().

Occorre precisare che se non esiste un'istanza Persistence con la stessa proprietà identificante dell'oggetto Detached su cui fare merge hibernate tenta di caricare dal database un oggetto Persistence sfruttando l'id e in caso contrario tenta di creare una nuova istanza Persistente sfruttando sempre lo stato dell'oggetto Detached; in ogni caso il puntatore tornato dal merge(...) si riferisce sempre e comunque ad un'istanza diversa da quella dell'oggetto Detached; riferendoci all'esempio possiamo dire che la relazione (regina2 == regina) è falsa.

 

Figura 8 - Merge

 

Eliminare un oggetto Persistent. Metodo delete(...)

Per poter eliminare le informazioni di stato di un oggetto dal Data Base occorre che l'oggetto sia in stato Persistent e, come si vede nella figura 9 la chiamata delete(obj) porta l'oggetto da stato Persistent ad un nuovo stato intermedio Removed fino alla Flush() gestita da Hibernate dove viene generata una query di DELETE; alla chiusura della sessione l'oggetto ritorna in un stato Transient.

Per esperienza precisiamo che:

  • Se l'istanza in stato Removed viene modificata lo stato dell'oggetto torna Persistent annullando la "prenotazione" alla query di DELETE.
  • Se l'istanza in stato Transient viene "ri-agganciata" a un persistence context il record cancellato potrebbe essere "ri-creato" (escludendo il nuovo id naturalmente se è autogenerato)

 

 

Figura 9 - Delete

Scartare un oggetto Persistent. Metodo evict()

Tramire il metodo evict(...) è possibile sganciare forzatamente un oggetto dal persistence context portando il suo stato da Persistent a Detached come illustrato nello schema comportamentale di figura 10; tutte le operazioni pianificate per la sincronizzazione sul database a fronte di modifiche apportate allo stato dell'oggetto vengono naturalmente cancellate

Figura 10 - Evict

 

Ultima precisazione sul "dirty checking"

Prima di terminare l'articolo, occorre fare una piccola ma importante precisazione. Abbiamo più volte illustrato come Hibernate determini quando un'istanza Persistent diventa "dirty" tramite il confronto dei valori dello stato dell'istanza e il suo "snapshot", che contiene i "vecchi valori" congelati. Questo modo di procedere è valido tranne che per le proprietà Collections (List, Set, etc.) che sono comparate tramite "Java object identity". Per questa ragione è consigliabile la non modifica del riferimento delle Collections associate all'oggetto Persistent se si vogliono evitare query di UPDATE inutili.

Come ultimissima cosa possiamo aggiungere che esiste la possibilità di verificare se un oggetto in stato Persistent è "dirty" tramite il metodo della Session isDirty(Object) che ritorna true se l'oggetto è stato modificato altrimenti false.

Riferimenti

[1] C. Bauer - G.King, "Java persistence with Hibernate", Manning

[2] Joshua Bloch "Effective Java", 2nd ed, Prentice Hall

[3] Alberto Brandolini, "Domain Driven Design - II parte: Primi passi nel Domain Model", MokaByte 135, Dicembre 2008
http://www2.mokabyte.it/cms/article.run?articleId=61K-V5Y-5UQ-2LJ_7f000001_10911033_27879a51

[4] Giovanni Puliti, "Il programmatore e le sue api - IV parte: Il design della persistenza", MokaByte 130, Giugno 2008
http://www2.mokabyte.it/cms/article.run?articleId=DJG-QRR-4YF-4CX_7f000001_10553237_77fd61d5

[5] Hibernate Documentation, Community Area
https://www.hibernate.org/109.html

Condividi

Pubblicato nel numero
143 settembre 2009
Cristian Faraoni è nato a Forlì (FC) nel 1970 ed è laureato in Scienze dell‘Informazione all‘Università degli studi di Bologna. A livello professionale, si occupa di sviluppo, analisi e progettazione del software dal 1997. Attualmente lavora per Imola Informatica S.P.A. svolgendo principalmente attività di consulenza.
Articoli nella stessa serie
Ti potrebbe interessare anche