Una transazione è un'insieme di operazioni che deve essere portata a termine nella sua interezza, oppure, nel caso di errori, deve prevedere il ritorno alle condizioni di partenza. Le transazioni sono fondamentali, in quanto attraverso il loro utilizzo viene garantita la consistenza dei sistemi, delle basi di dati e di tutte le informazioni che hanno un certo grado di sensibilità.
Per comprenderne meglio l'importanza vediamo un esempio di transazione. Senza dubbio ognuno di noi ha familiarità con le operazioni bancarie: il passaggio di denaro da un conto corrente ad un'altro è una transazione.
Se ci pensiamo bene, questa operazione, è composta da due attività: prelievo dal conto corrente x e deposito sul conto corrente y. Cosa accadrebbe se ci fosse un errore (guasto, errore di rete, caduta di tensione, ecc) subito dopo aver eseguito l'operazione di prelievo? Il sistema sarebbe in uno stato inconsistente, e l'utente x non molto felice dell'accaduto.
Per prevenire questo genere di cose dovremmo preoccuparci di effettuare delle operazioni di ripristino (a ritroso) a partire dal punto in cui si è verificato l'errore. Questi problemi ce li risolve il middleware con le transazioni. Attraverso le transazioni le operazioni di recupero sono automatizzate dal middleware, che si occupa di avviare una procedura di registrazione, all'inizio della transazione, per annotare tutte le operazioni svolte. Nel caso avvenga un'errore (qualsiasi), c'è il ritorno allo stato iniziale, con la garanzia della consistenza del sistema.
Spesso ci si riferisce alle transazioni come operazioni che seguono il paradigma ACID:
- Atomicity: le operazioni coinvolte in una transazione sono viste come singola (atomica) unità di lavoro;
- Consistency: garanzia che il sistema si trovi in uno stato di consistenza, anche in seguito ad un errore;
- Isolation: senza possibilità di accedere concorrentemente allo stesso insieme di dati coinvolti nella transazione;
- Durability: garanzia che i dati verranno mantenuti anche in seguito ad un crash di sistema.
Ogni application server garantisce queste proprietà generalmente prevedendo un modello transazionale flat (tutte le operazioni o nessuna). Interessante è il modello transazionale nested (ad albero) la cui implementazione è comunque complessa.
Due sono i modelli di sviluppo previsti dalla tecnologia J2EE per lo sviluppo delle transazioni: in maniera programmatica e in maniera dichiarativa. Semplicemente, nel primo caso, sarà cura dello sviluppatore occuparsi della gestione (programmandone il codice), nel secondo, invece, lo sviluppatore delega all'application server questo compito attraverso i descrittori di deploy.
Il secondo modo è quello che abbiamo implicitamente utilizzato fino ad adesso. Rivedendo i bean sviluppati nelle precedenti lezioni, nel descrittore di deploy possiamo identificare il tag che definiva la tipologia di transazione da utilizzare:
..//
<ejb-class>it.html.ejb.session.stateful.ShoppingCartSession</ejb-class>
<session-type>Stateful</session-type>
<transaction-type>Container</transaction-type>
</session>
..//
In questo modo abbiamo dichiarato che la gestione delle transazioni deve essere delegata al container. In particolare, omettendo ulteriori informazioni, diciamo al container di creare una nuova transazione per ogni metodo.
Ogni metodo è visto come una transazione. In molti casi però tutto ciò non è necessario. È possibile che le transazioni non servano, quindi potremmo notificare quest'informazione al container ed ottimizzare le risorse (evitando i controlli che altrimenti il container farebbe).
<container-transaction>
<method>
<ejb-name>BankAccount</ejb-name>
<method-name>*</method-name>
</method>
<trans-attribute>Required</trans-attribute>
</container-transaction>
Il codice precedente esplicita che per tutti i metodi del bean BankAccount è richiesta la presenza di una transazione. Volendo avremmo potuto definire il comportamento di ogni metodo con l'attributo necessario. Gli attributi sono required
, requiresNew
, supports
, mandatory
, notsupported
, never
. Il comportamento della prima, in pratica, fa sì che la transazione viene creata, se non ne esiste in atto alcuna, oppure, se esiste, essa si unisce a quella esistente (mantenendone quindi aperta solo una).
È evidente che l'utilizzo dichiarativo sia uno strumento più facile ma, laddove è necessario un livello di controllo più sensibile, è possibile sviluppare le transazioni in maniera programmatica (cosa che vedremo nella prossima lezione).