6. Gestione del Backend

E' molto raro che un web service non abbia bisogno di memorizzare informazioni in una base dati. Nel progetto d'esame è possibile utilizzare un qualunque database relazionale che supporti accesso tramite l'interfaccia JDBC di Java. Nel seguito, facendo riferimento all'uso del DB PostgreSQL (http://www.postgresql.org), mostriamo come predisporre la nostra applicazione per memorizzare le informazioni relative agli ordini ricevuti dal cliente e come tenere aggiornato il magazzino merce in modo tale da verificare le disponibilità.

La prima operazione da svolgere riguarda la creazione e l'inizializazione delle tabelle necessarie alla nostra applicazione. Esempi completi per l'esecuzione di queste operazioni in ambiente erby o postgresql sono analizzati nell'esercitazione sull'uso dei database.

Il passo successivo riguarda la capacità di agire programmaticamente sui dati. Nel progetto vi viene richiesto di programmare usando le API JDBC, lo standard java per l'esecuzione di comandi SQL. Per l'uso di JDBC nel progetto si rimanda alla lezione di introduzione all'uso di JDBC ed alle esercitazioni su DB.

6.1. SQL Injection

Molta attenzione va posta in fase di programmazione ai rischi di introdurre nel proprio codice vulnerabilità agli attacchi di SQL Injection, che permettono all'attaccante di modificare le query inviate dall'applicazione al DB.

Di solito queste vulnerabilità sono introdotte da un uso improprio delle istruzioni di tipo Statement, come discusso nella lezione di introduzione all'uso di JDBC. Per questo motivo nel progetto d'esame è richiesto di utilizzare PreparedStatement anzichà Statement in tutte le situazioni soggette a questo tipo di problema.

La differenza sostanziale sta nel fatto che la Statement prende il comando SQL come stringa o come concatenazione di stringhe, mentre la PreparedStatement viene inizializzata con un template SQL parametrico, cosicchè l'inserimento di parametri nell'istruzione SQL avviene tramite metodi spoecifici (setString, setInteger, ...) che garantiscono la verifica del contenuto, e l'escape delle stringhe di input utente che potrebbero causare la modifica dell'istruzione SQL originale.

Esempi specifici sono analizzati nell'esercitazione su SQL Injection.

6.2. Le Transazioni

Un'altro aspetto particolarmente critico nella programmazione dell'accesso al backend ` quello transazionale. Una transazione è una sequenza di azioni di lettura e scrittura della base di dati e di elaborazioni di dati in memoria temporanea che il database deve eseguire garantendo le seguenti proprietà:

  • Atomicità, una transazione e' un'unita di elaborazione. Solo le transazioni che terminano normalmente (committed) fanno transitare la base di dati in un nuovo stato. Le transazioni che terminano prematuramente (aborted) sono trattate come se non fossero mai iniziate.

    Nel caso dell'aggiunta di un ordine, se fallisce la query 3, l'aggiornamento della disponibilità, deve essere annullata anche la 2, l'inserzione degli articoli acquistati.

  • Consistenza, una transazione è una trasformazione corretta dello stato del database. Al termine di ogni transazione il DB deve trovarsi in uno stato consistente. Il Database Management System (DBMS) garantisce che nessuno dei vincoli di integrità del DB venga violato.

  • Serializzabilità, l'effetto sulla base di dati dell'esecuzione concorrente di più transazioni è equivalente ad un'esecuzione seriale delle stesse, cioè ad un esecuzione in cui le transazioni vengono eseguite una dopo l'altra in un qualche ordine. Quindi, transazioni concorrenti non devono influenzarsi reciprocamente.

    Supponiamo di ricevere due ordini contemporaneamente, ognuno per 8 pezzi dello stesso articolo quando in listino sono disponibili 10 pezzi. La sequenza di operazioni potrebbe essere questa:

    	-- Lettura dalla disponibilita
    	SELECT disponibilita FROM listino WHERE ...
    	
    	-- dato in memoria temporanea
    	var disponibilitaCorrente = $(disponibilita)
    	
    	-- verifico disponibilita
    	if(disponibilitaCorrente >= 8){
    	
    		-- Inserisco l'erdine
    		INSERT ordine ....
    		UPDATE listino SET disponibilita=(disponibilitaCorrente-8) WHERE ...
    	}
    

    Se le ordinazioni vengono eseguite simultaneamente, in assenza di serializzabilità potrebbe succedere che venga verificata la disponibilità, accettati entrambi gli ordini e aggiornata la disponibilità in maniera errata.

  • Persistenza, Le modifiche sulla base di dati di una transazione terminata normalmente sono permanenti, cioè non alterabili da eventuali malfunzionamenti successivi alla terminazione. Il DBMS deve proteggere il DB a fronte di guasti.

[Nota] Nota

Consistenza e Persistenza sono gestite dal DBMS e trasparenti alle applicazioni, quindi il programmatore può farci affidamento senza gestirle in alcun modo.

Una transazione può avere due esiti:

  • Commit, indica una terminazione corretta. L'applicazione dopo aver eseguito le varie operazioni di lettura/scrittura sul database che formavano la transazione esegue una particolare istruzione SQL COMMIT che comunica ufficialmente al Transaction Manager il termine delle operazioni, e fa quindi evolvere la base di dati verso un nuovo stato consistente modificato dalla transazione appena conclusa.

  • Rollback, indica una terminazione non corretta. E' possibile sia che la transazione, per qualche motivo applicativo, decida che non abbia senso continuare la transazione e quindi la abortisce eseguendo l'istruzione SQL ROLLBACK, sia che il sistema non sia più in grado (es. guasto tecnico o violazione di un vincolo tipo 'foreign key') di garantire la corretta prosecuzione della transazione e quindi la abortisce automaticamente.

Il modo più semplice di operare sul DB tramite JDBC è quello di usare l'autoCommit mode sulla connessione. Se l'autocommit è impostato a true ogni operazione sul DB sarà seguita automaticamente da una commmit. Impostando invece l'autocommit a false, la transazione viene avviata alla prima istruzione SQL inviata sulla connessione, mentre la sua chiusura non avviene più in automatico ma esplicitamente tramite l'esecuzione delle istruzioni di commit o rollback da parte del programmatore. Di seguito è riportato un frammento di codice Java che mostra come gestire una transazione con autommit a false.

	java.sql.Connection connectionDB = null;
	try{
	
	// inizializzazione driver JDBC
		Class.forName(...DRIVER...);
		connectionDB = DriverManager.getConnection(CONNECTION_URL , USERNAME , PASSWORD);
		
		// Avvio transazione, impostando l'auto-commit a false
		connectionDB.setAutoCommit(false);
		
		... LOGICA APPLICATIVA ...
		
		// transazione terminata con successo
		connectionDB.commit();
	
	}catch(Exception e){
	
		// transazione terminata con errore
		connectionDB.rollback();
		
		... GESTIONE ERRORE ...
	
	} finally {
	        // Chiusura connessione
		connectionDB.close();
	}
	

6.3. Livelli di isolamento

In teoria la serializzabilità dovrebbe essere sufficiente a garantire l'isolamento delle transazioni in tutte le possibili condizioni al contorno. In realtà non è così, perchè il costo da pagare in termini di riduzione della concorrenza sarebbe troppo elevato rispetto a quanto effettivamente richiesto dalle applicazioni. Per questo motivo è possibile definire, per ogni transazione, l'effettivo livello di isolamento necessario per quella transazione. I livelli di isolamento previsti in SQL sono quattro e sono analizzati nella lezione sulle transazioni.

Il seguente frammento di codice java mostra come impostare il livello di isolamento attraverso la libreria JDBC. La classe java.sql.Connection fornisce quattro costanti rappresentanti i valori dei quattro livelli di isolamento: Connection.TRANSACTION_READ_UNCOMMITTED, Connection.TRANSACTION_READ_COMMITTED, Connection.TRANSACTION_REPEATABLE_READ, Connection.TRANSACTION_SERIALIZABLE .

java.sql.Connection connectionDB = null;
try{
	       
   // inizializzazione driver JDBC
   Class.forName(...DRIVER...);
   connectionDB = DriverManager.getConnection(CONNECTION_URL , USERNAME , PASSWORD);

   // Imposto il livello di isolamento SERIALIZABLE
   connectionDB.setTransactionIsolation(Connection.SERIALIZABLE)

   // Avvio transazione, impostando l'auto-commit a false
   connectionDB.setAutoCommit(false);

         ... LOGICA APPLICATIVA ...

   // transazione terminata con successo
   connectionDB.commit();

}catch(Exception e){

   // transazione terminata con errore
   connectionDB.rollback();

         ... GESTIONE ERRORE ...

}finally{
   // Chiusura connessione
   connectionDB.close();
}

Nei più diffusi database (es. PostgreSQL, Oracle) è possibile impostare uno dei quattro livelli di isolamento previsti, ma internamente all'implementazione del DBMS ci sono solo due distinti livelli, che corrispondono a Read Committed e Serializable. Se viene scelto un livello Read Uncommitted verrà in realtà utilizzato dal DBMS il livello Read Committed, mentre se viene scelto il livello Repeatable Read verrà utilizzato il livello Serializable. Questo è permesso dal SQL standard poichè i quattro livelli di isolamento definiscono solamente quali anomalie, dovute agli accessi concorrenti dei dati, NON DEVONO succedere, ma non definiscono quale fenomeno POSSONO succedere.

Per una estesa trattazione dell'uso dei livelli di isolamento si rimanda all'ottima documentazione su Transaction Isolation di Postgresql.

Footer BGFooter BG
Tito Flagella - © 2007-2015