Capitolo 10. Transazioni e concorrenza

Hibernate non è in se stesso un database. È uno strumento leggero di mappaggio oggetto-relazione. La gestione delle transazioni viene delegata alla sottostante connessione con il database. Se la connessione è iscritta con il JTA, le operazioni effettuate dalla Session sono atomicamente parte della transazione JTA più esterna. Hibernate può essere considerato un sottile strato di adattamento sul JDBC che aggiunge la semantica orientata agli oggetti.

10.1. Configurazioni, sessioni e "factory"

Una SessionFactory è un oggetto che supporta l'utilizzo concorrente (threadsafe), costoso da creare, che è pensato per essere condiviso da tutti i thread dell'applicazione. Una Session è invece un oggetto non costoso da crearsi, non utilizzabile in maniera concorrente, che dovrebbe essere usato una volta sola per un singolo processo di business e poi scartato. Ad esempio, quando si usa Hibernate in un applicazione basata sui servlet, i servlet possono ottenere una SessionFactory usando

SessionFactory sf = (SessionFactory)getServletContext().getAttribute("my.session.factory");

Ogni chiamata ad un metodo di servizio potrebbe creare una nuova Session, farci sopra il flush() (scaricamento su db) , mandare un commit() sulla sua connesione, chiuderla (close()) ed infine eliminarla. (La SessionFactory può anche essere memorizzata nel JNDI o in una variabile di utilità Singleton.)

In un "session bean" senza stato si può usare un approccio simile. Il bean dovrebbe ottenere una SessionFactory con il metodo setSessionContext(). A questo punto, ogni metodo di business dovrebbe creare una Session, fare il flush() e chiuderla (close()). Naturalmente, l'applicazione non dovrebbe chiamare commit() sulla connessione. (Va lasciata al JTA, perché la connessione al database partecipa automaticamente nelle transazioni gestite dal contenitore.)

Usiamo l'API Transaction di Hibernate come discusso in precedenza, una singola commit() di una Transaction di Hibernate scarica lo stato e fa il commit di ogni connessione di database sottostante (con una gesione particolare delle transazioni JTA).

Assicuratevi di capire la semantica del flush(). Lo scaricamento (flushing) sincronizza il contenitore persistente con i cambiameti in memoria, ma non vice-versa. Notate che per tutte le connessioni/transazioni JDBC di Hibernate, il livello di isolamento transazionale si applica a tutte le operazioni che vengono eseguite da Hibernate stesso!

Le prossime sezioni discuteranno gli approcci alternativi che usano il versionamento per assicurare l'atomicità delle transazioni. Sono approcci che vengono considerati "avanzati" e vanno usati con attenzione.

10.2. Thread e connessioni

Dovreste osservare le indicazioni seguenti quando create Session di Hibernate:

  • Non creare più di una istanza di Session o Transaction concorrenti per connessione di database.

  • Siate estremamente attenti quando create più di una Session per database per transazione. La Session mantiene traccia di aggiornamenti fatti agli oggetti caricati, e quindi una Session differente potrebbe vedere dati non più validi.

  • La Session non è threadsafe (non consente più utilizzi concorrenti)! Non accedete alla stessa Session in due thread di esecuzione concorrenti. Una Session di solito è una singola unità di lavoro!

10.3. Considerazioni sull'identità degli oggetti

L'applicazione può accedere concorrentemente allo stesso stato persistente in due differenti unità di lavoro. Però, un'istanza di una classe persistente non viene mai condivisa tra due istanze di Session. Da qui, discendono due differenti nozioni di identità:

Identità per il database

foo.getId().equals( bar.getId() )

Identità per la JVM (java virtual machine)

foo==bar

Per due oggetti appartenenti ad una particulare Session, le due nozioni sono equivalenti. Però, mentre l'applicazione potrebbe accedere in maniera concorrente lo "stesso" (secondo l'identità persistente) oggetto di business in due sessioni differenti, le due istanze sono in realtà "differenti" (secondo l'identità della virtual machine).

Questo approccio fa sì che siano Hibernate e il database, a preoccuparsi della concorrenza. L'applicazione non deve mai sincronizzare l'accesso ad un oggetto di business, finché rispetta il fatto che l'accesso alla Session venga fatto da un singolo thread o le regole sull'identità degli oggetti (all'interno di una Session l'applicazione può tranquillamente utilizzare == per confrontare gli oggetti).

10.4. Controllo di concorrenza ottimistico

Molti processi di business richiedono una serie di interazioni con l'utente inframmezzate da accessi al database. Nelle applicazioni web e aziendali non è accettabile che una transazione sul database si estenda lungo una serie di interazioni con l'utente.

Mantenere l'isolamento dei processi di business in questi casi diventa una responsabilità parziale dello strato applicativo, ed in questo caso si dice che questo processo è una transazione applicativa di lunga durata. Una singola transazione applicativa di solito si estende su diverse transazioni sul database: essa sarà atomica se una sola di queste transazioni sul database (l'ultima) memorizza i dati aggiornati, e le altre semplicemente li leggono.

L'unico approccio che è consistente con alta concorrenza e alta scalabilità è il controllo di concorrenza ottimistico con versionamento. Hibernate fornisce tre possibili approcci alla produzione di codice applicativo che utilizzi la concorrenza ottimistica.

10.4.1. Sessione lunga con versionamento automatico

Una singola istanza di Session e gli oggetti persistenti che gestisce sono utilizzate per tutta la transazione applicativa.

La Session utilizza il locking ottimistico con versionamento per assicurarsi che molte transazioni sul database appaiano all'applicazione come una singola transazione applicativa logica. La Session è disconnessa dalla connessione JDBC mentre aspetta l'interazione con l'utente. Questo approccio è il più efficiente in termini di accesso al database. L'applicazione non deve preoccuparsi con il controllo delle versioni o con il riaggancio alla sessione delle istanze sganciate.

// foo è un'istanza caricata precedentemente dalla Session
session.reconnect();
foo.setProperty("bar");
session.flush();
session.connection().commit();
session.disconnect();

L'oggetto foo sa ancora da quale Session era stata caricato. Non appena la Session avrà una connessione JDBC verrà fatto il commit dei cambiamenti sull'oggetto.

Questo pattern è problematico se la Session è troppo grande per essere memorizzata durante il tempo di ragionamento dell'utente, ad esempio una HttpSession dovrebbe essere mantenuta il più ridotta possibile. Poiché la Session è anche la cache di primo livello (obbligatoria) e contiene tutti gli oggetti che ha caricato, possiamo probabilmente utilizzare questa strategia solo per pochi cicli di richiesta e risposta. Questo è in realtà raccomandato anche perché la Session avrebbe presto dati scaduti, in caso contrario.

10.4.2. Sessioni multiple con versionamento automatico

Ogni interazione con il contenitore persistente dei dati avviene in una nuova Session. Però, le stesse istanze persistenti vengono riutilizzate per ogni interazione con il database. L'applicazione manipola lo stato delle istanze sganciate originariamente caricate in un'altra Session e quindi le "riassocia" usando Session.update() o Session.saveOrUpdate().

// foo è una istanza caricata da una Session precedente
foo.setProperty("bar");
session = factory.openSession();
session.saveOrUpdate(foo);
session.flush();
session.connection().commit();
session.close();

È anche possibile chiamare lock() invece di update() e usare LockMode.READ (effettua un controllo di versione e aggira tutte le cache) se si è sicuri che l'oggetto non è stato modificato.

10.4.3. Controllo delle versioni da parte dell'applicazione

Ogni interazione con il database avviene in una nuova Session che ricarica tutte le istanze persistenti prima di manipolarle. Questo approccio obbliga l'applicazione a gestire in proprio il controllo delle versioni per assicurarsi che le transazioni applicative siano isolate. (Naturalmente Hibernate aggiornerà ancora i numeri di versione per voi). Questo approccio è il meno efficiente in termini di accesso al database, ed è il più simile a quello degli EJB di entità.

// foo è un'istanza caricata da una Session precedente
session = factory.openSession();
int oldVersion = foo.getVersion();
session.load( foo, foo.getKey() );
if ( oldVersion!=foo.getVersion ) throw new StaleObjectStateException();
foo.setProperty("bar");
session.flush();
session.connection().commit();
session.close();

Naturalmente, se state lavorando in un ambiente a bassa concorrenza dei dati e non avete bisogno di controllo delle versioni, potete adottare questo approccio e semplicemente evitare il controllo di versione.

10.5. Disconnessione della sessione

Il primo approccio descritto sopra è di mantenere una singola Session che si estende per un intero processo di business durante il periodo di ragionamento dell'utente. (Ad esempio, un servlet potrebbe mantenere una Session nella HttpSession dell'utente.) Per ragioni di performance si dovrebbe

  1. fare il commit della Transaction (o della connessione JDBC) e poi

  2. sconnettere la Session dalla connessione JDBC

prima di aspettare un'azione da parte dell'utente. Il metodo Session.disconnect() sconnetterà la sessione dalla connessione JDBC e restituirà la connessione al lotto di connessioni disponibili per l'uso (a meno che non siate stati voi a fornirla direttamente).

Session.reconnect() ottiene una nuova connessione (o potete fornirne una voi) e fa ripartire la sessione. Dopo la riconnessione, è possibile chiamare Session.lock() per forzare un controllo di versione sui dati che non sono stati modificati ma che potrebbero essere stati aggiornati da un'altra transazione. Non avete bisogno di porre dei "lock" su dati che state modificando.

Ecco un esempio:

SessionFactory sessions;
List fooList;
Bar bar;
....
Session s = sessions.openSession();

Transaction tx = null;
try {
    tx = s.beginTransaction();

    fooList = s.find(
    	"select foo from eg.Foo foo where foo.Date = current date"
        // uses db2 date function
    );
    bar = (Bar) s.create(Bar.class);

    tx.commit();
}
catch (Exception e) {
    if (tx!=null) tx.rollback();
    s.close();
    throw e;
}
s.disconnect();

In seguito:

s.reconnect();

try {
    tx = s.beginTransaction();

    bar.setFooTable( new HashMap() );
    Iterator iter = fooList.iterator();
    while ( iter.hasNext() ) {
        Foo foo = (Foo) iter.next();
        s.lock(foo, LockMode.READ);    //controlliamo che foo non sia scaduto
        bar.getFooTable().put( foo.getName(), foo );
    }

    tx.commit();
}
catch (Exception e) {
    if (tx!=null) tx.rollback();
    throw e;
}
finally {
    s.close();
}

Potete vedere da quanto precede che la relazione tra Transactioni e Sessioni è molti-a-uno. Una Session rappresenta una conversazione tra l'applicazione e il database. La Transaction spezza quella conversazione in unità di lavoro atomiche al livello del database.

10.6. Locking Pessimistico

Gli utenti non devono spendere molto tempo preoccupandosi delle strategie di locking. Solitamente è sufficiente specificare un livello di isolamento per le connessioni JDBC e poi semplicemente fare in modo che il database faccia tutto il lavoro. Però, gli utenti avanzati possono desiderare a volte di ottenere lock pessimistici esclusivi, o riottenere dei lock all'inizio di una nuova transazione.

Hibernate userà sempre il meccanismo di lock del database, e non porrà mai dei lock sugli oggetti in memoria!

La classe LockMode definisce i differenti livelli di lock che possono essere acquisiti da Hibernate. Un lock si può ottenere con i meccanismi seguenti:

  • LockMode.WRITE viene assunto automaticamente quando Hibernate modifica o inserisce una riga.

  • LockMode.UPGRADE può essere acquisito in seguito ad una richiesta esplicita dell'utente utilizzando SELECT ... FOR UPDATE su dei database che supportino questa sintassi.

  • LockMode.UPGRADE_NOWAIT può essere acquisito in seguito ad una richiesta esplicita dell'utente usando SELECT ... FOR UPDATE NOWAIT in Oracle.

  • LockMode.READ viene acquisito automaticamente quando Hibernate legge dati a livello di isolamento pari a "Repeatable Read" (letture ripetibili) o "Serializable". Può essere acquisito anche per esplicita richiesta dell'utente.

  • LockMode.NONE rappresenta una situazione di assenza di lock. Tutti gli oggetti si portano in questa modalità di lock alla fine di una Transaction. Gli oggetti associati con la session tramite una chiamata a update() o saveOrUpdate() vengono avviati in questa modalità.

La "richiesta esplicita dell'utente" viene espressa in una delle modalità seguenti:

  • Una chiamata a Session.load(), specificando un LockMode.

  • Una chiamata a Session.lock().

  • Una chiamata a Query.setLockMode().

Se si chiama Session.load() con UPGRADE o UPGRADE_NOWAIT, e l'oggetto richiesto non era ancora stato caricato dalla sessione, l'oggetto viene caricato usando SELECT ... FOR UPDATE. Se si chiama load() per un oggetto che è già stato caricato con un lock meno restrittivo di quello che è stato richiesto, Hibernate chiama lock() per quell'oggetto.

Session.lock() effettua un controllo del numero di versione se la modalità di lock è READ, UPGRADE o UPGRADE_NOWAIT. (Nel caso di UPGRADE o UPGRADE_NOWAIT, viene usato SELECT ... FOR UPDATE.)

Se il database non supporta la modalità di lock richiesta, Hibernate userà la modalità alternativa più appropriata (invece di lanciare un'eccezione). Questo fa sì che le applicazioni risultino portabili.