Un oggetto (istanza di entità) è transiente o persistente rispetto ad una particolare sessione (Session). Oggetti appena istanziati sono naturalmente transienti. La sessione offre servizi per salvare (cioè rendere persistenti, o "persistere") istanze transienti:
DomesticCat fritz = new DomesticCat(); fritz.setColor(Color.GINGER); fritz.setSex('M'); fritz.setName("Fritz"); Long generatedId = (Long) sess.save(fritz);
DomesticCat pk = new DomesticCat(); pk.setColor(Color.TABBY); pk.setSex('F'); pk.setName("PK"); pk.setKittens( new HashSet() ); pk.addKitten(fritz); sess.save( pk, new Long(1234) );
Il metodo save() con un solo argomento genera e assegna un identificatore unico a fritz. La forma con due argomenti tenta di rendere persistente pk utilizzando l'identificatore fornito. In generale scoraggiamo l'uso della forma con due argomenti perché può essere usata per creare chiavi primarie con un significato "di business" (ovvero legato in qualche modo al dominio applicativo). È più utile in certe situazioni speciali come quando si usa Hibernate per persistere un bean di entità ("entity bean") di tipo BMP ("bean managed persistence").
Gli oggetti associati possono essere resi persistenti in un ordine qualunque, a meno che abbiate un vincolo NOT NULL su una colonna di chiave esterna. Non c'è rischio di violare vincoli di chiave esterna, però potreste violare un vincolo NOT NULL se salvate (save()) gli oggetti nell'ordine sbagliato.
I metodi load() della Session vi danno un modo per recuperare un'istanza persistente se già conoscete il suo identificatore. Una prima versione del metodo riceve un oggetto di tipo "class" e caricherà lo stato in un oggetto nuovo. La seconda versione consente di fornire un'istanza in cui verrà caricato lo stato. La forma che riceve un'istanza è particolarmente utile se progettate di usare Hibernate con bean di entità BMP ed è fornita esattamente per questa ragione. Potete comunque scoprire altri usi (pooling di istanza fatto in casa, ecc.).
Cat fritz = (Cat) sess.load(Cat.class, generatedId);
// bisogna incapsulare gli identificatori primitivi long pkId = 1234; DomesticCat pk = (DomesticCat) sess.load( Cat.class, new Long(pkId) );
Cat cat = new DomesticCat(); // load pk's state into cat sess.load( cat, new Long(pkId) ); Set kittens = cat.getKittens();
Notate che load() lancerà un'eccezione non recuperabile se non c'è una riga di database corrispondente. Se la classe è mappata con un mediatore (proxy), load() restituisce un oggetto che è un mediatore non inizializzato e non tocca realmente il database finché non viene invocato un metodo dell'oggetto. Questo comportamento è molto utile se volete creare un'associazione ad un oggetto senza realmente caricarlo dal database.
Se non siete certi che una riga corrispondente esista, dovreste usare il metodo get(), che va direttamente sul database e ritorna null se la riga non esiste.
Cat cat = (Cat) sess.get(Cat.class, id); if (cat==null) { cat = new Cat(); sess.save(cat, id); } return cat;
Potete anche caricare un oggetto usando una istruzione SQL SELECT ... FOR UPDATE. Leggete la prossima sezione per una discussione dei LockMode (modalità di locking) di Hibernate.
Cat cat = (Cat) sess.get(Cat.class, id, LockMode.UPGRADE);
Notate che ogni istanza associata o le collezioni contenute non vengono caricate con una select FOR UPDATE.
È possibile ricaricare un oggetto e tutte le sue collezioni in qualsiasi momento, usando il metodo refresh(). È una cosa particolarmente utile quando dei trigger del database vengono usati per inizializzare alcune proprietà dell'oggetto.
sess.save(cat); sess.flush(); //forza l'INSERT SQL sess.refresh(cat); //rilegge lo stato (dopo che il trigger si esegue)
Se non conoscete l'identificatore (o gli identificatori) deli oggetti che state cercando, usate i metodi find() di Session. Hibernate supporta un linguaggio di interrogazione orientato agli oggetti semplice ma potente.
List cats = sess.find( "from Cat as cat where cat.birthdate = ?", date, Hibernate.DATE ); List mates = sess.find( "select mate from Cat as cat join cat.mate as mate " + "where cat.name = ?", name, Hibernate.STRING ); List cats = sess.find( "from Cat as cat where cat.mate.bithdate is null" ); List moreCats = sess.find( "from Cat as cat where " + "cat.name = 'Fritz' or cat.id = ? or cat.id = ?", new Object[] { id1, id2 }, new Type[] { Hibernate.LONG, Hibernate.LONG } ); List mates = sess.find( "from Cat as cat where cat.mate = ?", izi, Hibernate.entity(Cat.class) ); List problems = sess.find( "from GoldFish as fish " + "where fish.birthday > fish.deceased or fish.birthday is null" );
Il secondo argomento del metodo find() riceve un oggetto o un array di oggetti. Il terzo argomento accetta un tipo di Hibernate o un array di tipi. Questi tipi vengono usati per collegare gli oggetti dati ai segnaposto ? nelle query JDBC (che a loro volta si mappano su parametri IN di un PreparedStatement). Così come nel JDBC, dovreste usare questo meccanismo di associazione preferibilmente alla manipolazione delle stringhe.
La classe Hibernate definisce un certo numero di metodi statici e costanti che forniscono accesso alla maggior parte dei tipi predefiniti, sotto forma di istanze della classe net.sf.hibernate.type.Type.
Se vi aspettate che la vostra query restituisca un numero di oggetti molto largo, ma non vi aspettate di usarli tutti, potreste ottenere performance migliori dai metodi iterate(), che restituiscono un java.util.Iterator. L'iteratore caricherà oggetti al bisogno, usando gli identificatori restituiti da una query SQL iniziale (facendo n+1 select totali).
// caricamento degli id Iterator iter = sess.iterate("from eg.Qux q order by q.likeliness"); while ( iter.hasNext() ) { Qux qux = (Qux) iter.next(); // reperimento dell'oggetto // qualcosa che non abbiamo potuto esprimere nella query if ( qux.calculateComplicatedAlgorithm() ) { // cancellazione dell'istanza corrente iter.remove(); // non abbiamo bisogno di gestire il resto break; } }
Sfortunatamente, java.util.Iterator non dichiara eccezioni, così qualsiasi eccezione SQL o di Hibernate che capiti viene incapsulata in una LazyInitializationException (una sottoclasse di RuntimeException).
Il metodo iterate() ottiene performance migliori anche nel caso in cui ci si aspetti che molti oggetti siano già stati caricati e messi in cache dalla sessione, o se i risultati della query ottengono molte volte gli stessi oggetti. (Quando non ci sono dati in cache o dati ripetuti, find() è quasi sempre più veloce.) Ecco un esempio di un'interrogazione che dovrebbe venire chiamata usando iterate():
Iterator iter = sess.iterate( "select customer, product " + "from Customer customer, " + "Product product " + "join customer.purchases purchase " + "where product = purchase.product" );
Se chiamassimo l'interrogazione precedente usando find() restituiremmo un ResultSet JDBC molto ampio che conterrebbe gli stessi dati molte volte.
Le query di Hibernate a volte restituiscono tuple di oggetti, nel qual caso ogni tupla viene restituita come un array:
Iterator foosAndBars = sess.iterate( "select foo, bar from Foo foo, Bar bar " + "where bar.date = foo.date" ); while ( foosAndBars.hasNext() ) { Object[] tuple = (Object[]) foosAndBars.next(); Foo foo = tuple[0]; Bar bar = tuple[1]; .... }
Le interrogazioni possono specificare una proprietà di una classe nella clausola select. Possono anche chiamare funzioni aggregate SQL. Le proprietà o gli aggregati sono considerati risultati "scalari".
Iterator results = sess.iterate( "select cat.color, min(cat.birthdate), count(cat) from Cat cat " + "group by cat.color" ); while ( results.hasNext() ) { Object[] row = results.next(); Color type = (Color) row[0]; Date oldest = (Date) row[1]; Integer count = (Integer) row[2]; ..... }
Iterator iter = sess.iterate( "select cat.type, cat.birthdate, cat.name from DomesticCat cat" );
List list = sess.find( "select cat, cat.mate.name from DomesticCat cat" );
Se avete bisogno di specificare limiti per il vostro set di risultati (il numero massimo di righe che volete recuperare o la prima riga che volete caricare) dovreste ottenere un'istanza di net.sf.hibernate.Query:
Query q = sess.createQuery("from DomesticCat cat"); q.setFirstResult(20); q.setMaxResults(10); List cats = q.list();
Potete anche definire una query con un nome nel documento di mappaggio. (Ricordatevi di usare una sezione CDATA se la vostra query contiene caratteri che potrebbero essere interpretati come caratteri di contrassegno.)
<query name="eg.DomesticCat.by.name.and.minimum.weight"><![CDATA[ from eg.DomesticCat as cat where cat.name = ? and cat.weight > ? ] ]></query>
Query q = sess.getNamedQuery("eg.DomesticCat.by.name.and.minimum.weight"); q.setString(0, name); q.setInt(1, minWeight); List cats = q.list();
L'interfaccia Query supporta l'utilizzo di parametri per nome. I parametri per nome sono identificatori nella forma :name nella stringa di interrogazione. Ci sono metodi su Query per collegare valori ai parametri per nome o ai parametri ? nello stile di JDBC. Contrariamente a JDBC, Hibernate numera i parametri a partire da zero. I vantaggi dei parametri per nome sono:
i parametri per nome non hanno dipendenza dall'ordine con cui appaiono nella stringa di interrogazione
possono apparire più volte nella stessa query
sono auto-documentanti
//parametro per nome (consigliato) Query q = sess.createQuery("from DomesticCat cat where cat.name = :name"); q.setString("name", "Fritz"); Iterator cats = q.iterate();
//parametro posizionale Query q = sess.createQuery("from DomesticCat cat where cat.name = ?"); q.setString(0, "Izi"); Iterator cats = q.iterate();
//lista di parametri con nome List names = new ArrayList(); names.add("Izi"); names.add("Fritz"); Query q = sess.createQuery("from DomesticCat cat where cat.name in (:namesList)"); q.setParameterList("namesList", names); List cats = q.list();
Se il vostro driver JDBC supporta i ResultSet scrollabili, l'interfaccia Query può essere usata per ottenere un oggetto ScrollableResults che consente una navigazione più flessibile dei risultati dell'interrogazione.
Query q = sess.createQuery("select cat.name, cat from DomesticCat cat " + "order by cat.name"); ScrollableResults cats = q.scroll(); if ( cats.first() ) { // trova il primo nome su ogni pagina di una lista alfabetica di gatti per nome firstNamesOfPages = new ArrayList(); do { String name = cats.getString(0); firstNamesOfPages.add(name); } while ( cats.scroll(PAGE_SIZE) ); // ora recuperiamo la prima pagina di gatti pageOfCats = new ArrayList(); cats.beforeFirst(); int i=0; while( ( PAGE_SIZE > i++ ) && cats.next() ) pageOfCats.add( cats.get(1) ); }
Il comportamento del metodo scroll() è simile ad iterate(), eccettuato il fatto che gli oggetti possono essere inizializzati selettivamente da get(int), invece di essere inizializzati uno ad uno a righe intere.
Un filtro di collezione è un tipo speciale di interrogazione che può essere applicato ad una collezione persistente od un array. La stringa di interrogazione può fare riferimento a this, per indicare l'elemento corrente della collezione.
Collection blackKittens = session.filter( pk.getKittens(), "where this.color = ?", Color.BLACK, Hibernate.enum(Color.class) );
La collezione restituita viene considerata un "sacco" (bag).
Osservate che i filtri non richiedono una clausola from (benché possano averne una, se è necessario). I filtri non si limitano a restituire gli elementi stessi delle collezioni.
Collection blackKittenMates = session.filter( pk.getKittens(), "select this.mate where this.color = eg.Color.BLACK" );
L'HQL è molto potente, ma alcune persone preferiscono costruire dinamicamente le interrogazioni, usando un'API orientata agli oggetti, piuttosto che inserire stringhe nel loro codice Java. Per queste persone, Hibernate fornisce una API di interrogazione intuitiva per criteri (Criteria).
Criteria crit = session.createCriteria(Cat.class); crit.add( Expression.eq("color", eg.Color.BLACK) ); crit.setMaxResults(10); List cats = crit.list();
Se non avete familiarità con le sintassi "simil-SQL", questa è forse la maniera più semplice di approcciare Hibernate. Questa API è poi anche più estensibile dell'SQL: le applicazioni potrebbero fornire le loro implementazioni dell'interfaccia Criterion.
È possibile esprimere una query in SQL, usando createSQLQuery(). Dovete circondare gli alias SQL di parentesi graffe.
List cats = session.createSQLQuery( "SELECT {cat.*} FROM CAT AS {cat} WHERE ROWNUM<10", "cat", Cat.class ).list();
List cats = session.createSQLQuery( "SELECT {cat}.ID AS {cat.id}, {cat}.SEX AS {cat.sex}, " + "{cat}.MATE AS {cat.mate}, {cat}.SUBCLASS AS {cat.class}, ... " + "FROM CAT AS {cat} WHERE ROWNUM<10", "cat", Cat.class ).list()
Le interrogazioni SQL possono contenere parametri per nome e posizionali, proprio come quelle di Hibernate.
Le istanze persistenti e transazionali (cioè gli oggetti caricati, salvati, creati o interrogati dalla Session) possono venire manipolati dall'applicazione, ed ogni cambiamento allo stato persistente verrà salvato quando la Session viene scaricata (flushed) (questo concetto è discusso più oltre in questo stesso capitolo). Quindi, la maniera più semplice di aggiornare lo stato di un oggetto è di caricarlo (load()), e poi manipolarlo direttamente mentre la Session è aperta:
DomesticCat cat = (DomesticCat) sess.load( Cat.class, new Long(69) ); cat.setName("PK"); sess.flush(); // i cambiamenti all'oggetto gatto vengono automaticamente // individuati e resi persistenti
Alcune volte questo modello di programmazione è inefficiente, poiché richiederebbe sia una SELECT SQL (per caricare un oggetto) sia una UPDATE (per rendere persistente il suo stato aggiornato) nella stessa sessione. Per questo, Hibernate offre un approccio alternativo.
Molte applicazioni hanno bisogno di recuperare un oggetto in una transazione, mandarlo allo strato di interfaccia perché venga manipolato, e poi salvarne i cambiamenti in una nuova transazione. (Le applicazioni che usano questo genere di approccio in un ambiente ad alta concorrenza solitamente usano dati versionati per assicurare l'isolamento delle transazioni.) Questo approccio richiede un modello programmativo leggermente differente rispetto a quello descritto nell'ultima sezione. Hibernate supporta questo modello fornendo il metodo Session.update().
// nella prima sessione Cat cat = (Cat) firstSession.load(Cat.class, catId); Cat potentialMate = new Cat(); firstSession.save(potentialMate); // in uno strato più elevato dell'applicazione cat.setMate(potentialMate); // più tardi, in un'altra sessione secondSession.update(cat); // aggiornamento del gatto (cat) secondSession.update(mate); // aggiornamento dell'amichetto (mate)
Se l'oggetto Cat con identificatore catId fosse già stato caricato da secondSession quando l'applicazione tenta di aggiornarlo, verrebbe lanciata un'eccezione.
L'applicazione dovrebbe aggiornare (update()) istanze transienti raggiungibili dall'istanza transiente data se e solo se vuole che anche il loro stato venga aggiornato. (Eccetto per gli oggetti a ciclo di vita, discussi più avanti).
Gli utenti di Hibernate hanno chiesto un metodo di scopo generale che salvi un'istanza transiente generando un nuovo identificatore o aggiorni lo stato persistente associato con il suo identificatore corrente. Il metodo saveOrUpdate() adesso implementa questa funzionalità.
Hibernate distingue istanze "nuove" (non salvate) da istanze "esistenti" (salvate o caricate in una sessione precedente) tramiet il valore della loro proprietà identificatore (o versione, o marca di tempo). L'attributo unsaved-value degli elementi <id> (o <version>, o <timestamp>) nel mappaggio specifica quali valori dovrebbero venire interpretati come rappresentanti di una "nuova" istanza.
<id name="id" type="long" column="uid" unsaved-value="null"> <generator class="hilo"/> </id>
I valori consentiti di unsaved-value sono:
any - salvare sempre
none - aggiornare sempre
null - salvare quando l'identificatore è nullo (questa è l'opzione predefinita)
valid identifier value - salvare quando l'identificatore è nullo o il valore dato
undefined - il valore predefinito per version o timestamp, viene usato un controllo sull'identificatore
// nella prima sessione Cat cat = (Cat) firstSession.load(Cat.class, catID); // in uno strato più elevato dell'applicazione Cat mate = new Cat(); cat.setMate(mate); // più avanti, in un'altra sessione secondSession.saveOrUpdate(cat); // aggiorna lo stato esistente (cat ha un id non nullo) secondSession.saveOrUpdate(mate); // salva la nuova istanza (mate ha un id nullo)
L'utilizzo e la semantica di saveOrUpdate() sembra confondere i nuovi utenti. In primo luogo, finché non stiate cercando di usare istanze di una sessione in un'altra sessione, non dovreste aver bisogno di usare update() o saveOrUpdate(). Alcune applicazioni non avranno mai bisogno di nessuno di questi due metodi.
Di solito update() o saveOrUpdate() vengono usati nello scenario seguente:
l'applicazione carica un oggetto nella prima sessione
l'oggetto viene passato allo strato di interfaccia
vengono fatte alcune modifiche all'oggetto
l'oggetto viene ripassato allo strato della logica di business
l'applicazione rende persistenti queste modifiche chiamando update() in una seconda sessione
saveOrUpdate() fa quanto segue:
se l'oggetto è già persistente in questa sessione, non fare nulla
se l'oggetto non ha proprietà identificatore, lo salva (save())
se il valore dell'identificatore salva i criteri specificati da unsaved-value, lo salva (save())
se l'oggetto è con versioni (version o timestamp), allora la versione avrà precedenza sul controllo dell'identificatore, a meno che la versione non sia unsaved-value="undefined" (valore di default)
se un altro oggetto associato con la sessione ha lo stesso identificatore, lancia un'eccezione
Il metodo lock() consente all'applicazione di riassociare un oggetto non modificato con una nuova sessione.
//riassocia semplicemente: sess.lock(fritz, LockMode.NONE); //fa un controllo di versione, poi riassocia: sess.lock(izi, LockMode.READ); //fa un controllo di versione usando SELECT ... FOR UPDATE, quindi riassocia: sess.lock(pk, LockMode.UPGRADE);
Session.delete() rimuoverà lo stato di un oggetto dal database. Naturalmente, la vostra applicazione potesse ancora mantenere un riferimento ad esso. Per questo, è preferibile pensare a delete() come un modo per rendere transiente un'istanza persistente.
sess.delete(cat);
Potete anche cancellare molti oggetti allo stesso tempo, passando una stringa di interrogazione a delete().
Ora è poi possibile cancellare oggetti in qualsiasi ordine, senza il rischio di violazioni di chiave. Naturalmente, è sempre possibile violare un vincolo NOT NULL su una chiave esterna cancellando oggetti nell'ordine sbagliato.
Di tanto in tanto, la Session eseguirà le istruzioni SQL necessarie per sincronizzare lo stato della connessione JDBC con lo stato degli oggetti mantenuti in memoria. Questo processo, il flush, avviene come comportamento standard nei seguenti punti
per effetto di alcune invocazioni di find() o iterate()
in seguito a net.sf.hibernate.Transaction.commit()
in seguito a Session.flush()
Le istruzioni SQL vengono emesse nell'ordine seguente
tutti gli inserimenti di entità, nello stesso ordine con cui gli oggetti corrispondenti erano stati salvati usando Session.save()
tutti gli aggiornamenti di entità
tutte le cancellazioni di collezione
tutte le cancellazioni, gli aggiornamenti e inserimenti di elementi di collezioni
tutti gli inserimenti di collezione
tutte le cancellazioni di entità, nello stesso ordine con cui gli oggetti corrispondenti erano stati cancellati usando Session.delete()
(Una eccezione è che gli oggetti che usando meccanismi di generazione di identificator native vengono inseriti nel momento stesso in cui sono salvati.)
Eccettuato quando chiamate flush() esplicitamente, non ci sono assolutamente garanzie riguardo a quando la Session eseguirà le chiamate JDBC, solo l'ordine con cui verranno eseguite. In ogni caso, Hibernate garantisce che i metodi Session.find(..) non restituiranno mai dati obsoleti o che restituiranno dati sbagliati.
È possibile cambiare il comportamento predefinito in modo che lo scaricamento avvenga meno frequentemente. La classe FlushMode definisce tre modi differenti. Questo non è utile nel caso di transazioni "a sola lettura", in cui potrebbe solo essere usato per ottenere un (molto) leggero incremento di performance.
sess = sf.openSession(); Transaction tx = sess.beginTransaction(); //consente alle interrogazioni di restituire stato obsoleto sess.setFlushMode(FlushMode.COMMIT); Cat izi = (Cat) sess.load(Cat.class, id); izi.setName(iznizi); // esegue alcune interrogazioni.... sess.find("from Cat as cat left outer join cat.kittens kitten"); //il cambiamento su izi non viene scaricato! ... tx.commit(); //avviene lo scaricamento
La conclusione di una sessione implica quattro fasi distinte:
scaricamento della sessione
commit della transazione
chiusura della sessione
gestione delle eccezioni
Se state usando l'API Transaction, non avete bisogno di preoccuparvi di questo passo. Verrà effettuato implicitamente quando la transazione verrà committata. In caso contrario dovreste chiamare Session.flush() per assicurarvi che tutti i cambiamenti siano sincronizzati con il database.
Se state usando l'API Transaction di Hibernate, questo apparirà come:
tx.commit(); // scaricamento della Session e commit della transazione
Se state gestendo autonomamente le transazioni JDBC, dovreste chiamare manualmente commit() sulla connessione JDBC.
sess.flush(); sess.connection().commit(); // non necessario per un datasource JTA
Se decidete di non fare il commit dei cambiamenti:
tx.rollback(); // rollback della transazione
or:
// non necessario per un datasource JTA, importante altrimenti sess.connection().rollback();
Se fate il rollback della transazione dovreste immediatamente chiudere e scartare la sessione corrente, per assicurarvi che lo stato interno di Hibernate sia coerente.
Una chiamata a Session.close() segna la fine di una sessione. L'implicazione principale di close() è che la connessione JDBC verrà liberata dalla session.
tx.commit(); sess.close();
sess.flush(); sess.connection().commit(); // non necessario per un datasource JTA sess.close();
Se avete fornito le vostre connessioni, close() restituisce un riferimento ad esse, in modo tale che possiate chiuderle o restituirle al lotto (pool) manualmente. In caso contrario close() le restituisce al lotto.
Se la Session lancia un'eccezione (compresa una qualsiasi SQLException), dovreste immediatamente fare il rollback della transazione, chiamare Session.close() e scartare l'istanza di Session. Alcuni metodi di Session non lasceranno la sessione in uno stato coerente.
Raccomandiamo il seguente idioma di gestione delle eccezioni:
Session sess = factory.openSession(); Transaction tx = null; try { tx = sess.beginTransaction(); // fa del lavoro ... tx.commit(); } catch (Exception e) { if (tx!=null) tx.rollback(); throw e; } finally { sess.close(); }
O, quando si gestiscono manualmente le transazioni JDBC:
Session sess = factory.openSession(); try { // fa del lavoro ... sess.flush(); sess.connection().commit(); } catch (Exception e) { sess.connection().rollback(); throw e; } finally { sess.close(); }
O, quando si usa un datasource iscritto con il JTA:
UserTransaction ut = .... ; Session sess = factory.openSession(); try { // fa del lavoro ... sess.flush(); } catch (Exception e) { ut.setRollbackOnly(); throw e; } finally { sess.close(); }
Per salvare o aggiornare tutti gli oggetti in un grafo di oggetti associati dovete
chiamare save(), saveOrUpdate() o update() su ogni oggetto individuale O
mappare gli oggetti associati con cascade="all" o cascade="save-update".
Nello stesso modo, per cancellare tutti gli oggetti un grafo
chiamate delete() su ogni oggetto individuale O
mappate gli oggetti associati usando cascade="all", cascade="all-delete-orphan" o cascade="delete".
Raccomandazione:
Se il periodo di esistenza dell'oggetto figlio è incluso in quello del genitore fate in modo che diventi un oggetto del ciclo di vita (lifecycle object), specificando cascade="all".
In caso contrario, chiamate esplicitamente save() e delete() su di esso dal codice applicativo. Se volete davvero risparmiarvi della digitazione extra, usate cascade="save-update" e chiamate esplicitamente delete().
Il mappaggio di un'associazione (molti-a-uno, o collezione) con cascade="all" marchia l'associazione come una relazione di stile genitore/figlio in cui il salvataggio/aggiornamento/cancellazione del genitore risulta in operazioni analoghe dei figli. Inoltre, il semplice fatto di avere un riferimento ad un figlio in un genitore persistente risulterà in salvataggio / aggiornamento del figlio. La metafora è tuttavia incompleta. Un figlio che diventi non più referenziato dal padre non viene automaticamente cancellato, eccetto nel caso di una associazione <one-to-many> mappata con cascade="all-delete-orphan". La semantica precisa delle operazioni di cascata sono come segue:
Se un parente viene salvato, tutti i figli vengono passati a saveOrUpdate()
Se un parente viene passato a update() o saveOrUpdate(), tutti i figli vengono passati a saveOrUpdate()
Se un figlio transiente diventa referenziato da un padre persistente, viene passato a saveOrUpdate()
Se un parente viene cancellato, tutti i figli vengono passati a delete()
Se un figlio transiente viene de-referenziato da un padre persistente, non succede niente di speciale (l'applicazione dovrebbe cancellare esplicitamente il figlio se necessario) a meno che la relazione non sia mappata come come cascade="all-delete-orphan", nel qual caso il figlio "orfano" viene cancellato.
Hibernate non implementa completamente il concetto di "persistenza per raggiungibilità", che implicherebbe una (inefficiente) raccolta dei rifiuti (garbage collection) persistenti. In ogni caso, in seguito alle richieste di molta gente, Hibernate supporta la nozione di entità che diventano persistenti quando vengono referenziate da un altro oggetto persistente. Le associazioni marchiate cascade="save-update" si comportano così. Se volete usare questo approccio in tutta l'applicazione, è più comodo specificare l'attributo default-cascade nell'elemento <hibernate-mapping>.
L'interfaccia Interceptor fornisce dei punti di richiamo (callback) dalla sessione verso l'applicazione, consentendole di ispezionare e/o manipolare proprietà di un oggetto persistente prima che venga salvato, aggiornato, cancellato o caricato. Un utilizzo possibile per questo è di tracciare delle informazioni di auditing. Per esempio, l'Interceptor seguente imposta automaticamente il createTimestamp quando viene creato un oggetto Auditable e aggiorna la proprietà lastUpdateTimestamp quando un Auditable viene aggiornato.
package net.sf.hibernate.test; import java.io.Serializable; import java.util.Date; import java.util.Iterator; import net.sf.hibernate.Interceptor; import net.sf.hibernate.type.Type; public class AuditInterceptor implements Interceptor, Serializable { private int updates; private int creates; public void onDelete(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) { // non fare nulla } public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) { if ( entity instanceof Auditable ) { updates++; for ( int i=0; i < propertyNames.length; i++ ) { if ( "lastUpdateTimestamp".equals( propertyNames[i] ) ) { currentState[i] = new Date(); return true; } } } return false; } public boolean onLoad(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) { return false; } public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) { if ( entity instanceof Auditable ) { creates++; for ( int i=0; i<propertyNames.length; i++ ) { if ( "createTimestamp".equals( propertyNames[i] ) ) { state[i] = new Date(); return true; } } } return false; } public void postFlush(Iterator entities) { System.out.println("Creazioni: " + creates + ", Aggiornamenti: " + updates); } public void preFlush(Iterator entities) { updates=0; creates=0; } ...... ...... }
L'interceptor dovrebbe essere specificato alla creazione di una sessione.
Session session = sf.openSession( new AuditInterceptor() );
Hibernate richiede un modello di meta-livello molto ricco di tutte le entità e dei tipi di valori. Di tanto in tanto, questo modello è molto utile alla stessa applicazione. Ad esempio, l'applicazione potrebbe usare i metadati di Hibernate per implementare un algoritmo "intelligente" di copia "profonda" che capisca quali oggetti dovrebbero venire copiati (ad esempio i tipi di valore mutabili) e quali non dovrebbero (ad esempio i tipi di valore immutabili e, magari, le entità associate).
Hibernate espone metadati tramite le interfacce ClassMetadata e CollectionMetadata e la gerarchia Type. Le istanze delle interfacce dei metadati possono venire ottenute dalla SessionFactory.
Cat fritz = ......; Long id = (Long) catMeta.getIdentifier(fritz); ClassMetadata catMeta = sessionfactory.getClassMetadata(Cat.class); Object[] propertyValues = catMeta.getPropertyValues(fritz); String[] propertyNames = catMeta.getPropertyNames(); Type[] propertyTypes = catMeta.getPropertyTypes(); // ottiene una mappa di tutte le proprietà che non sono collezioni o associazioni // TODO: e i componenti? Map namedValues = new HashMap(); for ( int i=0; i<propertyNames.length; i++ ) { if ( !propertyTypes[i].isEntityType() && !propertyTypes[i].isCollectionType() ) { namedValues.put( propertyNames[i], propertyValues[i] ); } }