MokaByte Numero
31 - Giugno 1999
|
|||
|
|
|
|
|
Roberto Bagnoli |
|
|
Nell'articolo precedente sono stati introdotti alcuni concetti teorici sulle transazioni, validi in generale, indipendentemente dal DBMS e dal linguaggio di programmazione utilizzato. In questa seconda parte si cercherà di analizzare l'API JDBC alla ricerca delle primitive per la gestione delle transazioni. |
Supporto per le transazioniIn JDBC tutti i servizi di supporto alla gestione delle transazioni sono concentrati a livello dell'interfaccia java.sql.Connection. (Per chi avesse dei dubbi riguardo la creazione ed il funzionamento delle connessioni JDBC può utilizzare la breve appendice in coda a questo articolo, mentre per una spiegazione più approfondita sull'uso del JDBC si può far riferimento agli altri contributi sull'argomento apparsi nei precedenti numeri di MokaByte).Dando uno sguado alla documentazione dell'interfaccia Connection fornita a corredo con il JDK 1.1.x, si possono individuare i seguenti metodi: public void setAutoCommit(boolean autoCommit) throws SQLException I metodi setAutoCommit() e getAutoCommit()La specifica JDBC prevede che per default un oggetto di tipo Connection deve essere posto nello stato di auto-commit. Questo significa che una connessione esegue automaticamente un operazione di commit al termine dell'esecuzione di ogni statement SQL (in realtà il momento in cui viene effettivamente terminata la transazione viene deciso in modo un po' più complesso, ma la sostanza non cambia). In altre parole, per default un oggetto di tipo Connection non permette di racchiudere più comandi SQL all'interno di un unica transazione: ogni comando è una transazione a sè.Il metodo setAutoCommit() permette di modificare tale comportamento. Esso riceve un parametro booleano:
I metodi commit() e rollback()Una volta che l'oggetto Connection è stato posto in modalità non auto-commit, si può cominciare a realizzare transazioni complesse, composte da un numero generico di comandi SQL. Ovviamente, non occupandosene più la connessione in modo automatico, sarà compito del programmatore decidere quando una transazione deve essere terminata con successo oppure quando deve essere abortita. Ma non è proprio questo che si voleva?Nel seguente spezzone di codice (assolutamente insignificante e privo di una qualsiasi utilità pratica! :-) ) si può vedere un esempio d'uso dei metodi commit() e rollback(): try{
I metodi setTransactionIsolation() e getTransactionIsolation()Come spiegato nella prima parte di questo articolo, sono possibili diversi livelli di isolamento per le transazioni, ognuno dei quali permette di prevenire alcune o tutte le anomalie che possono presentarsi a seguito dell'esecuzione concorrente di più transazioni, vale a dire:
Occorre ora ricordare che passando dal livello più basso di isolamento a quello più alto aumenta di conseguenza il numero di risorse che il DBMS deve impiegare per evitare le anomalie dovute alla concorrenza, ed in particolare aumenta il numero di lock che una connessione deve acquisire per portare a termine una transazione. Tutto ciò ha ripercussioni dirette sulle prestazioni e sul grado di concorrenza ottenibile da un applicazione, quindi non è strano che alcuni DBMS forniscano la possibilità di stabilire di volta in volta i livello di isolamento desiderato per una transazione. A livello dell'API JDBC tutto questo si traduce nell'avere a disposizione due metodi (un setter e un getter) per impostare il livello di isolamento. In particolare
Il metodo getTransactionIsolation() è il duale del precedente e permette di determinare il livello di isolamento transazionale della connessione. Non riceve alcun parametro e ritorna un intero corrispondente ad una delle costanti descritte sopra. Determinare le caratteristiche del DBMS: il metodo getMetaData() e l'interfaccia DataBaseMetaData
Il lock ottimisticoSi supponga ora di dover realizzare un'applicazione gestionale in multiutenza con accesso a database. In applicazioni di questo tipo è molto frequente il caso in cui un utente accede ad un record di una tabella del database, ne controlla il contenuto e successivamente decide se apportare delle modifiche, cancellarlo oppure aggiungere e/o cancellare record ad altre tabelle correlate.Forti delle conoscenze acquiste sulle API JDBC e avendo a disposizione un DBMS che lo permette, si decide di implementare le funzionalità applicative usando le transazioni al loro massimo livello di isolamento. Affichè tutto funzioni nel modo corretto ogni interazione utente-applicazione dovrebbe essere costruita secondo il seguente schema:
Ora, non vi sarebbe nulla di sbagliato, almeno dal punto di vista logico, nella scelta di progetto abbozzata: l'operatività di ogni utente risulterebbe completamente isolata da quella di tutti gli altri proprio perchè tutto il codice di accesso al database sarebbe sempre racchiuso all'interno di una transazione al massimo livello di isolamento. Si avrebbe quindi l'innegabile vantaggio di poter scrivere i programmi come se fossero delle semplici applicazioni monoutente, semplicemente ricordandosi di "aprire" e "chiudere" le transazioni al momento giusto. In realtà, una soluzione di questo tipo non viene quasi mai impiegata perchè può avere conseguenze nefaste a tempo di esecuzione: un utente che accede ad un record di un database ne causa implicitamente il lock, e questo verrà rilasciato solo al termine della transazione. Cosa succede se un operatore attiva una form di accesso ad un record e prima di confermare o annullare le modifiche decide che è ora di andare a fare colazione? Semplice: quel record, al quale si è acceduto dall'interno di una transazione, rimarrà bloccato e nessun altro vi potrà accedere fino a quando l'utente non deciderà di rientrare dalla pausa! In realtà, per causare pasticci di questo tipo non è necessario aspettare l'ora di pranzo, in quanto non è raro che in un applicazione gestionale trascorra un apprezzabile lasso di tempo tra l'acquisizione di un record e la sua effettiva modifica: l'utente potrebbe aver bisogno di controllare diversi documenti cartacei prima di confermare le modifiche, oppure potrebbe essere semplicemente chiamato al telefono. Un altro caso molto importante è quello delle applicazioni con interfaccia Web, dove non è raro che un utente inizi una transazione e nel bel mezzo di essa decide che si è stufato e chiude il browser. Se a tutto questo si aggiunge che non è raro che un'applicazione multiutente di un certo rilievo debba poter servire diverse decine (se non centinaia o migliaia) di utenti contemporaneamente, si capisce bene che non è da trascurare la probabilità di avere carenze di risorse sul DBMS (con conseguente degrado delle prestazioni) oppure contesa di record tra client. Qual'è dunque la soluzione? Sembra strano a dirsi, ma l'unico rimedio consiste nell'evitare l'uso delle transazioni, o meglio, nel cercare di mantenerle aperte per il più breve intervallo di tempo possibile. A tal fine si adotta la tecnica del lock ottimistico, la quale si basa sull'assunto che, se si considera il rapporto tra numero totale di accessi al database e numero totale di anomalie, queste ultime non si presentano poi così di frequente. Il principio di funzionamento del lock ottimistico può essere compreso osservando le modifiche che è necessario applicare allo schema di funzionalità abbozzato sopra, il quale si trasforma in questo modo:
Ora l'utente è libero di andarsene a pranzo senza preoccuparsi (se mai lo fosse stato!) di bloccare il lavoro di altri: al suo ritorno la copia del record eventualmente visualizzata sul suo client potrebbe non essere più allineata con quella presente sul database perchè altri potrebbero averlo nel frattempo modificato e se tentasse di modificarlo e di salvarlo, riceverebbe un errore da parte dell'applicazione. Tutto questo ovviamente si paga! Si è infatti costretti a mantenere una copia del contenuto originale del record. Inoltre, ogni transazione necessita sempre di due letture dello stesso record (la prima per acquisirlo, la seconda per il controllo delle eventuali anomalie). Vi è comunque da considerare che, proprio grazie al fatto che è l'applicazione ad eseguire i controlli necessari ad evitarle, le anomalie read-write e write-read non devono essere gestite dal database, quindi si può (anzi, si deve!) abbassare il livello di isolamento transazionale del database, portandolo quindi a READ_COMMITED (quando possibile). In caso contrario dall'impiego del lock ottimistico non si avrebbe alcun guadagno in termini di prestazioni e livello di concorrenza, ma anzi, si avrebbe un degrado prestazionale dovuto alle doppie lettura necessarie al completamento di ogni transazione. La tecnica del timestampCome si è detto, il principio sul quale si fonda il lock ottimistico consiste nell'assicurarsi, prima dell'effettivo aggiornamento, che il record posseduto dall'applicazione non sia stato modificato sul database in un momento successivo alla sua acquisizione. Se questo controllo dovesse essere fatto tutte le volte su ogni campo che compone un record, l'overhead sarebbe veramente inaccettabile, tantopiù in presenza di campi contenenti dei large object. Per ovviare a tale inconveniente, si adotta la tecnica del timestamp, la quale consiste nell'inserire in ogni record un campo aggiuntivo di tipo timestamp, il cui unico scopo sia quello di mantenere traccia dell'istante in cui è avvenuto l'ultimo aggiornamento di un record.In questo modo, se ad ogni aggiornamento di un record sul database corrisponde anche l'aggiornamento del corripondente campo timestamp, si avrà a disposizione una marcatura sulla quale basarsi implementare il lock ottimistico. Ecco allora la versione definitiva dello schema di funzionalità applicative che implementa il lock ottimistico con la tecnica del timestamp:
ConclusioniMi pare di poter affermare che l'API JDBC sia davvero molto semplice da utilizzare, ma questa semplicità non deve trarre in inganno: realizzare applicazioni robuste significa adottare opportuni accorgimenti in fase di design che vanno al di là del semplice impiego di una libreria di classi.La gestione delle transazioni è uno di quegli argomenti che non può certo esaurirsi nella semplice elencazione delle classi e dei metodi che Java mette a disposizione per utilizzarle. In questo articolo si è quindi cercato di fornire anche una breve panoramica dei problemi che devono essere considerati ed affrontati quando si deve scrivere un'applicazione con accesso a database che debba supportare la multiutenza. In realtà la questione è ancora più complessa, in quanto vi è un intrinseca incompatibilità tra mondo ad oggetti e mondo relazionale la quale, per essere compensata, richiede un non trascurabile sforzo di analisi e design. Object-relational mapping, Enterprise Java Beans sono nomi che per alcuni possono non essere famigliari e che purtroppo non possono essere spiegati in un solo articolo, ma costituiscono un chiaro segnale che per ottenere applicazioni robuste e scalabili occorre molto di più di una semplice (ma potente!) API per l'accesso ai database. APPENDICE - La creazione delle connessioni JDBCIn questa appendice dell'articolo si entrerà nei dettagli relativi alla creazione di una connessione JDBC, in quanto tale argomento può essere utile alla comprensione del supporto delle transazioni, il quale viene fornito da JDBC proprio a livello delle connessioni.I driver JDBC L'API JDBC, racchiusa nel package java.sql.*, fornisce un'interfaccia unificata per l'accesso ad un qualunque database, "mascherando" le peculiarità di ogni singolo DBMS introducendo il concetto di driver. Per capire meglio cosa questo significhi basta dare un'occhiata alla documentazione del pacchetto java.sql: ci si accorge immediatamente che la maggior parte delle funzionalità vengono fornite tramite interfacce (cioè tipi puramente astratti), mentre le classi vere e proprie sono davvero poche. Cosa implica tutto ciò? Semplicemente, l'API JDBC si limita in gran parte a dichiarare le funzionalità che un'interfaccia generalizzata per l'accesso ad un database dovrebbe avere, delegando l'implementazione delle interfacce ai driver che devono fornire l'accesso ai singoli DBMS. Quindi, compito di un driver JDBC è fornire un insieme di classi che implementino tutte le interfacce dichiarate nel pacchetto java.sql. Ora, si supponga
che una certa classe Java debba accedere ad un database la cui istanza
è chiamata MioDb gestito dal DBMS IperDBdella software
house PincoPallo. Per poter eseguire una qualunque operazione su
un database occorre per prima cosa ottenere una connessione ad esso: in
termini JDBC ciò equivale a richiedere alla classe java.sql.DriverManager
un oggetto istanza di una classe che implementa l'interfaccia java.sql.Connection
import java.sql.*;
La registrazione dei driver JDBC Le specifiche
JDBC redatte dalla Sun prevedono che, per poter essere utilizzato, un driver
JDBC deve essere caricato in memoria, e una volta che ciò è
avvenuto, il driver stesso ha il compito di registrarsi presso il Driver
Manager (d'ora in avanti solamente DM), il quale ha quindi il compito di
tenere traccia dei driver JDBC disponibili, in modo da poter costruire
correttamente le istanze delle classi che implementano l'interfaccia java.sql.Connection
quando queste vengono richieste dalle applicazioni.
public MiaClasse{
Gli URL JDBCLe chiavi subprotocol registrate dai driver vengono utilizzate all'interno degli URL JDBC, passati dalle applicazioni al DM all'atto della richiesta di una connessione. Un JDBC URL deve rispettare la seguente sintassi:jdbc:<subprotocol>:<subname>
//<host>:<port>/<dbname>
import java.sql.*;
|
|
||
|
||
MokaByte ricerca
nuovi collaboratori
|
||
|