Abbiamo già parlato delle collezioni da un punto di vista funzionale. In questa sezione mettiamo in evidenza alcune questioni legate a come le collezioni si comportano durante l'esecuzione
Hibernate definisce tre tipi fondamentali di collezioni:
collezioni di valori
associazioni uno-a-molti
associazioni molti-a-molti
Questa classificazione distingue le varie relazioni tra tabelle e chiavi esterne, ma non ci dice abbastanza di quello che ci interessa sul modello relazionale. Per capire completamente la struttura relazionale e le caratteristiche di performance, dobbiamo anche prendere in cosiderazione la struttura della chiave primaria che viene usata da Hibernate per modificare o cancellare le righe corrispondenti alla collezione. Questo suggerisce la classificazione seguente:
collezioni con indice (indexed collection)
insiemi (set)
"sacchi" (bags)
Tutte le collezioni indicizzate (mappe, liste, array) hanno una chiave primaria che consiste nelle colonne <key> (chiave) e <index> (indice). Solitamente in questi casi gli aggiornamenti delle collezioni sono molto performanti, poiché la chiave primaria può essere indicizzata in modo efficiente e una riga particolare può quindi essere localizzata rapidamente quando Hibernate cerca di modificarla o cancellarla.
Gli insiemi hanno una chiave primaria che consiste delle colonne <key> ed <element>. Questo può essere meno efficiente per alcuni tipi di elemento della collezione, in particolare per elementi composti o campi molto lunghi di testo o dati binari; il database può non essere in grado di indicizzare una chiave primaria complessa in maniera altrettanto efficiente che nel caso precedente. Da un altro punto di vista, per associazioni uno-a-molti o molti-a-molti, in particolare nel caso di identificatori sintetici, è probabile che sia efficiente nello stesso modo. (annotazione: se volete che SchemaExport crei davvero la chiave primaria di un <set> per voi, dovete dichiarare tutte le colonne come not-null="true".)
I "sacchi" (bags) sono il caso peggiore. Poiché un bag consente elementi duplicati e non ha una colonna indice, non può essere definita una chiave primaria. Hibernate non ha modo di distinguere tra righe duplicate, e quindi risolve il problema rimuovendo completamente (con una singola DELETE) e ricreando la collezione ogni volta che cambia. Questo tuttavia può essere molto inefficiente.
Notate che per una collezione uno-a-molti, la "chiave primaria" può non essere la chiave primaria fisica della tabella del database - ma anche in questo caso la classificazione qui sopra è comunque utile, poiché riflette come Hibernate recupera righe specifiche della collezione.
Dalla discussione di cui sopra, dovrebbe essere chiaro che le collezioni indicizzate e (di solito) gli insiemi consentono le operazioni più efficienti in termini di aggiunta, rimozione e modifica di elementi.
C'è un vantaggio ulteriore che le collezioni indicizzate hanno rispetto agli insiemi per le associazioni molti-a-molti o le collezioni di valori. Per come è fatta la struttura di un Set, Hibernate non aggiorna neppure (UPDATE) una riga, quando un elemento è "cambiato". I cambiamenti ad un Set funzionano semper via INSERT e DELETE (di righe individuali). Ancora una volta, ripetiamo che questa considerazione non si applica alle associazioni uno-a-molti.
Poiché ricordiamo che gli array non possono essere caricati a richiesta (lazy), concludiamo quindi che le liste, le mappe e gli insiemi sono i tipi di collezione più performanti. (Con l'avvertimento, ancora una volta, che un set può essere meno efficiente per alcune collezioni di valori)
Gli insiemi sono probabilmente il genere di collezione più comune nelle applicazioni basate su Hibernate.
C'è una funzionalità non documentata in questa versione di Hibernate. Il mappaggio <idbag> implementa una semantica a "bag" per una collezione di valori o una associazione molti-a-molti ed è più efficiente di qualsiasi altro stile di collezione, in questo caso!
Prima che buttiate via i "bag" per sempre, c'è un caso particolare in cui essi (e le liste) sono molto più performanti degli insiemi. Per una collezione con inverse="true" (l'idioma standard per una relazione uno-a-molti, ad esempio) possiamo aggiungere elementi ad un bag o una lista senza bisogno di inizializzare (fetch) gli elementi del bag stesso! Questo perché Collection.add() o Collection.addAll() devono sempre ritornare "true" per un bag o una List (a differenza di un Set). Questo può rendere il codice seguente molto più veloce:
Parent p = (Parent) sess.load(Parent.class, id); Child c = new Child(); c.setParent(p); p.getChildren().add(c); //no need to fetch the collection! sess.flush();
Di tanto in tanto, cancellare elementi di una collezione ad uno ad uno può essere estremamente inefficiente. Hibernate non è completamente stupido, per cui sa che non deve farlo nel caso in cui una collezione sia stata appena svuotata (tramite list.clear(), ad esempio). In questo caso, Hibernate utilizzerà una singola DELETE ed è tutto!
Supponiamo di aggiungere un elemento singolo ad una collezione di dimensione venti, e poi rimuovere due elementi. Hibernate lancerà una INSERT e due DELETE (a meno che la collezione sia un bag). Questo è certamente auspicabile.
Però, supponiamo di rimuovere diciotto elementi, lasciandone due, e poi di aggiungere tre elementi nuovi. Ci sono due modi possibili di procedere.
cancellare le diciotto righe una ad una e poi inserire le tre
rimuovere tutta la collezione in un solo comando DELETE e inserire tutti i cinque elementi rimanenti uno ad uno
Hibernate non è abbastanza furbo da sapere che la seconda opzione è probabilmente più veloce, in questo caso. (e probabilmente non sarebbe auspicabile che Hibernate lo fosse, perché un comportamento del genere può confondere dei trigger, ecc.)
Fortunatamente, potete imporre questo comportamento (cioè la seconda strategia) in ogni momento scartando (cioè dereferenziando) la collezione originale ed impostando una nuova collezione con tutti gli elementi che devono rimanere. Questo può essere molto utile e potente, in certi casi.
Abbiamo già mostrato come si può usare l'inizializzazione a richiesta (lazy) per le collezioni persistenti nel capitolo sui mappaggi delle collezioni. Un effetto simile si può ottenere per i riferimenti agli oggetti comuni, usando i mediatori (proxy) CGLIB. Abbiamo anche detto che Hibernate fa il caching degli oggetti persistenti al livello della Session. È comunque possibile impostare strategie di caching più aggressive per classi specifiche.
Nella prossima sezione, vi mostriamo come usare queste funzionalità, e quindi raggiungere prestazioni più elevate quando necessario.
Hibernate implementa un sistema per l'inizializzazione ritardata (lazy) degli oggetti persistenti tramite dei mediatori (proxy) creati in fase di esecuzione tramite una tecnica di arricchimento del codice binario (byte-code) che sfrutta le funzionalità fornite dall'eccellente libreria CGLIB.
Il file di mappaggio dichiara una classe o un'interfaccia che va usata come interfaccia del proxy per quella classe. L'approccio raccomandato è specificare la classe stessa:
<class name="eg.Order" proxy="eg.Order">
Il tipo dei proxy in fase di esecuzione sarà una sottoclasse di Order. Notate che la classe "mediata" (proxied) deve implementare un costruttore di default per lo meno con visibilità a livello di package.
Ci sono alcune peculiarità di cui essere a conoscenza, quando si estende questo approccio alle classi polimorfiche, ad esempio:
<class name="eg.Cat" proxy="eg.Cat"> ...... <subclass name="eg.DomesticCat" proxy="eg.DomesticCat"> ..... </subclass> </class>
Prima di tutto, le istanze di Cat non potranno essere oggetto di "cast" a DomesticCat, anche se l'istanza sottostante è effettivamente un DomesticCat.
Cat cat = (Cat) session.load(Cat.class, id); // instantiate a proxy (does not hit the db) if ( cat.isDomesticCat() ) { // hit the db to initialize the proxy DomesticCat dc = (DomesticCat) cat; // Error! .... }
In secondo luogo, è possibile che la semantica di == non valga per il proxy.
Cat cat = (Cat) session.load(Cat.class, id); // instantiate a Cat proxy DomesticCat dc = (DomesticCat) session.load(DomesticCat.class, id); // required new DomesticCat proxy! System.out.println(cat==dc); // false
Comunque, queste situazioni non sono poi così male come sembra. Anche se ora abbiamo due riferimenti diversi ad oggetti proxy, l'istanza sottostante è comunque la stessa:
cat.setWeight(11.0); // hit the db to initialize the proxy System.out.println( dc.getWeight() ); // 11.0
Terzo, non è possibile usare un mediatore CGLIB per una classe final o per una classe con metodi final.
Infine, se il vostro oggetto persistente acquisisce delle risorse in fase di istanziazione (ad esempio negli inizializzatori o nel costruttore di default), quelle risorse saranno acquisite anche dal proxy, poiché la classe del proxy è effettivamente una sottoclasse della classe persistente.
Questi problemi sono tutti derivanti da limitazioni di base nel modello a ereditarietà singola di Java. Se volete evitarli, le vostre classi persistenti devono implementare un'interfaccia che dichiari i loro metodi di business. Dovete poi specificare queste interfacce nel file di mapping, ad esempio così:
<class name="eg.Cat" proxy="eg.ICat"> ...... <subclass name="eg.DomesticCat" proxy="eg.IDomesticCat"> ..... </subclass> </class>
laddove Cat implementa l'interfaccia ICat e DomesticCat implementa l'interfaccia IDomesticCat. A questo punto, load() o iterate() possono restituire direttamente istanze di Cat e DomesticCat . (Notate che find() non restituisce mediatori.)
ICat cat = (ICat) session.load(Cat.class, catid); Iterator iter = session.iterate("from cat in class eg.Cat where cat.name='fritz'"); ICat fritz = (ICat) iter.next();
Anche le relazioni sono inizializzate in maniera ritardata. Questo significa che dovete dichiarare le proprietà di tipo ICat, e non Cat.
Alcune operazioni non richiedono inizializzazione del proxy
equals(), se la classe persistente non sovrascrive equals()
hashCode(), se la classe persistente non sovrascrive hashCode()
Il metodo "getter" per l'identificatore.
Hibernate individuerà le classi persistenti che sovrascrivono equals() o hashCode().
Le eccezioni che capitano quando si inizializza un proxy vengono racchiuse in una LazyInitializationException.
In alcuni casi, dobbiamo assicuarci che un mediatore o una collezione vengano inizializzati prima di chiudere la Session. Naturalmente, possiamo sempre forzare l'inizializzazione chiamando cat.getSex() o cat.getKittens().size(), ad esempio. Ma questo confonde chi legge il codice e non è pratico per del codice generico. I metodi statici Hibernate.initialize() e Hibernate.isInitialized() forniscono all'applicazione un modo comodo per lavorare con collezioni inizializzate a richiesta o con i mediatori. Hibernate.initialize(cat) imporrà l'inizializzazione di un mediatore cat, a condizione che la sua Session sia ancora aperta. Hibernate.initialize( cat.getKittens() ) ha un effetto simile per la collezione dei gattini (kitten ;) ).
Una Session di Hibernate è una cache di dati persistenti durante la transazione. È possibile configurare una cache a livello di cluster o a livello di macchina virtuale (JVM-level o SessionFactory-level) per classi o collezioni specifiche. È anche possibile agganciare (plug-in) una cache in cluster. Fate attenzione, tuttavia: le cache non sono mai coscienti di cambiamenti fatti ai dati sul contentitore fisico da un'altra applicazione (benché possano essere configurate in modo tale da fare scadere i dati conservati in memoria).
L'impostazione predefinita di Hibernate è di usare la libreria EHCache per il caching a livello di JVM (Il supporto di JCS è deprecato e verrà rimosso in una versione futura di Hibernate). È possibile scegliere una implementazione diversa speficicando il nome di una classe che implementi net.sf.hibernate.cache.CacheProvider usando la proprietà hibernate.cache.provider_class.
Tabella 14.1. Fornitori di cache
Cache | Classe fornitore | Tipo | Funziona in cluster | Supporta interrogazione della cache |
---|---|---|---|---|
Hashtable (non adatta per un uso in produzione) | net.sf.hibernate.cache.HashtableCacheProvider | memoria | sì | |
EHCache | net.sf.ehcache.hibernate.Provider | memoria, disco | sì | |
OSCache | net.sf.hibernate.cache.OSCacheProvider | memoria, disco | sì | |
SwarmCache | net.sf.hibernate.cache.SwarmCacheProvider | cluster (via ip multicast) | sì (invalidazione sul cluster) | |
JBoss TreeCache | net.sf.hibernate.cache.TreeCacheProvider | cluster (via ip multicast), transazionale | sì (replicazione) |
L'elemento <cache> per il mappaggio di una classe o di una collezione ha la forma seguente:
<cache
usage="transactional|read-write|nonstrict-read-write|read-only" (1)
/>
(1) | usage specifica la strategia di caching: transactional, read-write, nonstrict-read-write or read-only |
In alternativa (preferibilmente), si possono specificare gli elementi <class-cache> e <collection-cache> in hibernate.cfg.xml.
L'attributo usage speficica una strategia di concorrenza per la cache.
Se la vostra applicazione ha bisogno di leggere ma non modifica mai istanze di una classe persistente, si può usare una cache read-only (sola lettura). Si tratta della strategia più semplice e più performante. Funziona anche perfettamente in un cluster.
<class name="eg.Immutable" mutable="false"> <cache usage="read-only"/> .... </class>
Se l'applicazione deve modificare i dati, una cache read-write (lettura/scrittura) potrebbe essere appropriata. Questa strategia di caching non dovrebbe essere mai usata se è richiesto un livello di isolamento serializzabile delle transazioni. Se la cache è usata in un ambiente JTA, dovete specificare la proprietà hibernate.transaction.manager_lookup_class, indicando una strategia per ottenere il TransactionManager JTA. In altri ambienti, dovete assicurarvi che la transazione venga completata quando vengono chiamati Session.close() o Session.disconnect(). Se volete usare questa strategia in un cluster, dovete assicurarvi che l'implementazione della cache sottostante supporti il locking. La cache fornita con Hibernate non lo fa.
<class name="eg.Cat" .... > <cache usage="read-write"/> .... <set name="kittens" ... > <cache usage="read-write"/> .... </set> </class>
Se l'applicazione ha bisogno di modificare dati solo occasionalmente (cioè se è molto improbabile che due transazioni tentino di modificare lo stesso oggetto simultaneamente) e l'isolamento stretto delle transazioni non è richiesto, potrebbe essere appropriata una cache nonstrict-read-write (lettura/scrittura non stretta). Se la cache è usata in un ambiente JTA, dovete specificare hibernate.transaction.manager_lookup_class. In altri ambienti, dovete assicurare che la transazione sia completa quando vengono chiamati Session.close() o Session.disconnect().
La strategia di caching transazionale fornisce supporto per cache completamente transazionali come la JBoss TreeCache. Una cache di questo tipo può essere usata solo in un contesto JTA e dovete specificare la proprietà hibernate.transaction.manager_lookup_class.
Nessuno dei fornitori di cache supporta tutte le strategie di concorrenza. La tabella seguente mostra quali fornitori sono compatibili con quali strategie di concorrenza.
Ogni volta che passate un oggetto ai metodisave(), update() o saveOrUpdate() e ogni volta che ne recuperate uno usando load(), find(), iterate(), o filter(), quell'oggetto viene aggiunto alla cache interna della Session. Quando poi viene chiamato flush(), lo stato di quell'oggetto sarà sincronizzato con il database. Se non volete che questa sincronizzazione avvenga, o se state elaborando un grande numero di oggetti e volete gestire la memoria efficentemente, potete usare il metodo evict() per rimuovere l'oggetto e le sue collezioni dalla cache.
Iterator cats = sess.iterate("from eg.Cat as cat"); //a huge result set while ( cats.hasNext() ) { Cat cat = (Cat) iter.next(); doSomethingWithACat(cat); sess.evict(cat); }
La Session fornisce anche un metodo contains() per determinare se un'istanza appartiene alla cache di sessione.
Per rimuovere completamente tutti gli oggetti dalla cache si sessione, esiste il metodo Session.clear()
Per la cache di secfondo livello, ci sono dei metodi definiti su SessionFactory e che hanno lo scopo di rimuovere lo stato di un'istanza dalla cache, una intera classe, una istanza di collezione o un intero ruolo di collezione.
Gli insiemi di risultati (result set) delle query possono anche venire messi in cache. Questo è utile solo per quelle query che vengono lanciate frequentemente con gli stessi parametri. Per usare la cache delle query dovete prima attivarla settando la proprietà hibernate.cache.use_query_cache=true. Questo causa la creazione di due regioni nella cache, una che mantiene i set di risultati delle query, (net.sf.hibernate.cache.QueryCache), l'altra che mantiene le etichette di tempo (timestamp) degli aggiornamenti più recenti alle tabelle interrogate. (net.sf.hibernate.cache.UpdateTimestampsCache). Notate che la cache delle query non memorizza lo stato delle entità nel result set; quello che mette in cache sono solo i valori dei risultati e i valori dei tipi. Per questo, la cache delle query viene solitamente usata insieme alla cache di secondo livello.
La maggior parte delle interrogazione non traggono particolari benefici dal caching, per questo l'impostazione predefinita non lo prevede. Per attivarlo, chiamate Query.setCacheable(true). Questo metodo consente alla query di cercare risultati nella cache o di aggiungere i suoi risultati quando viene eseguita.
Se avete bisogno di controllo più raffinato sulle politiche di scadenza delle cache, potete specificare una regione della cache per nome e per una particolare interrogazione chiamando il metodo Query.setCacheRegion().
List blogs = sess.createQuery("from Blog blog where blog.blogger = :blogger") .setEntity("blogger", blogger) .setMaxResults(15) .setCacheable(true) .setCacheRegion("frontpages") .list();