Concorrenza ottimistica e DataSet
Molte applicazioni gestiscono ed elaborano dati che hanno bisogno di essere
conservati al di là del tempo di esecuzione del programma su un computer, e
normalmente si basano su database relazionali per conservare i dati con cui
lavorano, per recuperarli in tempi adeguatamente rapidi, per garantire
l'integrità dei dati e per sfruttare servizi di base dei server di database
(backup, ridondanza dei server nei cluster, repliche distribuite in siti
geograficamente distanti).
L'accesso contemporaneo ai dati da parte di più utenti è fonte di rischi che
vanno affrontati con un approccio accorto da parte di chi disegna le
applicazioni. Un esempio di questi rischi è il caso in cui due utenti,
chiamiamoli Anna e Bruno, leggano entrambi lo stesso record per mezzo
dell'applicazione. Entrambi gli utenti vedono la stessa versione originale dei
dati, per esempio un campo di testo che descrive le caratteristiche di un
prodotto offerto dall'azienda in cui lavorano. Anna intende modificare la parte
del testo in cui si parla delle caratteristiche estetiche del prodotto. Bruno
intende modificare la parte del testo in cui vengono descritte alcune
caratteristiche tecniche. Gli scenari possibili sono i seguenti:
Concorrenza pessimistica
Anna comincia a modificare i propri dati, e l'applicazione crea un lock sul
record; se Bruno tenta di cominciare anch'egli le modifiche allo stesso record,
verrà bloccato dall'applicazione che non riuscirà ad ottenere il lock sullo
stesso record.
Una volta che Anna avrà sbloccato il record, Bruno potrà ottenere il lock sul
record, leggere la versione del record con le modifiche apportate da Anna,
aggiungere le proprie modifiche e aggiornare il database. Al termine di questa
operazione, il record conterrà sia le modifiche di Anna che quelle di Bruno.
Concorrenza ottimistica
Anna comincia a modificare i propri dati, senza che l'applicazione crei lock sul
record. Anche Bruno comincia a modificare gli stessi dati. Anna andrà ad
aggiornare il record con le proprie modifiche. Bruno tenterà di aggiornare lo
stesso record; l'applicazione che esegue l'aggiornamento avrà cura di
verificare che non ci sono state modifiche tra il momento in cui Bruno ha letto
i dati e il momento in cui li ha aggiornati con le proprie modifiche. Siccome
nel nostro caso Anna ha modificato il record letto da Bruno, l'applicazione
comunicherà a Bruno che l'aggiornamento del record non è andato a buon fine, a
causa della modifica dello stesso record apportata da un altro utente (Anna). A
questo punto Bruno potrebbe rileggere il record, apportare di nuovo le stesse
modifiche, e se nessun altro apporta altre modifiche allo stesso record, Bruno
riuscirà a salvare il record che conterrà le modifiche di entrambi gli utenti.
L'applicazione avrebbe anche potuto proporre a Bruno di sovrascrivere le
modifiche apportate da Anna (probabilmente mostrandogliele), oppure avrebbe
potuto tentare di integrare le modifiche di entrambi gli utenti. In ogni caso,
di fronte al conflitto di versioni, l'applicazione si dovrebbe rivolgere a
Bruno per sapere come comportarsi.
L'ultimo vince
Anna e Bruno leggono la stessa versione del record, entrambi modificano la
versione originale del record. Anna salva le proprie modifiche per prima, Bruno
le salva per secondo, e l'applicazione sovrascrive le modifiche apportate da
Anna, senza ulteriori notifiche, né ad Anna, né a Bruno.
Concorrenza e dati salvati su file system
L'esempio di cui abbiamo parlato è relativo a dati che risiedono in un database,
ma gli stessi scenari di aggiornamento dei dati sono in teoria possibili anche
quando i dati dell'applicazione sono salvati sul file system, ad esempio in una
cartella condivisa in rete, e più utenti tentano di modificare
contemporaneamente lo stesso file. Anche in questo caso l'applicazione può
decidere quale degli approcci adottare. Word ed Excel optano per il lock di
tipo pessimistico: il secondo utente che apre il file lo trova lockato, e può
decidere di aprire il file in sola lettura. La maggior parte delle altre
applicazioni che salvano i dati su file system usa l'approccio "l'ultimo vince"
(a partire dal Blocco note di Windows...). Questo approccio può essere una
scelta consapevole da parte dello sviluppatore, oppure più comunemente non si
pensa a gestire l'accesso concorrente allo stesso file perché si reputa il
problema poco importante per l'applicazione che si sta sviluppando.
Pregi e difetti dei tre approcci
Il problema dell'accesso concorrente ai dati è avvertito soprattutto quando ci
sono molti utenti che accedono contemporaneamente agli stessi dati, e quindi
quando questi risiedono su database, perciò limiteremo l'analisi dei pro e dei
contro dei tre approcci a questo caso.
L'approccio della concorrenza pessimistica è quello che risparmia all'utente
degli input che potrebbero essere inutili in caso di conflitti: mentre Anna
modifica la propria copia dei dati, a Bruno è impedito di modificare lo stesso
record, dato che Anna potrebbe decidere di salvare le proprie modifiche e
costringere Bruno a ricominciare dalla nuova versione che comprende le
modifiche di Anna. D'altro canto, questo è anche l'approccio più dispendioso in
termini di risorse lato server, se gli utenti che eseguono operazioni
contemporanee comincia ad essere significativo. E' applicabile solo se il
database server supporta i lock (tutti i database server che si rispettino li
supportano). Questo è un approccio poco naturale per lo sviluppo di
applicazioni web, mentre può essere più facile da implementare in applicazioni
desktop.
La strategia della concorrenza ottimistica è quella più naturale per le
applicazioni web, in cui non è necessario (anzi, è inopportuno) mantenere
aperta una connessione al database per tempi che vanno oltre il tempo impiegato
per elaborare la singola richiesta. Può essere utile anche nelle versioni
desktop di una applicazione con due versioni di interfaccia utente
(applicazione web e desktop) se si decide che è meglio unificare gli approcci
nei due casi, o se si decide che è meglio evitare di mantenere aperte le
connessioni al database più dello stretto necessario. Le query di aggiornamento
si complicano perché devono supportare le verifiche che servono a rifiutare
aggiornamenti concorrenti.
La strategia dell'"ultimo vince" è indicata solo per casi particolari, in cui
non è importante difendersi da modifiche concorrenti. Ha il vantaggio di avere
procedure di aggiornamento molto più semplici (la clausola where delle query di
aggiornamento contiene solo il valore della chiave primaria).
In sostanza, l'approccio della concorrenza ottimistica è quello che più si
adatta a una vasta schiera di situazioni, e quindi è bagaglio fondamentale di
uno sviluppatore che si occupa di accesso ai dati.
Accorgersi della modifica concorrente
Abbiamo visto sopra come la modifica di Anna, con l'approccio della concorrenza
ottimistica, vada a buon fine, mentre la modifica di Bruno non debba andare a
buon fine. Questo può essere ottenuto in modi differenti a seconda anche del
database server di cui si dispone. L'approccio magari più complicato, ma
implementabile in tutti i casi è quello di tenere contemporaneamente traccia,
per i record modificati da Bruno, sia del valore dei campi letto dal database e
presentato a Bruno (valore originale), sia del valore dei campi modificati da
Bruno (valore corrente). Le query di aggiornamento impostano al valore corrente
tutti i campi, limitando il campo d'azione a un solo record, individuato
includendo nella clausola where una condizione sui campi che compongono la
chiave primaria. Con l'approccio della concorrenza ottimistica si vuole
contemporaneamente verificare che nessun altro abbia fatto modifiche allo
stesso record, e quindi si include nella clausola where la condizione che
verifica che il valore del campo modificato contenga ancora il valore
originale, letto in precedenza e mostrato a Bruno per la modifica. Se il valore
è ancora lo stesso, la modifica di Bruno andrà a buon fine e la query
aggiornerà un record, se invece è stato modificato, la query non aggiornerà
alcun record; il numero di record aggiornati è restituito come risultato del
comando al database, quindi l'applicazione può comportarsi di conseguenza,
segnalando a Bruno, in caso di modifica concorrente, che qualcun altro ha già
modificato il record che stava aggiornando lui.
La query eseguita al momento del salvataggio delle modifiche di Bruno, potrebbe
essere simile a questa:
UPDATE Prodotti SET (Descrizione = "Lettore CD-ROM 52x. Interfaccia IDE.")
WHERE IdProdotto = 25 AND Descrizione = "Lettore CD-ROM 52x."
Come si può notare, la query contiene sia il valore inserito da Bruno, sia il
valore originale letto in precedenza. Ovviamente, in luogo di una query di
questo genere, si poteva usare una query parametrica o una stored procedure per
ottenere prestazioni migliori.
Quando la tabella contiene più campi, ciascuno di questi compare due volte, una
volta dopo la parola chiave SET nella lista dei nuovi valori, e una volta nella
clausola WHERE. Una variazione di questa tecnica prevede di verificare che non
siano cambiati solamente i campi che sono stati modificati dall'utente. Il
vantaggio è che permette a due utenti la modifica contemporanea dello stesso
record purché le modifiche avvengano su campi differenti. Lo strato di accesso
ai dati dell'applicazione invece può complicarsi un po' se si vuole adattare la
lista di parametri passati ai campi modificati dall'utente.
La tecnica di conservare e passare alla fonte dati i valori originali può essere
poco efficiente quando la tabella da aggiornare ha parecchi campi: il numero di
parametri per le query di aggiornamento può essere da doppio a triplo rispetto
alle query dell'approccio "l'ultimo vince". Una soluzione differente prevede
che sulla tabella esista un campo aggiuntivo che contiene un valore modificato
automaticamente dal database server ad ogni modifica di un record.
L'applicazione anziché doversi conservare sia i valori originali che i valori
correnti dei campi, si può conservare solamente i valori correnti: per
controllare che nessuno abbia modificato uno qualsiasi dei campi del record è
sufficiente includere nella clausola WHERE solamente la condizione che il campo
TimeStamp contenga ancora il valore letto in precedenza. La query si semplifica
si riduce anche lo scambio dei dati tra client e server.
Il DataSet per la cache di dati locale
In ciascuno dei casi, può essere utile ricorrere al dataset come cache di dati
locale: i vantaggi sono la somiglianza di questa struttura dati al database, la
presenza di strumenti di mapping automatici tra i dati nel database e quelli
negli oggetti DataRow, la possibilità di usare facilmente questo oggetto
insieme a controlli windows forms o web forms per presentare i dati all'utente.
L'alternativa è scriversi da zero i propri business objects, cosa che può
indubbiamente avere dei vantaggi, come quello di evitare l'uso di oggetti che
possono essere piuttosto pesanti quali il dataset tipizzato, o la costrizione
dei propri oggetti in tipi di dato "da tabella". Non voglio dilungarmi su
questo punto, ma se la vostra scelta è quella di utilizzare il dataset per
rappresentare i vostri dati in memoria, vi troverete in regalo un aiuto al
supporto per la concorrenza ottimistica: l'oggetto DataRow presenta agevolmente
la versione corrente del dato, sia in lettura che in modifica, e supporta la
conservazione e la lettura del valore originale del dato, utilizzabile al
momento in cui si va ad aggiornare la fonte dati con una query che includa i
controlli dell'approccio concorrenza ottimistica.
|