Document

annuncio pubblicitario
Università degli Studi di Padova
Dipartimento di Matematica
Corso di informatica
Tesi di Laurea Triennale
Applicazioni Web Offline
studio delle caratteristiche di applicazioni web offline
Candidato: Riccardo Cucco - 593945
Relatore: Dott.sa Ombretta Gaggi
Azienda ospitante: Zucchetti SPA
Tutor aziendale: Dott. Stefano Boldrin
A.A. 2011/2012
Scopo del presente documento è quello di illustrare le attività svolte dal laureando Riccardo Cucco durante lo stage obbligatorio atto al conseguimento della laurea triennale in
informatica. Le attività documentate sono divise in capitoli corrispondenti ai vari aspetti
affrontati ovvero studio delle funzionalità messe a disposizione, progettazione di prototipi,
considerazioni sull’utilizzo di dati locali e loro sincronizzazione con la controparte remota.
Le attività di raccolta dati e documentazione sono state costantemente presenti durante lo
svolgimento dello stage.
Indice
1 Introduzione
1.1 L’evoluzione del web, da siti ad applicazioni
1.2 Obiettivi dello stage . . . . . . . . . . . . .
1.3 Azienda ospitante . . . . . . . . . . . . . . .
1.3.1 Informazioni generali . . . . . . . . .
1.3.2 Gruppo Zucchetti . . . . . . . . . .
2 Funzionalità e standard coinvolti
2.1 Le funzionalità offline . . . . . . . . . . . .
2.1.1 Cache . . . . . . . . . . . . . . . . .
2.1.2 Application Cache . . . . . . . . . .
2.2 La persistenza dei dati . . . . . . . . . . . .
2.2.1 SessionStorage . . . . . . . . . . . .
2.2.2 LocalStorage . . . . . . . . . . . . .
2.2.3 WebSQL . . . . . . . . . . . . . . .
2.2.4 IndexedDB . . . . . . . . . . . . . .
2.3 Funzionalità d’appoggio . . . . . . . . . . .
2.3.1 Chiamate asincrone . . . . . . . . .
2.3.2 JSON . . . . . . . . . . . . . . . . .
2.3.3 Programmazione ad eventi e callback
2.4 Maturità degli standard e limitazioni . . . .
2.4.1 Standard per manifest . . . . . . . .
2.4.2 Standard per WebSQL . . . . . . . .
2.4.3 Standard per IndexedDB . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
3 Applicazioni web offline
3.1 Perchè un’applicazione offline? . . . . . . . . . . . . . . .
3.1.1 Un esempio di applicazione esistente: Google Docs
3.2 Tipologie di applicazioni offline . . . . . . . . . . . . . . .
3.2.1 Applicazioni isolate . . . . . . . . . . . . . . . . . .
3.2.2 Applicazioni in sola lettura . . . . . . . . . . . . .
3.2.3 Applicazioni complesse . . . . . . . . . . . . . . . .
3.3 Progettazione delle applicazioni . . . . . . . . . . . . . . .
3.3.1 Interfacce distinte o integrazione trasparente . . .
3.3.2 Spostamento della componente di presentazione . .
3.3.3 Considerazioni sulle caratteristiche delle reti mobili
3
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
7
7
10
10
11
11
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
12
12
12
13
14
14
14
14
15
15
15
16
16
16
17
17
17
.
.
.
.
.
.
.
.
.
.
18
18
19
22
22
23
23
24
24
25
26
3.4
Il manifest . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.4.1 Funzionamento del file manifest . . . . . . . . . . . . . . . . . . . . .
3.4.2 Stati dell’applicationCache e relativi eventi . . . . . . . . . . . . . .
3.4.3 Problematiche relative allo stato updateReady e inconsistenza dell’applicazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.4.4 Interferenza con la cache standard . . . . . . . . . . . . . . . . . . .
3.4.5 Problematiche relative al caching delle pagine e all’asincronia degli
eventi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.4.6 Svuotamento dell’application cache . . . . . . . . . . . . . . . . . . .
3.4.7 Effetto BlankPage . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.4.8 Gestire l’effetto BlankPage . . . . . . . . . . . . . . . . . . . . . . .
3.4.9 Differenza tra assenza di connessione e tolleranza ai guasti . . . . . .
3.4.10 Implementare il rilevamento della connessione . . . . . . . . . . . . .
Considerazioni conclusive . . . . . . . . . . . . . . . . . . . . . . . . . . . .
33
34
34
35
36
38
40
4 Persistenza dei dati
4.1 Sistemi di persistenza non strutturati . . . . . . . . . . . . . . . . . . . . . .
4.1.1 Cookies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.1.2 Sessioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.1.3 LocalStorage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2 Sistemi di persistenza strutturati . . . . . . . . . . . . . . . . . . . . . . . .
4.2.1 Database remoti . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2.2 WebSQL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2.3 IndexedDB . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.3 Studio dei database locali . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.3.1 Contesto d’utilizzo di un database locale . . . . . . . . . . . . . . . .
4.3.2 Differenze tra IndexedDB e WebSQL . . . . . . . . . . . . . . . . . .
4.3.3 IndexedDB nel dettaglio . . . . . . . . . . . . . . . . . . . . . . . . .
4.3.4 La scelta di supportare solamente IndexedDB: motivazioni e critiche
4.3.5 Scegliere il proprio database offline . . . . . . . . . . . . . . . . . . .
4.3.6 Variazioni al concetto di sicurezza del dato nel database locale . . .
4.4 Confronto con i database remoti . . . . . . . . . . . . . . . . . . . . . . . .
4.4.1 Relazionali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.4.2 Non relazionali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.4.3 Teorema di CAP . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
41
41
41
42
42
43
43
44
45
45
45
46
48
49
50
50
51
51
52
52
5 Sincronizzazione dei dati
5.1 Sincronizzazione fisica di database . . . . . . . . . .
5.1.1 Ipotesi iniziale: interpretazione in ottica fisica
5.1.2 Problematiche riscontrate lato client . . . . .
5.1.3 Problematiche riscontrate lato server . . . . .
5.1.4 Partizionamento dei dati . . . . . . . . . . . .
5.1.5 Rivalidazione delle azioni eseguite client side
54
55
55
55
56
57
58
3.5
4
. . .
(file)
. . .
. . .
. . .
. . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
27
28
30
31
32
5.2
5.3
5.4
Sincronizzazione a livello applicativo . . . . . . . . . . . . .
5.2.1 Sincronizzazione trasparente . . . . . . . . . . . . . .
5.2.2 Sincronizzazione incrementale mediante copie delta .
Strumenti di sincronizzazione . . . . . . . . . . . . . . . . .
5.3.1 Utilizzo di marcature di sincronizzazione . . . . . . .
5.3.2 DBDigest . . . . . . . . . . . . . . . . . . . . . . . .
5.3.3 Marcatori di operazioni incrementali legati a MVCC
5.3.4 Problematiche legate al Vacuum . . . . . . . . . . .
5.3.5 Aggiunta di timestamp ai fini della sincronizzazione
5.3.6 Inserire uno strato logico nel driver del database . .
Sincronizzare le eliminazioni . . . . . . . . . . . . . . . . . .
5.4.1 Eliminazioni logiche . . . . . . . . . . . . . . . . . .
5.4.2 Tabelle cestino . . . . . . . . . . . . . . . . . . . . .
5.4.3 View di facciata . . . . . . . . . . . . . . . . . . . .
5.4.4 Log delle eliminazioni . . . . . . . . . . . . . . . . .
6 Conclusioni
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
58
59
59
60
60
61
62
63
65
65
65
66
67
67
68
69
5
Elenco delle figure
1.1
1.2
1.3
1.4
1.5
1.6
1.7
1.8
1.9
logo
logo
logo
logo
logo
logo
logo
logo
logo
Google Docs. . . . .
Facebook. . . . . . .
From Dust. . . . . .
Symfony. . . . . . . .
Google Web Toolkit.
Sproutcore. . . . . .
Impress.js. . . . . . .
JavaScriptMVC. . . .
Zucchetti SPA . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
7
7
8
8
8
9
9
9
11
3.1
3.2
3.3
una schermata di Google Docs offline . . . . . . . . . . . . . . . . . . . . . .
logo Zucchetti SPA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
logo Zucchetti SPA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
19
20
21
6
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
1 Introduzione
Il presente documento descrive l’attività di stage svolta dal laureando Riccardo Cucco presso
l’azienda Zucchetti SPA.
Il progetto di stage consiste nello studio delle funzionalità a supporto delle applicazioni
web offline: in particolare l’obbiettivo primario di questo progetto è stato quello di valutare la maturità delle funzionalità offerte dai browser nell’attuale implementazione degli
standard (ancora in via di sviluppo). Lo studio delle funzionalità è stato svolto in un ottica non prettamente teorica, ma applicativa, e quindi è stata dedicata una gran parte del
tempo alla sperimentazione e allo sviluppo di prototipi che potessero verificare la corretta
implementazione e l’effettiva applicabilità delle specifiche analizzate.
1.1 L’evoluzione del web, da siti ad applicazioni
Il mondo del web per come lo conosciamo ora è un ambiente estremamente dinamico e ricco
di risorse. Negli ultimi anni alcuni grandi progetti hanno trasformato la concezione statica
del web in una versione molto più interattiva e funzionale, entrando nel quotidiano con vere
e proprie applicazioni quali GMail, Google Docs, Facebook, Twitter e simili.
Tutte queste realtà indicano come il web sia diventato e venga percepito come una reale
piattaforma di sviluppo al pari dell’ambiente desktop. La ricchezza delle applicazioni sviluppate raggiunge livelli estremamente alti, specie considerando la rapidità con cui siamo
passati da meccanismi di interazione basilari a navigatori, giochi di ruolo e via dicendo.
Figura 1.1: logo Google Docs.
Google Docs è un’applicazione che è nata in supporto al progetto GMail. Google Docs viene utilizzata in tutto il mondo come suite di strumenti d’ufficio.
Offre la possibilità di utilizzare in maniera
collaborativa i propri documenti. Questa applicazione è stata una delle prime a concretizzare la visione
del Cloud Computing orientata all’ottica di produzione da ufficio e a fornirla gratuitamente su vasta scala.
Facebook è un’applicazione web che ha guadagnato rapidamente una vastissima fascia d’utenza. Il suo successo
deriva sicuramente anche dal livello di aggiornamento tecnico costante.
7
Figura 1.2: logo Facebook.
Figura 1.3: logo From Dust.
From Dust è un gioco 3D con un particolare sistema di interazione distribuito dalla Ubisoft, inizialmente per Xbox360, Playstation3, Windows e Mac. Recentemente è stata curata una sua trasposizione in applicazione web per Chrome (seppur sfruttando del codice nativo). Questo gioco è vincitore di molti premi,
e trasformato in applicazione web ha potuto raggiungere un panorama di utenza ancora più vasto, tra cui
l’intera fascia di mercato rappresentata dagli utenti Linux.
Lo sviluppo di queste applicazioni si deve innanzitutto ad un potenziamento dei motori
Javascript integrati nei browser, senza i quali saremmo ancora completamente schiavi dei
tempi di trasmissione e dell’aggiornamento della pagina.
Per ottenere un alta usabilità ed efficacia dei programmi sviluppati sono fiorite una serie di
librerie in Javascript per la codifica client-side, in grado di migliorare l’interazione avanzata con l’utente. Per quanto riguarda la programmazione server-side, la maggior parte dei
linguaggi ha sviluppato dei moduli per la programmazione web based, ed alcune compagnie
hanno rilasciato interi framework in grado di rendere più veloce e disciplinato il processo di
costruzione di un’applicazione web.
Symfony è un framework completo per lo sviluppo web
in PHP. È stato sviluppato da una web agency francese
nel corso di lunghi anni di lavoro, condensando in un unico progetto tutte le conoscenze acquisite con l’esperienza.
Attualmente il framework è uno dei più completi ed ha
una comunità di supporto molto attiva. Data la sua stabilità è stato scelto come base per la prossima versione
del noto CMS Drupal.
Figura 1.5: logo Google
Toolkit.
Figura 1.4: logo Symfony.
Google Web Toolkit è un framework basato su Java che permette di collegare a questo poliedrico linguaggio un interfaccia ed un modello di interazione prettamente web.
Con GWT è possibile creare la propria applicazione Java e vederla poi eseguire nativamente su di un browser, senza curarsi di gestire chiamate asincrone, callback in JavaWeb Script, stile degli elementi e composizione della pagina.
Con la diffusione di tutte queste risorse per lo sviluppo la concezione di web piatto è stata
sostituita da un nuovo ambiente web coinvolgente, multidevice oltre che multipiattaforma.
Le applicazioni sopracitate hanno aperto la strada verso una vera e propria rivoluzione: le
8
chiamate asincrone, framework capaci di creare dinamicamente la pagina client side disegnando gli elementi su un canvas, effetti grafici sempre più stupefacenti, modellazione 3D,
database integrati nel browser e copia permanente in cache di intere applicazioni.
Sproutcore è un framework Javascript utilizzato da Apple per il suo progetto ICloud. Lavorare con Sproutcore
significa utilizzare un sistema di sviluppo ben definito e
disciplinato, ma garantisce una resa visiva ed un efficacia
veramente invidiabili.
Figura 1.7: logo Impress.js.
Figura 1.6: logo Sproutcore.
Impress.js è una libreria JavaScript che permette di codificare in maniera rapida e semplice un sistema di presentazione con slide disposte all’interno di una vasta area
piana (o tridimensionale), simulando il concetto alla base
del sistema di presentazione Prezi. L’utilizzo del canvas e
della visualizzazione tridimensionale è stato curato molto
bene, garantendo effetti di transizione fluidi e armoniosi.
JavaScriptMVC appartiene alla categoria dei framework
strutturali, utilizzati per garantire alle applicazioni da
sviluppare una corretta ripartizione del codice, secondo
i pattern dell’ingegneria del software. Vi sono molte altre Figura 1.8: logo
librerie simili, ognuna con un diverso pensiero riguardo alJavaScriptMVC.
l’organizzazione delle componenti dell’applicazione: Backbone.js (pattern Model View Collection), Spine.js (Model Controller).
In un solo punto la differenza tra applicazione web based e desktop è insormontabile: la
necessità di una connessione. La disponibilità di applicazioni offline è un punto chiave, che
crea un divario notevole tra applicazioni web e desktop.
9
1.2 Obiettivi dello stage
L’obbiettivo dello stage è uno studio di maturità delle tecniche di programmazione, delle
funzionalità e degli strumenti necessari a colmare il divario descritto nel paragrafo precedente. La necessità di una connessione continua comporta alcune difficoltà, specialmente
quando l’applicazione deve essere usata in un contesto particolare, come quello della mobilità, che quindi verrà spesso utilizzato come ambiente di riferimento.
I punti di principale interesse nello studio delle applicazioni web offline sono quindi:
• lo studio delle differenti forme di applicazioni offline
• l’utilizzo delle funzioni di caching HTML5 per sopperire alla mancanza di connessione
• l’utilizzo di dati locali in vari formati di archiviazione e
• i modelli di sincronizzazione possibili tra dati locali e remoti in un ottica multidevice
e multiutente
Per lo studio delle funzionalità sono stati utilizzati vari riferimenti: come prima fonte sono
stati scelti gli standard del W3C, integrati dalle specifiche tecniche dei browser più fedeli
agli standard. Utilizzando i primi come fonti teoriche e le seconde come fonti più pratiche è
stato possibile in varie situazioni poter comprendere il concetto generale e il reale obbiettivo
di una certa funzionalità prima di calarsi nella sua attuale implementazione, a volte solo
parziale e condizionata o limitata da altre necessità tecniche o progettuali.
1.3 Azienda ospitante
Il progetto di stage, comprendente le attività di analisi, progettazione, programmazione e
documentazione, è stata svolta presso l’azienda Zucchetti SPA con sede a Padova.
La Zucchetti SPA, sede dello stage, fa parte del gruppo Zucchetti.
La scelta di tale azienda è stata guidata dall’interesse dimostrato nello studio delle nuove
soluzioni messe a disposizione dai moderni standard per il web. L’azienda sviluppa da anni
applicazioni web per la gestione aziendale, ed ha quindi affrontato una vasta gamma di
problemi derivanti dall’uso avanzato delle funzionalità web in sostituzione ai normali programmi desktop.
La sede di Padova, scelta per lo stage, si occupa totalmente di ricerca e sviluppo di nuove
soluzioni, ed è quindi un ambiente molto stimolante, in cui vari progetti vengono portati
avanti in parallelo.
10
1.3.1 Informazioni generali
Nome: Zucchetti SPA
Sede del Tirocinio: Via Giovanni Cittadella 7
CAP: 35137
Città: Padova
Provincia: PD
Telefono: 049659831
Fax: 049659831
Partita IVA: 05006900962
Mail: [email protected]
1.3.2 Gruppo Zucchetti
Il gruppo Zucchetti si occupa principalmente di fornire soluzioni software, hardware e servizi
innovativi ad aziende di ogni dimensione e settore, professionisti, associazioni di categoria,
CAF e alla pubblica amministrazione.
Il gruppo è all’attivo da oltre 30 anni e fornisce prodotti e servizi utilizzati da oltre 10.000
aziende italiane.
Nella figura 1.9 è illustrato il logo del gruppo Zucchetti.
Figura 1.9: logo Zucchetti SPA
11
2 Funzionalità e standard coinvolti
L’intera esperienza di stage può essere considerata uno studio avanzato di alcune funzionalità. Il principio di fondo che ha guidato questo studio non si basa solamente sul modo
in cui applicare gli standard, non è una sorta di guida allo sviluppo. L’idea principale è
quella di poter formare prima di tutto un concetto di applicazione web offline, che differisce radicalmente dall’applicazione web online. Per riuscire ad acquisire la visione generale
dell’ambiente offline è però necessario capire preventivamente su quali principi si basa. I
punti esposti successivamente sono un anticipazione di ciò che verrà esposto nella tesi, ma
sono utili per formare una rapida visione d’insieme, necessaria per poter poi capire meglio
la connessione dei vari elementi.
2.1 Le funzionalità offline
Studiare le funzionalità offline significa cercare di comprendere i linguaggi, le tecniche di
programmazione e di progettazione, gli standard e gli strumenti messi a disposizione per
risolvere il problema dell’assenza di connessione.
Il principio fondante è mantenere una copia delle pagine necessarie all’applicazione in memoria, in modo da poterle presentare senza doverle scaricare nuovamente alla prossima
richiesta. Un’applicazione di questo tipo richiede quindi una prima connessione (definita
come connessione di setup) in cui i dati vengono recuperati ed immagazzinati. Per realizzare
questo comportamento vi sono due principali soluzioni, che differiscono pesantemente per
quanto riguarda sia il setup che la gestione delle pagine memorizzate.
2.1.1 Cache
Il principio di funzionamento della cache è il più basilare: ogni file viene marcato con un
istruzione nell’intestazione. Questa istruzione indica il periodo per il quale il file deve essere
considerato attendibile e quindi riutilizzato senza essere aggiornato.
Tale istruzione non dev’essere necessariamente inserita nell’intestazione di ogni file, utilizzando htaccess è possibile associare una specifica politica di caching ai file contenuti
all’interno di una cartella, o corrispondenti ad un particolare schema.
I problemi principali di questo approccio sono due: l’irreversibilità dell’istruzione e la necessità della scansione.
Per irreversibilità dell’istruzione si intende l’impossibilità di forzare un aggiornamento del
file finché l’istruzione di caching non esaurisce il suo effetto. In sostanza un elemento marcato come definitivo, con un periodo di caching illimitato, non verrà mai aggiornato, e quindi
12
successive modifiche alla sua politica di cache non verranno mai interpretate. Questa caratteristica comporta quindi un isolamento di alcune sezioni dell’applicazione, che seppur
inizialmente voluto potrebbe creare successivamente dei gravi problemi di manutenibilità.
Il secondo problema riguarda la necessità di effettuare una scansione manuale completa di
tutti i file che compongono l’applicazione, dal momento che le istruzioni di caching vengono
interpretate solamente se il file viene effettivamente utilizzato almeno una volta in presenza
di connessione. Per memorizzare un’applicazione e poterla utilizzare in modalità offline, l’utente dovrebbe quindi utilizzare almeno una volta ogni sua funzionalità online, esplorando
ogni sezione, comprese azioni proibite, errori e simili.
Le tecniche per gestire in maniera intelligente gli aggiornamenti della cache sfruttano gli
appigli più impensati, cercando di modificare il nome del file, la data di creazione e simili.
Purtroppo questi espedienti non hanno un funzionamento uniforme sui vari browser (essendo appunto principalmente espedienti), i quali al di là delle istruzioni di caching fornite
applicano delle politiche automatiche difficilmente gestibili dallo sviluppatore.
2.1.2 Application Cache
L’application cache è un evoluzione del concetto di cache definito al punto precedente. Le
istruzioni di cache non vengono distribuite file per file, ma mantenute in un singolo file
chiamato manifest. Nel manifest di un’applicazione web sono elencate tutte le risorse che
lo compongono, e per ogni elemento è specificato se debba essere mantenuto in memoria ed
utilizzato senza essere richiesto al server, o se debba essere costantemente richiesto.
Utilizzando questo approccio si isolano immediatamente le due aree dell’applicazione: quella offline e quella online.
L’application cache differisce soprattutto nella gestione dei due punti analizzati precedentemente nella sezione della cache: nonostante le politiche di caching previste è possibile
aggiornare senza difficoltà la struttura e la versione dei file inseriti, e non è richiesta una
scansione completa dell’applicazione per il setup.
Quando il file manifest viene interpretato per la prima volta il browser legge l’elenco delle
risorse elencate, ed avvia lo scaricamento di tutti gli elementi inseriti nella sezione offline.
Da questo momento in poi tutti i file inseriti nella sezione offline non verranno più richiesti
al server. Scaricando in un singolo istante tutti i file dell’applicazione può venire a crearsi
qualche effetto indesiderato (trattato successivamente nella tesi come effetto BlankPage )
ma si risolve il problema della scansione dell’applicazione.
Ogni volta che viene caricata una pagina presente nel file manifest viene interrogato automaticamente il file manifest remoto, per verificarne la versione. Se il file remoto non è
reperibile (funzionamento offline) oppure è identico a quello locale, vengono utilizzati tutti
i file precedentemente scaricati. Se invece il file manifest remoto risulta differente dal file
manifest locale esso viene riscaricato e reinterpretato con il conseguente aggiornamento di
tutte le risorse in esso contenute (anche quelle non aggiornate). Questa politica risolve
il problema delle variazioni nella politica di caching e dell’aggiornamento dei file, seppur
introducendo alcuni effetti indesiderati che verranno analizzati successivamente.
13
2.2 La persistenza dei dati
Per ottenere una funzionalità completa in assenza di connessione non è sufficiente analizzare solamente il problema del reperimento delle risorse; è necessario occuparsi anche della
strutturazione dei dati, del loro recupero e dell’utilizzo.
La persistenza di dati tra una pagina e l’altra è stato uno dei primi fattori chiave nello
sviluppo dei siti web. Una versione di web stateless, cioè senza memoria di stato tra una
pagina e la successiva, non avrebbe permesso la creazione di procedure di login, di carrelli
della spesa e quant’altro. I sistemi di memorizzazione più utilizzati sono stati inizialmente i
cookies (piccoli file testuali generati dal browser client side ed altamente vulnerabili), poi le
sessioni (un meccanismo server side in grado di abbinare un set di dati ad una connessione
attiva tra client e server), ed ovviamente i database, in grado di memorizzare dati strutturati come in qualsiasi altro contesto applicativo.
La necessità di limitare le iterazioni con il server ha spinto gli sviluppatori ad immaginare
un sistema alternativo di memorizzazione tra le pagine che non coinvolgesse direttamente
il server se non in caso di reale bisogno. Distinguiamo quindi i dati locali da quelli remoti,
in base sia al loro dominio d’applicazione che alla loro effettiva localizzazione.
2.2.1 SessionStorage
SessionStorage è stata una delle prime soluzioni al problema sviluppate. In sostanza offre un
sistema di persistenza basato su coppie chiave-valore in cui entrambi i campi sono rappresentati da stringhe. Questo sistema basilare di archiviazione permette quindi di archiviare
rapidamente valori, istruzioni ed anche oggetti se opportunamente serializzati.
La persistenza si SessionStorage è decisamente limitata, infatti ogni dato memorizzato al suo
interno viene eliminato all’uscita dal dominio dell’applicazione o alla chiusura del browser.
2.2.2 LocalStorage
Se SessionStorage può essere vista come un area prettamente temporanea, LocalStorage
offre invece gli stessi servizi con un livello di persistenza concreto. Tutti i dati memorizzati
per il dominio dell’applicazione vengono mantenuti anche all’uscita dal dominio stesso o alla
chiusura dell’applicazione, richiedendo quindi all’utente di cancellarli nel caso si rendessero
non più necessari.
2.2.3 WebSQL
Per lavorare con dati strutturati utilizzando le soluzioni descritte finora sarebbe necessario
codificare i dati all’interno di oggetti, serializzarli ed archiviarli, perdendo quindi la possibilità di eseguire su di essi ricerche specifiche in base alla struttura o alle caratteristiche del
dato stesso.
WebSQL fornisce un interfaccia di accesso ad un vero e proprio database contenuto all’interno del browser (attualmente un implementazione di SQLite). Il sistema di interrogazione
14
prevede l’utilizzo del linguaggio SQL, largamente diffuso ed adottato nella quasi totalità
degli ambiti di programmazione.
La versione di SQLite utilizzata in WebSQL è in realtà affetta da alcune limitazioni, ma
rimane comunque uno strumento decisamente potente. L’idea di poter sincronizzare i dati
tra SQLite e un server remoto è stata lo spunto iniziale per questo stage.
2.2.4 IndexedDB
A seguito delle pesanti critiche a WebSQL è stato sviluppato parallelamente un progetto
che sembra aver preso il sopravvento: IndexedDB. Questo DBMS si rifà a database non
relazionali quali MongoDB, offrendo al tipico programmatore Javascript un ambiente estremamente familiare, sia per le definizioni che per le interrogazioni.
I dati da memorizzare vengono codificati nella sintassi JSON (JavaScript Object Notation),
che ne definisce attributi e metodi proprio come i normali oggetti nei linguaggi di programmazione orientati ad oggetti. IndexedDB è in grado di memorizzare questi oggetti in
collezioni, senza che essi rispondono ad uno schema strutturale preciso. Il fattore chiave è
la presenza di un indice in un formato stabilito per la collection, che viene utilizzato per
identificare univocamente l’elemento ed effettuare le ricerche. Per i campi più comuni è poi
possibile definire indici secondari che verranno sfruttati per eseguire partizionamenti del
database per velocizzare le ricerche. Contrariamente alla soluzione con WebSQL i dati non
vengono serializzati, ma memorizzati nel formato di definizione, rendendo possibile l’utilizzo
di ogni attributo come chiave di ricerca senza eseguire deserializzazioni ripetitive.
2.3 Funzionalità d’appoggio
Per concludere la panoramica delle funzionalità utilizzate è conveniente descrivere alcune
caratteristiche del linguaggio JavaScript necessarie per far dialogare le componenti descritte
tra di loro.
2.3.1 Chiamate asincrone
Le chiamate asincrone rendono possibile un dialogo con il server indipendente dalla normale
procedura di aggiornamento della pagina. Possono essere eseguite richieste al server, è possibile inviare dati di moduli come se l’utente li avesse compilati personalmente, ed ottenere
la risposta che il server avrebbe fornito per presentarla poi all’utente.
Alla base delle chiamate asincrone vi è un oggetto HttpRequest, presente in varie forme
su tutti i browser. Questo oggetto compone delle chiamate incapsulando i dati forniti nel
formato corretto, pronti per essere inoltrati ad un indirizzo specificato nella richiesta.
Nel contesto in analisi le chiamate asincrone sostituiscono la maggior parte della navigazione
tra le pagine che compongono le applicazioni, questo per aumentare il senso di consistenza
dell’interfaccia (nessuna pagina bianca, ne caricamenti evidenti che possano ricondurre ad
un esperienza web standard) e rende possibile l’utilizzo di politiche di instradamento logico
per le risorse o la generazione di risorse in realtà virtuali.
15
2.3.2 JSON
La JavaScript Object Notation è un formato di definizione di dati largamente diffuso ed
utilizzato. La sua semplicità lo rende adatto a rappresentare efficacemente la maggior parte
dei costrutti JavaScript, mantenendone intatta la chiarezza e la semplicità di utilizzo. Ogni
oggetto JSON è considerabile come un array associativo, in cui ogni funzione o attributo è
identificato dal suo nome che ne è la chiave di ricerca.
Ogni oggetto JSON può essere serializzato rapidamente con una funzione spesso implementata nativamente dai browser, ed in maniera altrettanto rapida è possibile ritornare dalla
rappresentazione testuale a quella di oggetto.
2.3.3 Programmazione ad eventi e callback
Nel momento in cui si utilizza JavaScript per scopi avanzati, quali l’interrogazione dei database o l’utilizzo di chiamate asincrone, ci si trova a dover ragionare in maniera non lineare,
prevedendo un flusso di codice da seguire progressivamente, al contrario, bisogna fornire
continuamente funzioni di callback che vengono eseguite nel momento in cui determinati
eventi si verificano. In sostanza è possibile provocare la chiamata ad una funzione, e fornire
come argomenti le funzioni da richiamare per la gestione dei risultati nei diversi casi.
Questo ragionamento a volte risulta difficile da comprendere fino in fondo e porta a dover
dare al proprio codice una forma molto meno comprensibile. Non è più possibile quindi
determinare in un particolare punto del proprio programma quali operazioni siano state
eseguite e quali no, in quanto ogni chiamata può dare il via ad una reazione a catena parallela in grado di determinare ulteriori eventi e modifiche ai dati in comune. In questa
situazione caotica a volte è necessario cercare di progettare in anticipo alcuni checkpoint
in cui le chiamate secondarie convergono e il flusso principale possa proseguire verificando
preventivamente alcune condizioni.
2.4 Maturità degli standard e limitazioni
Alcuni degli standard relativi alle componenti presentati sono abbastanza maturi da avere un’implementazione coerente tra i browser attuali, mentre vi sono altri casi in cui lo
standard non è ancora pienamente definito, e l’implementazione dei principali browser è determinata, oltre che dalle necessità tecniche, da una differente interpretazione dei concetti
di fondo, o da un assenza di chiarezza su alcune specifiche.
Gli standard W3C per HTML5 sono stati scritti con uno scopo più grande rispetto alla
semplice definizione di interfacce comuni. Prima di HTML5 l’approccio era più descrittivo:
si definiva un componente, quali funzionalità doveva offrire e perché. Gli standard attuali
invece fanno un passo avanti, dando oltre che alla definizione, alcune importanti linee guida
sul funzionamento, che vengono poi utilizzate come base per un implementazione comune
a tutti i browser. Questo approccio è molto importante, perché limitando leggermente la
libertà degli sviluppatori dei browser, rende gli stessi più conformi agli standard, e scongiura
in certi ambiti esplosioni della nota guerra tra browser.
16
2.4.1 Standard per manifest
Lo standard per la componente manifest [1] è ad uno stadio di definizione quasi conclusivo.
Fa parte della definizione dello standard HTML5.
Le funzionalità elencate sono ben descritte, ed attualmente le implementazioni nei browser
principali non differiscono sensibilmente. La chiarezza nella descrizione della componente
deriva sicuramente dalla semplicità del concetto alla base, che la rende una funzionalità solida su cui poter costruire senza troppi pensieri la propria applicazione web. Il file manifest
è riconosciuto sia dai browser webkit (Chrome, Safari e Opera per l’ambiente desktop, i
browser di Android, iPhone e Nokia per l’ambiente mobile) che da Firefox. Rimane invece
inutilizzabile su Internet Explorer, eliminandolo quindi dalla lista dei potenziali utilizzatori di applicazioni web offline, salvo poi effettuare una sostituzione con il motore webkit
mediante il plugin Chrome Frame.
2.4.2 Standard per WebSQL
La stesura dello standard per WebSQL [2] è stata abbandonata in quanto il progetto è
stato sostituito ufficialmente da IndexedDB. Compagnie come Google e Apple continuano
comunque ad utilizzare largamente WebSQL, rendendone di fatto improbabile l’abbandono
completo. Google Docs e molte altre applicazioni si appoggiano in maniera decisa sul
database integrato, alcune proprio per fornire funzionalità offline complete. I browser non
webkit possono utilizzare tramite quale espediente la stessa interfaccia di WebSQL: Firefox
utilizza internamente SQLite per la gestione dei preferiti, ed è possibile raggiungere questa
componente e reindirizzarvi le chiamate destinate originariamente a WebSQL.
2.4.3 Standard per IndexedDB
Lo standard per IndexedDB [3] è attualmente in fase di sviluppo, sebbene i concetti chiave
siano già chiari e difficilmente potranno subire modifiche. Anche in questo caso non si è
inventato ex novo un DBMS ma si sono applicati i concetti esplorati dai database non relazionali e i sistemi di interrogazione sviluppati per MongoDB. IndexedDB è implementato
oltre che su Firefox (Mozilla è stato uno dei principali sostenitori per l’esclusione di WebSQL), su i browser webkit e su Internet Explorer (il quale però non può attualmente essere
preso in considerazione a causa dell’assenza del supporto per il file manifest)
17
3 Applicazioni web offline
3.1 Perchè un’applicazione offline?
Nella parte introduttiva è stato spiegato l’obbiettivo di questa tesi: dimostrare come le
funzionalità offline possano appianare completamente il divario tra le applicazioni web e
desktop. Non si è però discusso degli altri scenari d’applicazione di tali funzionalità. Alcuni effetti secondari della programmazione in ottica offline valgono decisamente la pena di
essere considerati ai fini di un progetto efficiente, offline o non.
Il primo passo nella strutturazione di un’applicazione con supporto offline è la divisione
tra le componenti stabili, cioè quelle che di norma non vengono mai variate tranne in casi
di radicali modifiche all’applicazione, componenti variabili, che cioè vengono poste periodicamente ad aggiornamento, e componenti temporanee come i dati stessi. Escludendo i
dati, che verranno trattati ampiamente più avanti, rimane isolata la struttura portante dell’applicazione, quella principalmente interessata dai vantaggi dell’application caching. La
suddivisione definita può essere individuata all’interno di qualsiasi applicazione web o perfino sito, garantendo la possibilità di estendere queste considerazioni alla quasi totalità del
panorama web.
Le applicazioni web senza politiche di caching specifiche prevedono che l’utente ottenga
una nuova versione dell’applicazione ogni volta che esso vi si collega, garantendo quindi la
freschezza dell’aggiornamento. Con la diffusione delle connessioni ad alta velocità lo scaricamento ripetuto delle pagine potrebbe essere chiaramente accettabile, ma la crescente
diffusione di dispositivi mobile in grado di accedere al web implica delle considerazioni molto importanti a riguardo. La navigazione mobile generalmente non può fare affidamento su
una grande disponibilità di banda, e in molti casi oltre alla scarsa velocità di trasferimento
vengono imposti dai provider dei limiti alla quantità di dati da scaricare. Integrando quindi
la visione iniziale con lo scenario mobile si capisce come la leggerezza di un’applicazione,
e quindi il suo utilizzo intelligente delle risorse e delle connessioni, sia un valore di chiara
importanza.
Organizzare una politica di caching efficace, permette all’utente di non dover mai riscaricare componenti applicativi che già possiede, riducendo i tempi d’attesa, e abbattendo i
costi di trasferimento. In una visione ideale l’applicazione locale scambia solamente i dati
con la controparte remota, arrivando quindi al massimo dell’efficienza.
A causa della natura implicitamente dinamica dell’ambiente web è difficile ipotizzare applicazioni che non siano costantemente in aggiornamento, basti pensare agli sforzi costanti
18
per ottenere una resa perfetta sui vari browser. Un’applicazione web, a differenza di un
comune sito, potrebbe avere la necessità ulteriore di eseguire degli avanzamenti di versione, con conseguente modifica delle funzioni di base o del formato dei dati. In quest’ottica
un’applicazione offline soffre della mancanza d’immediatezza, ma con i dovuti accorgimenti
è possibile progettare un vero e proprio sistema di aggiornamento della propria applicazione.
3.1.1 Un esempio di applicazione esistente: Google Docs
Google Docs è stato progettato come suite di applicazioni d’ufficio basata su web. La
possibilità di avere i propri file ed il proprio editor in qualsiasi browser lo rende un prodotto
eccezionale, ma senza un adeguata componente offline non potrebbe ambire alla sostituzione
completa di altre suite d’ufficio.
L’approccio utilizzato da Google Docs è completo, infatti permette all’utente sia la visione
dei propri file in modalità di sola lettura, che la possibilità di modificarli ed applicare le
modifiche al file principale una volta ripristinata la connessione. Nelle figure 3.1, 3.2 e
3.3 è possibile notare come Google Docs in versione offline sfrutti i database locali per
memorizzare i documenti.
Figura 3.1: una schermata di Google Docs offline
19
Nella figura 3.1 si può vedere come il database locale sia stato popolato dalle entità necessarie a modellare sia i documenti che l’applicazione. In particolare la tabella Documents
contiene i riferimenti a tutti i documenti memorizzati. In questo caso la freccia azzurra
a metà immagine indica il record relativo al documento attualmente aperto, come si può
notare dal titolo del documento e dall’attributo title del record.
Figura 3.2: logo Zucchetti SPA
Nella figura 3.2 viene ispezionata la sezione del database che contiene del corpo del documento. Le frecce sono posizionate in modo da evidenziarne il titolo.
Nella figura 3.3 viene dimostrato come l’aggiornamento della versione online provochi automaticamente una modifica della versione locale. Questa schermata si differenzia dalla prima
poiché Google Docs non sta lavorando in modalità offline. Nonostante questo le modifiche
vengono apportate sia al documento in lavorazione online che alla copia offline, effettuando
quindi una sincronizzazione trasparente che non coinvolge l’utente. Applicando le modifiche
20
Figura 3.3: logo Zucchetti SPA
alla versione locale anche quando c’è connessione con il server si garantisce la possibilità di
poter troncare in qualsiasi momento il collegamento senza perdere la possibilità di utilizzare
l’applicazione. Quest’ultimo punto rappresenta un ulteriore vantaggio di una applicazione
offline ben progettata: la tolleranza ai guasti.
21
3.2 Tipologie di applicazioni offline
Le applicazioni web che offrono funzionalità offline possono essere classificate in tre categorie
principali: applicazioni offline isolate, applicazioni di sola lettura, ed applicazioni complesse.
Le tre categorie hanno scopi differenti e pertanto sono basati su diverse concezioni, prima
tra tutte il ruolo del dato all’interno dell’applicazione.
Nell’applicazione isolata i dati generati localmente (se ve ne sono) vengono utilizzati solamente a livello locale, senza alcuna condivisione con il server. Nell’applicazione offline di
sola lettura i dati sono generati solamente online, e vengono replicati localmente in modo
da poter essere letti in ogni momento. Non è però concessa all’utente la possibilità di modificare tali dati se non utilizzando la versione online. Nell’applicazione complessa i dati
vengono creati sia localmente in modalità offline che remotamente in modalità online. In
quest’ultima variante le strategie di sincronizzazione e gestione dei conflitti (specialmente
con dati condivisi) è di primaria importanza.
3.2.1 Applicazioni isolate
Le applicazioni offline isolate sono forse le più semplici da comprendere poiché l’applicazione
nasce per un utilizzo espressamente offline. Se viene generato qualche contenuto rimane ad
uso esclusivo dell’applicazione locale e non viene sincronizzato con il server.
Un esempio concreto di un’applicazione di questo tipo potrebbe essere una guida interattiva
per un museo o una galleria, composta da i dati relativi alle opere, alle sale e ai percorsi
tematici, che caricata su dispositivi mobile può fornire assistenza offline ai dispositivi dei
clienti. Alla reception potrebbe essere disponibile un collegamento con l’applicazione remota, che una volta visitata replica i propri contenuti all’interno del dispositivo. Una volta
replicata l’applicazione ogni visitatore potrebbe visitare il museo senza necessariamente essere collegato al server principale.
Una funzionalità simile potrebbe non essere necessaria in una struttura moderna e ben informatizzata, ma probabilmente si adatterebbe molto bene a ville storiche, giardini e grandi
parchi.
In un’applicazione simile non è previsto che gli utenti manipolino i dati effettuandovi modifiche, né che i dati vengano aggiornati continuamente. Gli utenti potrebbero generare dei
nuovi dati (ad esempio salvandosi percorsi preferiti o appuntandosi le opere da rivedere alla
prossima visita o da consigliare agli amici), ma non è prevista la sincronizzazione di questi
dati con quelli contenuti nel server.
Ragionando più in un ottica di sviluppo potremmo dire che i dati memorizzati localmente,
cosı̀ come il loro schema di rappresentazione all’interno del database locale, potrebbero essere una copia speculare di quelli inseriti nel server. Un database server potrebbe addirittura
non essere presente, dal momento che non vi sarebbe bisogno di sincronizzazione. I dati
necessari all’utente potrebbero essere inviati al dispositivo contestualmente alle pagine che
compongono l’applicazione e generati quindi localmente.
22
3.2.2 Applicazioni in sola lettura
Le applicazioni offline di sola lettura non sono in realtà applicazioni complete, ma componenti di compatibilità inserite all’interno di un’applicazione web più grande. I contenuti e le
funzionalità dell’applicazione sono accessibili solamente online, ma mantenendo una copia
dello stato dell’applicazione nella memoria locale è possibile visualizzarne i contenuti senza
modificarli.
Un esempio di questo tipo di sistema potrebbe essere un blog reader: mano a mano che
si naviga nei vari blog vengono archiviate localmente tutte le informazioni contenute nei
post, e nel momento in cui viene a mancare la connessione è possibile fornire una versione
di “sola lettura” dei blog visitati.
In questo caso si possono eseguire delle valutazioni sui dati da immagazzinare e sulla struttura dello schema. Un’applicazione gestionale lato server struttura i dati secondo le proprie
logiche, valutando le relazioni tra i dati e suddividendoli in modo da mantenere integrità e
consistenza (ad esempio rispettando i vincoli delle forme normali nello schema del database).
La progettazione del database nel server segue una logica funzionale, considerando quindi
principalmente le componenti, le relazioni tra le stesse ed i moduli che devono manipolarle.
Nel momento in cui si utilizza l’applicazione i dati vengono manipolati, combinati e presentati attraverso una selezione e una proiezione. I dati ottenuti non sono più strutturati
secondo una logica di relazione, ma secondo una logica di presentazione appunto.
Qui si possono introdurre alcune considerazioni sulla tipologia e la struttura dei dati da
memorizzare in modalità offline: se l’obbiettivo della componente offline è solamente quello
di visualizzare i dati archiviati, potrebbe non aver senso mantenere localmente una copia
esatta della struttura dei dati remoti, ma piuttosto una struttura progettata appunto nell’ottica della presentazione. Se i dati lato server sono strutturati finemente, e poi per essere
presentati vengono proiettati in una vista, potrebbe aver senso memorizzare localmente i
dati nel formato elaborato piuttosto che nel formato di partenza.
3.2.3 Applicazioni complesse
Nelle applicazioni offline complesse non vi è un grande sbilanciamento tra componente locale
e remota, poiché entrambe le componenti vengono sviluppate per poter collaborare attivamente. In un’applicazione offline complessa buona parte delle funzionalità della versione
online sono replicate nella versione offline (ovviamente in caso queste abbiano un senso). I
dati online vengono replicati localmente, e le modifiche in modalità offline vengono applicate
ai dati remoti avviando specifiche politiche di sincronizzazione.
Le politiche di sincronizzazione utilizzate da questo tipo di applicazioni devono necessariamente prevedere un sistema di risoluzione di conflitti, anche se non vi è condivisione di dati
tra più utenti. Questo deriva dal fatto che nel momento in cui si replicano i dati remoti su
di un dispositivo, questo rappresenta un unità di memorizzazione a sé stante, con il risultato
che lo stesso utente, utilizzando più dispositivi potrebbe trovarsi a generare delle situazioni
23
di conflitto con i dati che ha utilizzato. Risulta chiaro quindi che qualsiasi applicazione offline complessa si può trovare in diversi livelli di condivisione che possono richiedere diversi
sistemi di gestione dei conflitti: un dato potrebbe essere non condiviso, condiviso tra vari
dispositivi dello stesso utente e infine condiviso tra più dispositivi di più utenti.
Un esempio di un’applicazione del genere potrebbe essere una rubrica telefonica aziendale
o un taccuino per le note condiviso. Ogni utente può accedere all’applicazione in modalità
offline, creare del contenuto sia online che offline, ed avviare la sincronizzazione del dispositivo in momenti differenti, esponendo quindi il sistema al rischio di conflitti di scrittura e
aggiornamento.
La progettazione di un’applicazione di questa tipologia deve necessariamente prevedere una
struttura dati locale molto simile alla struttura dati remota, ma contenente solamente una
sezione dei dati liberamente accessibili all’utente. Valutando le problematiche di sincronizzazione si rende necessaria una progettazione particolare delle operazioni di manipolazione
dei dati, che dovranno cercare di mantenere il più aggiornato possibile lo stato locale e
quello remoto in modo da ridurre il numero di potenziali conflitti.
3.3 Progettazione delle applicazioni
3.3.1 Interfacce distinte o integrazione trasparente
Nella progettazione dell’applicazione è necessario studiare le interazioni che l’utente può
avere con la stessa in un contesto offline. Realizzare una componente offline dedicata, con
una propria interfaccia e meccanismi di presentazione e controllo differenti, può presentare
dei grandi vantaggi in termini di chiarezza, ma implica una duplicazione del codice che ne
aumenta la complessità di manutenzione.
L’applicazione presentata al paragrafo 3.1.1, Google Docs, utilizza due differenti interfacce
per la versione online ed offline. Nella versione offline manca tutta la parte di interfaccia
dedicata alla modifica del documento. Troviamo invece un impostazione dell’ambiente più
ordinato, adatto appunto alla lettura. La dicitura solo visualizzazione in alto nella pagina
fuga ogni dubbio.
Un altra applicazione Google molto famosa è GMail. La versione offline di GMail non solo
presenta un interfaccia differente, ma decisamente progettata per un utilizzo mobile. L’intera struttura della pagina tende a migliorare l’esperienza di utilizzo su dispositivi quali
tablet e smartphone, dimostrando quindi che le funzionalità offline devono essere applicate
principalmente all’ambiente mobile.
Presentare una doppia interfaccia permette di mantenere i due ambienti di utilizzo (online e
offline) nettamente distinti, esponendo funzionalità ed interfacce personalizzate per entrambi, con la possibilità di condividere comunque le strutture di controllo basilari. L’utilizzo
di un approccio a doppia interfaccia migliora la chiarezza per l’utente, riducendo al minimo la possibilità di confondere operazioni offline e online. La realizzazione di una seconda
interfaccia, dove possibile, potrebbe inoltre ridurre la necessità di modificare pesantemente
24
il codice preesistente nel caso una ristrutturazione completa fosse troppo complicata.
L’integrazione delle funzionalità offline in maniera trasparente all’interno di una struttura
preesistente può presentare alcune problematiche, specialmente se la componente di presentazione è posizionata completamente nel server. Come componente di presentazione si
intendono i meccanismi di composizione delle pagine e le componenti che si occupano dell’inserimento dei contenuti nelle stesse. Spostare la componente di presentazione dal server
al client presenta dei vantaggi per quanto riguarda la compatibilità tra offline e online, ed
aumenta la separazione funzionale del codice, rendendo necessaria l’applicazione di design
pattern che regolino il comportamento tra le componenti.
3.3.2 Spostamento della componente di presentazione
Per comprendere il concetto dello spostamento della componente di presentazione da server
a client è necessario comprendere come vengono generalmente progettate le applicazioni
web, e in quali punti si inseriscono i vari linguaggi in base alle loro funzioni.
Generalmente i dati di partenza provengono da un database posizionato nel server che
fornisce l’applicazione, dal quale vengono prelevati tramite istruzioni SQL che li proiettano
in una visione tabellare singola in base alle necessità. I dati passano quindi dalla forma
strutturale relazionale ad una forma composta più semplice che ricorda una tabella. A questo punto il linguaggio server side che ha richiesto i dati li compone in un formato visivo,
generalmente popolando un template precostruito. Il passaggio in cui i dati vengono inseriti
all’interno dell’interfaccia rappresenta la componente di presentazione del sistema. Spesso
nel template vengono incluse anche alcune logiche di controllo basilari in JavaScript, finalizzate alla manipolazione ridotta dei dati o al miglioramento delle funzionalità di interazione
lato client. A questo punto la pagina che è stata composta viene inviata al client che la
riceve e la visualizza mediante l’uso di un browser.
Se la composizione dei dati all’interno della pagina viene realizzata lato server è difficile
inserire correttamente e in maniera trasparente un meccanismo di recupero e di inserimento
dei dati locali, poiché la pagina proveniente dal server è già completa. Inoltre se la pagina
dovesse essere memorizzata localmente per funzionare in offline dovrebbe essere il quanto
più possibile generica: una sorta di interfaccia vuota con meccanismi di controllo dati, che
dovrebbe prelevare i dati dal server o dal client in base alla presenza o assenza di connessione.
Spostare la componente di presentazione verso il client significa quindi prevedere la composizione delle pagine direttamente nel client, costruendo quindi lato server una pagina
abbastanza scarna, con dei meccanismi di composizione lato client molto più avanzati. Un
sistema progettato in questo modo permetterebbe il passaggio da online ad offline senza interruzioni, garantendo quindi la tolleranza ai guasti della rete. Il ruolo dell’applicazione lato
server cambierebbe lievemente, espandendo i proprio compiti in base alle nuove necessità:
verificare la bontà delle operazioni eseguite dal client in base ai limiti imposti, verificare
25
l’integrità dei dati che devono essere sincronizzati, e in generale controllare il flusso di informazioni tra l’applicazione remota e la controparte locale.
Ragionando in ottica di dispositivi mobile vi sono inoltre altri vantaggi in un approccio
simile: trasferendo solamente i dati variati ed i comandi, si ottiene una riduzione del traffico tra il client ed il server, e probabilmente una riduzione del carico del server stesso, dal
momento che buona parte delle operazioni di lettura potrebbero essere realizzate in modalità offline, sgravando quindi il server dal costo di gestione delle richieste, del recupero dei
dati e della generazione delle pagine da inviare al client.
3.3.3 Considerazioni sulle caratteristiche delle reti mobili
Come detto precedentemente, progettare in un ottica online/offline richiede la definizione
di procedure di sincronizzazione ben definite. Ragionando in termini di dispositivi mobile
bisogna considerare alcune caratteristiche della trasmissioni su reti non cablate, che generalmente hanno tempi di latenza superiori alle connessioni su cavo. Questa caratteristica
comporta alcune considerazioni importanti per la progettazione delle comunicazioni.
Dal momento che nell’ambiente mobile i tempi di connessione sono più alti, il peso dei dati
da trasferire passerebbe in secondo piano, con la conseguenza che anche con un contenuto
molto ridotto, il costo di trasferimento in termini di latenza potrebbe essere non indifferente.
Questa osservazione pone quindi in secondo piano la necessità di creare risposte estremamente compatte, e rende invece più efficiente compattare insieme le richieste (comandi o
dati da inviare) per accumulare contenuto e ridurre i tempi medi di trasferimento.
Al fine di ottimizzare i trasferimenti è importante sfruttare al massimo ogni risposta del
server creando una sorta di piggybacking. Il piggybacking è una tecnica utilizzata nella progettazione dei pacchetti a basso livello su reti generiche. Questa tecnica consiste
nell’aggiungere informazioni aggiuntive ai pacchetti trasmessi tra un dispositivo e l’altro,
generalmente pacchetti necessari a regolare la connessione tra le parti o a verificare l’integrità del messaggio. Cercare di utilizzare il piggybacking nel contesto mobile significa
sfruttare i trasferimenti richiesti dalle due applicazioni per trasferire dati aggiuntivi necessari non tanto all’utente ma alle due applicazioni in sé.
Un esempio di funzionamento potrebbe essere questo: l’applicazione locale esegue delle
operazioni di modifica ad alcuni dati, e li mantiene in un area temporanea per la sincronizzazione. Nel momento in cui si rende necessaria una comunicazione con il server, magari
per recuperare un file allegato o una qualsiasi risorsa, si inoltra insieme alla richiesta del
contenuto dei comandi di sincronizzazione ed un marcatore che indichi lo stato locale. Il
server, ricevendo la richiesta, interpreterebbe anche i comandi aggiuntivi e verificherebbe lo
stato dell’applicazione locale grazie al marcatore. Una volta eseguite queste operazioni il
server comporrebbe i dati da inviare richiesti da una pagina in un pacchetto da trasferire,
a cui allegherebbe l’esito dei comandi richiesti.
In questo semplice schema abbiamo compattato in una richiesta ed una risposta:
• l’iniziale richiesta della pagina al server
26
• la risposta del server che fornisce la pagina
• l’invio da parte del client dei comandi da eseguire
• la risposta da parte del server riguardo l’esecuzione dei comandi richiesti
• la richiesta o l’inoltro da parte del client del marcatore di sincronizzazione
• la risposta o la valutazione e risposta da parte del server con il proprio marcatore
In base alle necessità è poi possibile includere buona parte dei dati in chiamate asincrone
di questo tipo, ottimizzando quindi i tempi di apertura delle connessioni e diminuendo il
costo medio del trasferimento dei dati.
3.4 Il manifest
Come introdotto nel paragrafo 3.1, per progettare un’applicazione offline è necessario distinguere il progetto in due parti: una parte statica che verrà memorizzata localmente
nell’application cache ed una parte dinamica che verrà richiesta al server ogni volta in cui
viene utilizzata.
È importante eseguire un ragionamento appena si opera questa distinzione: mentre la parte
statica può essere sottoposta ad una sorta di versionamento dell’applicazione condizionato dalla versione del file manifest, la componente dinamica non può essere versionata in
modo analogo. Questa considerazione indica che nel momento in cui vengono modificate
sostanzialmente alcune componenti dinamiche dell’applicazione lo stato globale della stessa
potrebbe trovarsi in una condizione di incoerenza. Dividere l’applicazione comporta alcune
problematiche nel momento in cui il programma viene modificato, ed in una buona parte
di applicazioni web le modifiche sono all’ordine del giorno. Modificare la parte online è
semplice: nel momento in cui il client si connette troverà la nuova versione dell’applicazione
e la parte locale dialogherà con essa. Ciò rappresenta un problema in caso l’applicazione
locale non abbia eseguito lo stesso aggiornamento di versione della parte remota e tenti di
dialogarvi secondo un formato non più utilizzato. Questa incoerenza tra le due applicazioni
deve essere gestita in fase di progettazione, predisponendo appositi meccanismi di avanzamento di versione.
Il meccanismo del file manifest prevede già un meccanismo di aggiornamento, che però
può risultare ingestibile in alcuni casi. Ogni volta che viene rilevata una variazione al file manifest tutta la parte statica dell’applicazione viene eliminata e riscaricata. Questo
comportamento risulta drastico in alcuni casi e potrebbe rappresentare un problema, specialmente se l’aggiornamento avviene tra una schermata e l’altra dell’applicazione o se a
causa dell’aggiornamento della stessa i dati archiviati in memoria si vengano a trovare in
una situazione di inconsistenza. I problemi principali in questo sistema di aggiornamento
sono due: l’impossibilità di definire un unico punto di aggiornamento e l’impossibilità di
rifiutare un aggiornamento e mantenere la versione corrente dell’applicazione. Nei capitoli successivi verranno proposti alcuni stratagemmi utili a evitare di incappare in uno dei
problemi elencati.
27
3.4.1 Funzionamento del file manifest
Il file manifest è un semplice file testuale contenente l’elenco delle risorse utilizzate dall’applicazione. Questo file è suddiviso in tre sezioni: cache, network e fallback. Nella sezione
cache vengono elencate le risorse che devono essere memorizzate localmente (in un area di
memoria chiamata application cache), nell’area network vengono elencate tutte le risorse
che devono essere richieste al server ad ogni utilizzo, e nell’area fallback sono elencate una
serie di risorse da utilizzare in mutua esclusione in base alla presenza o meno della connessione. In sostanza ogni elemento della versione fallback specifica quale elemento offline
utilizzare in caso venga richiesto un file remoto in assenza di connessione.
Le due aree cache e network rappresentano quindi la parte statica e dinamica dell’applicazione, ed istruiscono il meccanismo di memorizzazione su quali risorse richiedere e mantenere
localmente.
Nel momento in cui viene visitata per la prima volta una pagina contenente il file manifest,
il browser reagisce lanciando una serie di eventi che avviano dei processi di sincronizzazione
tra la versione in application cache e quella fornita dal server. Una volta che le risorse
cache sono state completamente scaricate e memorizzate l’applicazione procede con il suo
normale funzionamento, anche in assenza di connessione. Ogni volta che viene utilizzata
una risorsa elencata in un file manifest il browser cerca di recuperare nuovamente il file
manifest dell’applicazione dal server. Se questo file non è disponibile (com’è normale che
sia in assenza di connessione) l’applicazione prosegue con il suo normale funzionamento. In
caso invece sia presente una connessione il file manifest viene scaricato nuovamente, e viene
confrontato con la copia locale. Se i due file risultano identici l’application cache non viene
aggiornata, ma se vi è una qualsiasi differenza tra i due file vengono richieste nuovamente
tutte le risorse elencate, e l’application cache relativa a tale file manifest viene cancellata. In
sostanza i passaggi del processo di caching controllato attraverso il manifest sono i seguenti:
Primo caso: nessun elemento in cache
• riconoscimento di un file manifest nella pagina
• (evento checking) controllo di differenze tra il file manifest locale ed il file manifest
remoto
• (evento downloading) viene avviato un download massiccio dell’applicazione
• (evento progress) vengono scaricati i file necessari e memorizzati nell’application
cache
• (evento cached) terminato il download dei file viene lanciato l’evento cached, che
informa il browser che da quel punto in poi dovranno essere utilizzate le risorse locali
Secondo caso: nessuna variazione nel manifest o manifest irraggiungibile
• utilizzo di una risorsa memorizzata nell’application cache
28
• (evento checking) controllo di differenze tra il file manifest locale ed il file manifest
remoto
• (evento noupdate) al browser viene confermata la versione memorizzata nell’application cache
Terzo caso: applicazione in application cache e variazione nel manifest
• utilizzo di una risorsa memorizzata nell’application cache
• (evento checking) controllo di differenze tra il file manifest locale ed il file manifest
remoto
• (evento downloading) verificata una differenza nel manifest viene avviato un download massiccio dell’applicazione
• (evento progress) vengono scaricati i file necessari e memorizzati nell’application
cache
• (evento updateReady) una volta scaricati tutti i file che compongono l’applicazione
notifico la disponibilità ad un aggiornamento di versione, da questo momento in poi
si utilizzerà la nuova application cache
Si è parlato più volte di file manifest referenziato da una pagina, per essere più specifici il
file manifest è un attributo del tag <HTML> che apre la pagina. Il contenuto dell’attributo è
necessariamente un indirizzo URL con mimetype text/cache-manifest e estensione .appcache, quindi il server deve essere istruito per fornire questo file con l’opportuno mimetype.
Vi è una precisazione importante da fare riguardo al primo punto della scaletta descritta.
Come spiegato precedentemente la proceduta di caching dell’applicazione non viene eseguita
solamente in caso venga richiamata una pagina con l’attributo manifest all’interno del tag
<HTML>, ma anche in caso venga utilizzata una qualsiasi pagina contenuta in application
cache ed elencata all’interno di un file manifest.
Un esempio potrebbe essere questo: un’applicazione composta da un set di pagine contiene
l’attributo manifest solamente nella pagina di ingresso. Al primo avvio dell’applicazione
viene scaricato tutto il set di pagine. Ci si aspetterebbe che il file manifest venga verificato
solamente quando si visita la pagina principale, dal momento che non è referenziato nelle
altre pagine. Il file manifest viene invece ricontrollato ogni volta che una pagina richiesta
viene fornita dall’application cache, rendendo quindi impossibile ottenere un singolo punto
di controllo del file manifest. Il concetto del singolo punto di controllo può essere vista come
il problema del punto d’aggiornamento, infatti se questo comportamento non venisse regolato con qualche stratagemma risulterebbe impossibile forzare l’applicazione ad aggiornarsi
in un punto ben definito, adibito al cambiamento di versione e lontano dai passaggi critici.
29
3.4.2 Stati dell’applicationCache e relativi eventi
L’oggetto window.applicationCache racchiude in sé lo stato e le funzioni di controllo
dell’application cache.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
DOMApplicationCache
oncached : null
onchecking : null
ondownloading : null
onerror : null
onnoupdate : null
onobsolete : null
onprogress : null
onupdateready : null
status : 1
__proto__ : DOMApplicationCache
CHECKING : 2
DOWNLOADING : 3
IDLE : 1
OBSOLETE : 5
UNCACHED : 0
UPDATEREADY : 4
addEventListener : function addEventListener () { [ native code
] }
constructor : function DOMApplicationCache () { [ native code ]
}
dispatchEvent : function dispatchEvent () { [ native code ] }
removeEventListener : function removeEventListener () { [
native code ] }
swapCache : function swapCache () { [ native code ] }
update : function update () { [ native code ] }
__proto__ : Object
Listing 3.1: Struttura dell’oggetto window.applicationCache
Gli elementi dalla linea 2 alla 9 sono gli eventi che vengono scatenati come discusso precedentemente, alla linea 10 abbiamo lo stato dell’application cache che può assumere i valori
da 0 a 4 come specificato nelle linee dalla 12 alla 17.
Nel momento in cui viene riconosciuta una variazione nel file manifest avviene, come precedentemente descritto, un download automatico della nuova versione dell’applicazione. In
questo momento tutte le nuove risorse appena scaricata compongono un application cache
più aggiornata, mentre la pagina continua a vivere con le risorse indicate nella application
cache della versione precedente. In questo caso l’application cache è pronta per l’avanzamento di versione e si pone nello stato di UPDATEREADY. Intercettando questo cambiamento
di stato è possibile invocare il comando swapCache() che forzando il cambiamento di application cache aggiorna la pagina corrente alla versione corretta.
Una caratteristica che è importante sottolineare è l’impossibilità di gestire selettivamente il passaggio da una versione dell’applicazione ad una versione successiva. La possibilità
fornita dall’oggetto che espone l’applicationCache è limitata alla decisione se passare istan-
30
taneamente alla nuova versione dell’applicazione o se attendere il prossimo ricaricamento
o cambiamento di pagina. Non vi è attualmente alcun modo per interrompere il sistema
di aggiornamento e riprenderlo in un secondo momento, magari dopo aver richiesto una
conferma esplicita all’utente, e ciò può portare ai problemi di inconsistenza presentati in
apertura al paragrafo.
3.4.3 Problematiche relative allo stato updateReady e inconsistenza
dell’applicazione
In caso l’applicazione sia in stato di updateReady, tutte le risorse a cui essa si riferisce sono
prelevate dalla vecchia versione dell’application cache, a meno che non sia stato eseguito
un comando di swapCache(). È importante fare questa precisazione, perché permette di
stabilire lo stato di coerenza della pagina attualmente in esecuzione. È stato verificato
durante l’esecuzione del progetto di stage il comportamento effettivo di questo meccanismo,
ed esso non ha presentato differenze rispetto a quanto descritto. Il test è stato eseguito in
questo modo:
• Ci si è posizionati nella pagina principale, contenuta nell’application cache,
• sono state apportare modifiche sia alla pagina in questione, sia a risorse esterne (in
questo caso un file JavaScript in cui sono state aggiunte delle chiamate di debug),
• È stata aggiornata la pagina, con conseguente download in background della nuova
versione,
• È stata verificata l’assenza di modifiche alla pagina principale e l’esecuzione dello
script senza chiamate di debug,
• È stato eseguito un refresh ed osservato l’aggiornamento della pagina e l’esecuzione
dello script con le chiamate di debug.
Questa verifica ci permette di fare un assunzione di coerenza sulla pagina in visione al
momento dell’aggiornamento della cache. L’unico punto di incoerenza è rappresentato da
chiamate asincrone ad altre pagine con trasferimento di dati.
Immaginando che la pagina in questione sia alla versione X e pensi di dialogare con una
pagina secondaria secondo un formato dati specificato e coerente nella versione X, nel momento in cui la pagina subisce un aggiornamento di application cache senza essere ricaricata,
tenterebbe di dialogare con la pagina secondaria secondo il protocollo della versione X, ma
la pagina secondaria avrebbe già eseguito l’aggiornamento di versione, magari sostituendo
il protocollo di comunicazione della versione X con quello della versione X+1.
Una versione più semplice del problema potrebbe prevedere nella pagina principale un
link interno all’applicazione con dei parametri di tipo GET codificati tramite querystring,
il cui schema viene cambiato dalla versione X alla X+1. Transitando dal link della index in
versione X finiremmo per accedere al file di versione X+1, al cui interno magari uno script
tenta di recuperare i parametri GET ipotizzando un diverso formato.
31
3.4.4 Interferenza con la cache standard
L’utilizzo della memoria application cache, generata a partire dalle risorse elencate dal file
manifest interagisce anche con il sistema di caching generico delle pagine. Purtroppo le
risorse scaricate per essere inserite nell’application cache vengono memorizzate anche nella memoria cache del browser alla prima richiesta, generando una fastidiosa interferenza.
Mentre il file manifest rappresenta un sistema chiaro di controllo della gestione delle pagine
locali, la browser cache ragiona secondo logiche determinate dal produttore del browser,
come presentato al paragrafo 2.1.1.
L’interferenza tra i due sistemi rappresenta un problema quando avviene un avanzamento di
versione (ipotizzando di nuovo un avanzamento dalla versione X alla versione X+1), poiché
parte dei file richiesti dall’aggiornamento del manifest potrebbero essere recuperati dalla
memoria cache locale e non dalla rete, producendo quindi uno stato di alta incoerenza tra
le pagine dell’applicazione, che si troverebbero parte in una versione aggiornata del sistema
e parte in un altra, senza nessuna possibilità di avanzare coerentemente alla versione più
recente fino alla prossima modifica del file manifest. La divisione quindi non sarebbe più
solamente tra parte statica e parte dinamica, ma anche tra parte statica soggetta ad aggiornamento controllato e parte statica non più aggiornabile.
Per evitare questo comportamento riscontrato è necessario specificare delle politiche di
caching esplicite nei singoli file, imponendo quindi che non vengano mai memorizzati nella
cache del browser, ma solo nell’application cache. Comprendere questa particolarità può
chiarire alcuni dubbi relativi a comportamenti incoerenti delle applicazioni offline.
Un altra strategia potrebbe essere utilizzare i due sistemi in collaborazione, dividendo cioè
l’applicazione in parte dinamica online, parte statica offline soggetta ad aggiornamenti e
parte statica offline non soggetta ad aggiornamenti. Utilizzare un approccio simile può
fornire un vantaggio relativo al tempo di aggiornamento, poiché purtroppo a qualsiasi modifica del file manifest corrisponde uno scaricamento di tutti i file dell’application cache,
non solamente la sezione realmente modificata. In caso l’applicazione offline sia corposa,
scaricarla completamente ad ogni aggiornamento potrebbe essere improponibile. Combinando la memoria cache con l’application cache sarebbe possibile recuperare i file soggetti
a cambiamento dalla rete, ed i file che per loro natura non vengono modificati (magari i più
pesanti quali immagini, video e quant’altro) dalla memoria cache, senza attendere tempi di
scaricamento inopportuni.
32
3.4.5 Problematiche relative al caching delle pagine e all’asincronia degli eventi
Durante il caching delle risorse elencate nel manifest vengono lanciati gli eventi descritti
precedentemente. Teoricamente si potrebbero intercettare gli eventi per creare una barra
di progressione che informi l’utente della percentuale di completamento del processo di caching. In realtà a causa della velocità di trasferimento e della gestione interna dei thread del
browser gli eventi vengono scatenati nell’ordine corretto, ma solo una volta eseguite tutte
le operazioni. Per rendersi conto di questo problema è stato utilizzato un programma con
la funzione di filtro, in grado di rallentare la velocità di scaricamento a piacere, rendendo
possibile uno studio accurato dei passaggi eseguiti dall’applicazione.
Creando ad esempio una semplice serie di listener in questo modo è possibile intercettare
gli eventi di progressione dell’application cache:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
applicationCache . addEventListener ( ’ cached ’ , cachingStop ,
false ) ;
applicationCache . addEventListener ( ’ downloading ’ ,
cachingStart , false ) ;
applicationCache . addEventListener ( ’ progress ’ ,
cachingProgress , false ) ;
function cachingStart ( ev ) {
console . log ( " caching iniziato " , ev ) ;
}
function cachingProgress ( ev ) {
console . log ( " progresso " , ev ) ;
}
function cachingStop () {
console . log ( " caching concluso " ) ;
}
Listing 3.2: esempio: cache listener
L’evento progress ha due attributi che possono tornare utili per la progress bar: loaded che
indica il numero progressivo della risorsa scaricata e total che indica ovviamente il totale
delle risorse da scaricare. Avendo questi due valori è possibile fornire una progress bar non
basata sul tempo d’esecuzione del download, ma sulla progressione delle risorse richieste.
Purtroppo in ambiente mobile questi due attributi non sono attualmente disponibili, ma è
comunque possibile eseguire un polling per verificare la completezza dello scaricamento.
Vi è però un problema relativo al posizionamento della chiamata JavaScript che associa
i listener agli eventi. Posizionando il codice JavaScript in fondo alla pagina tutte le chiamate di tipo console.log avvengono una volta terminati i download delle risorse, rendendo
quindi inutile presentare una progressbar una volta completato il download.
Posizionando lo script nel tag <HEAD> come prima istruzione otteniamo un miglioramento:
una prima parte di risorse vengono scaricate (circa la metà secondo i test effettuati), do-
33
podiché vengono notificati gli eventi di checking, download e parte degli eventi progress. A
questo punto può capitare nel caso migliore che ogni evento progress venga lanciato correttamente all’inizio di ogni download, oppure nel caso peggiore che tutti gli eventi progress
vengano lanciati in blocco una volta terminati tutti i download, ottenendo una barra di
progress che si completa in due singoli passaggi.
Purtroppo il file manifest viene interpretato appena viene processato il relativo attributo nel
tag <HTML>, e non esiste punto più vicino al tag <HTML> della prima linea del tag <HEAD>. In
questo sistema risulta quindi impossibile intercettare l’evento download prima dell’effettivo
avvio dello scaricamento. Nel paragrafo 3.4.8 viene proposto una soluzione ad un altro
problema che fortunatamente risolve anche quanto appena descritto.
3.4.6 Svuotamento dell’application cache
Nel momento in cui il browser rileva una nuova versione del file manifest, avvia le politiche
descritte precedentemente per la gestione dell’application cache in cui è contenuta l’applicazione in esecuzione.
Vi è però un caso particolare in cui l’application cache viene posta in stato obsolete, che
sta ad indicare che l’application cache in utilizzo non è più necessaria e dev’essere quindi
abbandonata e cancellata. Ciò avviene quando il file manifest viene rimosso dal server o
viene reso inaccessibile. In questo caso l’applicazione interpreta questo cambiamento come
un comando di rimozione dell’application cache che viene quindi pulita completamente.
Questa soluzione può essere utile in caso ci si trovi nella condizione di dover sospendere tutte le funzionalità offline e ritornare ad una versione di applicazione perennemente
connessa.
3.4.7 Effetto BlankPage
È stato notato un comportamento insolito al primo ingresso in un’applicazione con manifest
non ancora presente in memoria: un blocco del rendering della pagina (la composizione e
visualizzazione della stessa), che rimane bianca fino al termine del download delle risorse
elencate nel manifest. Essendo registrato nel file manifest l’intero elenco delle risorse necessarie all’applicazione è possibile ipotizzare che questo ritardo cresca proporzionalmente
all’aumentare della dimensione dell’applicazione. Questo effetto non viene rilevato quando
la pagina è già stata visitata almeno una volta, o quando essa è già stata memorizzata in
cache o application cache e subisce un aggiornamento di manifest. Probabilmente ciò avviene perché al primo avvio non si ha ancora nessun dato relativo alla pagina da mostrare,
ed il download delle risorse listate nel manifest assume la priorità assoluta, impedendo il
download dei file relativi alla pagina.
Aggiungendo questo comportamento all’asincronia delle chiamate descritta precedentemente si presenta uno scenario spesso inaccettabile per la maggior parte degli utenti: al primo
34
avvio dell’applicazione ci si presenta una schermata bianca immobile, e dopo aver atteso
il tempo necessario a scaricare tutta l’applicazione viene disegnata parte della pagina e si
viene trasportati istantaneamente in una pagina secondaria che ci informa della progressione (ultimata) del download dell’applicazione. Dal momento che spesso in ambiente web la
prima impressione è quella decisiva non è il caso di presentare ad un utente un’applicazione
che come prima azione sembra bloccare il browser del proprio dispositivo, mobile magari.
La tolleranza degli utenti per un applicazione che fallisce al primo tentativo è estremamente
bassa, e per certi contesti potrebbe rappresentare un’opzione inaccettabile.
3.4.8 Gestire l’effetto BlankPage
Per evitare l’effetto BlankPage iniziale è possibile utilizzare uno stratagemma che è stato
ideato dopo aver riscontrato questo fastidioso effetto. La pagina bianca viene presentata
solamente quando si entra per la prima volta nella pagina iniziale dell’applicazione contenente il file manifest. Se la pagina non ha riferimenti interni al file manifest il suo tempo di
caricamento e di rendering risulta normale, e quindi accettabile. Sfruttando le caratteristiche del file manifest, ed in particolare il fatto che ogni risorsa listata al suo interno provochi
una verifica dello stesso quando viene utilizzata in seguito, possiamo predisporre un sistema
che abbia un accesso rapido all’applicazione, presenti una barra di progressione ben temporizzata, e fornisca anche successivamente un punto di aggiornamento per il sistema.
Una prima alternativa potrebbe essere rimuovere il file manifest dalla pagina di ingresso
(forzando gli utenti ad accedere all’applicazione solamente attraverso tale pagina), valutare
lo stato dell’oggetto window.applicationCache e se trovata vuota, richiedere una pagina
secondaria contenente il manifest (definita pagina di setup), in cui avviare automaticamente
i meccanismi di loading dell’application cache.
In realtà questo sistema risolve il problema dell’effetto BlankPage nella pagina iniziale,
ma lo sposta alla pagina di setup. Abbiamo quindi ottenuto una schermata iniziale che
presenta l’applicazione seguita da una lunga schermata bianca. La soluzione risulta quindi
ancora incompleta.
Per migliorare la soluzione potrebbe essere utilizzato un iframe nascosto nella pagina di
setup, creato dinamicamente via JavaScript, relativo ad una pagina bianca contenente solo
il riferimento al manifest. L’iframe nascosto avrebbe un proprio oggetto window, il quale si
occuperebbe di eseguire il caching senza bloccare l’esecuzione della pagina principale.
Analizziamo in maniera più dettagliata il meccanismo:
All’ingresso nella prima pagina dell’applicazione viene valutato lo stato dell’application cache. Se l’applicazione è già stata salvata nell’application cache quest’ultima si può trovare
in due stati: IDLE che indica nessuna operazione in corso (quindi nessun aggiornamento
eseguito) e UPDATEREADY che indica la possibilità di eseguire un avanzamento di versione dei file contenuti nell’application cache.
35
Se l’applicazione non è stata ancora salvata nell’application cache lo stato di quest’ultima sarà UNCHACHED. Rilevando questo stato è possibile spostarsi nella pagina di setup
senza che siano ancora state avviate politiche di caching dell’applicazione.
All’ingresso della pagina di setup (che presenterà una progress bar vuota e le altre voci
utili all’inizializzazione dell’applicazione, come i passaggi che eseguirà l’applicazione per
configurare il database) viene generato dinamicamente via JavaScript un iframe nascosto,
avente come indirizzo un ipotetico file manifest_loader.html che contiene al suo interno
solamente il riferimento al file manifest e gli script che legano gli eventi dell’application
cache ai listener della finestra principale.
In questo modo il download dell’applicazione viene avviato solamente quando viene generato l’iframe, e l’effetto blank page viene spostato in secondo piano, precisamente sull’iframe
nascosto. In questo modo abbiamo evitato l’effetto BlankPage iniziale ed abbiamo inoltre ottenuto un evento di inizio download effettivamente precedente all’inizio del caching
dell’applicazione, risolvendo quindi anche il problema dell’asincronia degli eventi descritta
precedentemente. L’evento di avvio del download coincide con la creazione dell’iframe, e
collegando tutti gli eventi di caching dell’iframe alla finestra principale possiamo visualizzare
al suo interno la progressione delle risorse scaricate.
3.4.9 Differenza tra assenza di connessione e tolleranza ai guasti
L’utilizzo di una sezione di fallback nel file manifest permette di creare alcune dinamiche
di gestione basilari della connessione. Utilizzando correttamente le istruzioni di fallback è
possibile specificare un diverso funzionamento per la versione offline e la versione online dell’applicazione. Una gestione orientata alle risorse però ha delle pesanti limitazioni, poiché
si dovrebbe teoricamente duplicare l’intera sezione da fornire in modalità offline senza avere
un effettivo potere decisionale a tempo d’esecuzione (dal punto di vista delle operazioni
JavaScript). In parole povere un utilizzo avanzato della sezione fallback non permetterebbe
una condivisione avanzata di codice tra la versione offline ed online, e renderebbe impossibile il passaggio istantaneo dall’una all’altra.
Una gestione più avanzata della connessione passa attraverso l’oggetto window.navigator
esposto dal browser. L’oggetto window.navigator, oltre ad offrire un attributo che indica
se lo stato del browser e del sistema operativo sottostante è online o offline, viene notificato
anche in caso avvengano una serie di eventi relativi alla cache. Gli eventi che è possibile
intercettare vanno dall’aggiornamento della cache, al download dei file e alla rilevazione di
errori.
Per una descrizione degli eventi nel dettaglio é possibile consultare la specifica degli eventi
di caching delle risorse nell’application cache[4].
36
Un esempio di gestione avanzata della connessione può essere il seguente. Il codice riportato
simula il funzionamento di un sistema di notifica in stile Twitter. Viene richiesto lo stato
all’utente, che verrà poi inviato al server in caso l’applicazione sia in stato connesso, o verrà
memorizzato localmente in caso vi sia assenza di connessione. Vengono poi legate delle
funzioni alle variazioni di stato della connessione, realizzando quindi un sistema che rileva
la possibilità di dialogo con il server ed agisce di conseguenza inviando gli stati rimasti in
locale.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function w h at Is Y ou rC u rr e nt St a tu s () {
var status = window . prompt ( " Qual ’e ’ il tuo stato attuale ? " ) ;
if (! status ) return ;
if ( navigator . onLine ) {
sendToServer ( status ) ;
} else {
saveStatusLocally ( status ) ;
}
}
function sendLocalStatus () {
var status = readStatus () ;
if ( status ) {
sendToServer ( status ) ;
window . localStorage . removeItem ( " status " ) ;
}
}
window . addEventListener ( " load " , function () {
if ( navigator . onLine ) {
sendLocalStatus () ;
}
} , true ) ;
window . addEventListener ( " online " , function () {
sendLocalStatus () ;
} , true ) ;
window . addEventListener ( " offline " , function () {
alert ( " Sei entrato in modalita ’ offline . I tuoi
aggiornamenti di stato verranno inviati quando tornerai
online . " ) ;
} , true ) ;
Listing 3.3: esempio di gestione della connessione
Questo sistema sembrerebbe funzionare bene concettualmente, ma si appoggia allo stato
logico della presenza o assenza di connessione fornito dal browser. Purtroppo in base ai test
effettuati esso non sempre è concorde con il reale stato del sistema.
Togliere il cavo di rete da un computer non provoca istantaneamente un aggiornamento dello
stato logico. Analogamente in ambiente mobile un assenza di connessione determinata dal
passaggio del dispositivo in modalità offline o aeroplano comporta i relativi aggiornamenti
dello stato logico dell’applicazione, mentre un comune passaggio in galleria o perdita di
37
segnale non vengono rilevati, provocando quindi un completamento incoerente. Ci si può
quindi trovare nel caso in cui un applicazione ben organizzata per funzionare in modalità
offline si trovi poi a non sfruttare mai tale componente a causa di un errore si valutazione
dello stato del dispositivo.
Vi è un altro punto che vale decisamente la pena di sottolineare: la presenza di connessione del server. Si è finora valutata la presenza di connessione solamente tra il dispositivo
e la rete, assumendo che ciò bastasse a garantire una comunicazione con la componente
offline dell’applicazione.
Questa visione è incompleta, poiché potrebbe venire a verificarsi la situazione in cui sia il
server a non essere raggiungibile dalla rete, e non il dispositivo client. Con i moderni sistemi
di backup si potrebbe tranquillamente affermare che con un minimo investimento sarebbe
possibile mantenere un server online in ogni situazione, ma estendendo questa considerazione all’ambiente aziendale ci si rende conto di come l’impossibilità di raggiungere il server da
qualsiasi postazione per ragioni di sicurezza sia in realtà una situazione piuttosto comune.
L’ipotetica applicazione aziendale, progettata per funzionare nel browser del portatile del
dipendente con supporto offline, potrebbe avere come scopo la generazione di grafici e report
nel server , e la loro memorizzazione locale in sola lettura per poter essere presentati in altre
sedi o altre aziende senza necessariamente aprire una connessione dal proprio portatile al
server aziendale (anche per motivi di sicurezza).
Accedendo all’applicazione ci si troverebbe nello stato problematico in cui il dispositivo
può contare su una connessione alla rete, ma non riesce a raggiungere la controparte remota dell’applicazione. In questo caso la componente offline potrebbe essere completamente
ignorata, rendendo di fatto impossibile la presentazione.
Queste considerazioni rendono chiaramente necessaria una valutazione della connessione differente, non basata sugli stati logici del sistema, ma sull’effettiva presenza di un collegamento
stabile tra applicazione client e controparte server.
3.4.10 Implementare il rilevamento della connessione
Per realizzare delle vere e proprie funzionalità offline con tolleranza ai guasti è possibile
muoversi in diversi modi, tra cui la sostituzione dell’oggetto window.navigator.onLine
con un indicatore più “intelligente” e l’utilizzo di chiamate asincrone con relative funzioni
di callback eseguite in base al contenuto ottenuto. In entrambi casi la sezione fallback del
manifest viene ignorata, ed è quindi necessario utilizzarla solamente per scopi basilari e non
per elaborate politiche di gestione della connessione.
Uno scopo basilare adatto alle attuali politiche di utilizzo della sezione fallback potrebbe servire per diminuire la quantità di risorse necessarie per la versione offline. In caso si
decida di implementare le versioni offline ed online di un applicazione nella stessa interfaccia, sarebbe possibile decidere di ridurre le risorse da memorizzare in application cache per
mantenere libero più spazio possibile. Si potrebbero prevedere delle risorse alternative, più
leggere, da utilizzare nell’applicazione offline. Le immagini potrebbero essere sostituite con
risorse più leggere ed i video potrebbero a loro volta essere sostituiti con immagini.
38
La prima soluzione proposta al problema della reale presenza di connessione prevede la decisione delle risorse da utilizzare non più basandosi sull’oggetto window.navigator.onLine,
ma su di un test pratico di connessione. Un iniziale chiamata a una delle risorse del server
deve quindi occuparsi di testare effettivamente la presenza di un collegamento. L’inserimento di un test simile non è tutto sommato un operazione cosı̀ invasiva, ma comporta
l’aggiunta delle chiamate di prova prima della richiesta di risorse al server. Questa tecnica
può essere utilizzata per la richiesta di risorse in modalità sincrona come ad esempio i link:
prima di spostarsi in una pagina memorizzata nel server e listata nella sezione network
del file manifest è possibile testare l’effettiva connessione e successivamente ridirezionare la
chiamata verso una risorsa memorizzata nell’application cache. Questo sistema in sostanza
simula il funzionamento della sezione fallback, ma testando la connessione esplicitamente
prima della richiesta di una risorsa.
Bisogna però ricordare le considerazioni espresse nel paragrafo 3.3.3 riguardo al costo delle
connessioni in ambiente mobile. Duplicare tutti i tentativi di connessione potrebbe essere
un opzione non ammissibile.
Nel caso il sistema di navigazione tra le pagine incorpori già la possibilità di utilizzare
chiamate asincrone, ad esempio per l’invio dei dati di un modulo ad una pagina secondaria
(come nel framework per sviluppo web mobile JQueryMobile), non è necessario testare in
anticipo la connessione, ma semplicemente fornire un adeguata funzione di callback da attivare in caso di fallimento.
Lo scenario più facile da immaginare è proprio l’invio di dati tramite una chiamata di tipo
POST o GET ad una pagina server. Dovendo recuperare i parametri forniti per elaborarli
lato server, si ipotizza che la pagina da contattare sia elencata nella sezione NETWORK
del file manifest. In questo caso non è necessario testare in anticipo la connessione, è sufficiente tentare l’invio dei dati, ed in caso non sia possibile contattare la pagina necessaria,
avviare una seconda chiamata asincrona verso una pagina in application cache che si occupi
di gestire il salvataggio dei dati in una memoria temporanea.
In questo modo si divide in tre fasi il processo descritto precedentemente. L’utente genera
il contenuto utilizzando la componente locale offline, la quale tenta di inviarlo alla componente di memorizzazione online, ed in caso di fallimento ripiega comunicando i dati inviati
ad una componente di memorizzazione offline sostitutiva.
Ragionando in termini di sincronizzazione, in realtà coinvolgere sempre anche la componente di memorizzazione offline sostitutiva potrebbe rappresentare un vantaggio. Procedendo
in questo modo sarebbe possibile minimizzare le differenze tra la versione dei dati remota e
quella locale, ottimizzando quindi le politiche di sincronizzazione.
39
3.5 Considerazioni conclusive
Le considerazioni espresse finora sono decisamente generiche, e devono essere contestualizzate al caso di utilizzo. Generalmente per quanto verificato durante lo stage, occorre valutare
tutte le possibilità e combinare al meglio le alternative offerte per creare delle soluzioni
personalizzate per il problema, valutandone le caratteristiche e gli obbiettivi.
Per la progettazione di un’applicazione offline realmente funzionale non è possibile utilizzare un approccio classico ed introdurre una piccola variante pensando di guadagnare
istantaneamente la libertà di utilizzo in qualsiasi ambiente. Realizzare un applicazione offline completa significa abbandonare completamente la progettazione web, e calarsi in un
contesto di progettazione di applicazioni client-server, valutando costantemente tutte le limitazioni imposte dal browser, che diventa in sostanza un contenitore di applicazioni, una
macchina virtuale in cui eseguire le proprie applicazioni.
La progettazione di un’applicazione offline è più complessa di una normale progettazione di applicazioni web o desktop, perché prevede la combinazione delle due realtà, cercando
di trarre i vantaggi da entrambe e minimizzando le problematiche. Dal momento che le
applicazioni web hanno delle caratteristiche di portabilità decisamente convincenti, risolvere il problema dell’assenza di connessione permetterebbe di estendere il funzionamento di
un’applicazione non solo a molteplici dispositivi, ma anche a molteplici scenari d’uso.
È necessario però valutare ampiamente a che livello di dettaglio scendere con le funzionalità
offline, poiché mano a mano che si amplia il set di funzioni supportate ci si espone sempre
di più ad una possibile inconsistenza dei dati (tra più utenti, tra più dispositivi, e anche
tra diverse versioni della stessa applicazione) ed in alcuni casi perfino dell’applicazione in
sé (aggiornamenti parziali o corrotti).
40
4 Persistenza dei dati
Come introdotto nel capitolo 2.2 esistono vari sistemi di memorizzazione che permettono la
condivisione di dati tra le pagine web. Questi sistemi sono stati progettati in periodi differenti, ed hanno seguito lo sviluppo dell’ambiente web fornendo gli strumenti più adeguati
ai problemi del momento.
Possiamo dividere i sistemi di persistenza in due grandi categorie, i sistemi strutturati
ed i sistemi non strutturati. Per sistema non strutturato si intende un sistema in grado
di memorizzare solamente dati semplici, generalmente stringhe, memorizzate nella forma
chiave-valore per consentirne la selezione in un secondo momento. Per sistema strutturato
si intendono invece i sistemi in grado di memorizzare dati complessi, con campi dato in
grado di memorizzare per ogni oggetto i relativi attributi. Sistemi di memorizzazione di
questo tipo offrono spesso le funzionalità e le potenzialità di veri e propri database.
4.1 Sistemi di persistenza non strutturati
I sistemi di persistenza non strutturati sono stati la prima soluzione fornita al problema
dell’assenza di stato tra le pagine web. Si differenziano principalmente per la loro posizione
(lato client o lato server) e per livello di sicurezza.
4.1.1 Cookies
I cookies rappresentano la forma più basilare di persistenza. Sono costituiti da piccoli file
testuali memorizzati nelle cartelle del browser, contenente un elenco di coppie chiave-valore
che possono essere scritte e lette dal server tramite dei comandi inseriti nell’intestazione
delle pagine web. Quando viene generato un cookie gli viene assegnato un tempo di vita,
oltre il quale tale file non dev’essere più considerato attendibile, e viene quindi rimosso dal
browser. Il fatto che il contenuto del cookie sia memorizzato in un file testuale rappresenta
un problema non indifferente per la siscurezza del dato, poichè potrebbe essere alterato
senza difficoltà sia dall’utente che da altri programmi.
I cookies non sono quindi una forma di persistenza dati realmente utilizzabile per memorizzare dati importanti quali password, account e simili. Possono essere utilizzati per
memorizzare impostazioni di poco conto, come le preferenze dell’utente per quanto riguarda lingua, contrasto e dimensione di carattere della pagina. In caso questi dati vengano
cancellati o corrotti non rappresenterebbero una gran perdita per l’utente, che potrebbe
reimpostarli in pochi click.
41
4.1.2 Sessioni
Le sessioni sono in grado di memorizzare coppie di valori in una posizione differente: il
server web. Questi dati sono quindi accessibili alla componente software remota, e possono
essere utilizzati agevolmente per la composizione delle pagine web secondo specifiche impostazioni relative all’utente. Nel momento in cui il linguaggio lato server lo richiede, può
essere creata una sessione per l’utente in grado di memorizzarne i dati. Tale sessione è unica
per ogni utente, e rappresenta un sistema di sicurezza molto efficace, perché rende possibile
l’implementazione di politiche di login, carrelli, liste dei desideri e simili. Se non esistessero
le sessioni il server potrebbe essere ingannato facilmente, rendendo insicura ogni operazione
via web che coinvolga meccanismi di riconoscimento dell’utente.
Il sistema delle sessioni rappresenta attualmente l’unico sistema di memorizzazione lato
server che non richieda l’utilizzo di un database. Questo sistema non è esente da vulnerabilità, poiché anche esso si basa in parte sui cookies, ma prendendo le giuste contromisure
è possibile assicurarsi un certo margine di sicurezza.
Ovviamente il meccanismo delle sessioni è adatto ad una memorizzazione temporanea (ristretta appunto alla sessione di navigazione), non certo ad una persistenza duratura o che
coinvolga una quantità elevata di informazioni. Per tutte queste situazioni vengono utilizzati i database remoti residenti nel server in cui vengono ospitate le applicazioni o ad esse
collegati.
4.1.3 LocalStorage
LocalStorage è stato introdotto nell’ambito dello standard HTML5 e rappresenta un evoluzione del concetto di dato locale non strutturato. Questo sistema è accessibile solamente
via JavaScript, e permette di memorizzare dati nel browser con una semplicità disarmante.
In una chiamata è possibile leggere o scrivere nell’archivio, rendendo quindi molto agevole l’utilizzo di questo sistema all’interno di applicazioni web. La memorizzazione avviene
all’interno di un database contenuto nel browser, in una sezione accessibile solamente da
pagine del determinato dominio. Questa accortezza limita il rischio di intrusione da parte
di script malevoli provenienti da altri domini, ma non assicura l’integrità dei dati, in quanto
un utente con capacità avanzate è in grado di arrivare a modificare i dati archiviati in un
paio di passaggi.
LocalStorage risulta quindi perfetto per raccogliere i dati provenienti da moduli di inserimento distribuiti su più pagine, che verranno poi validati e inviati al server in un unico
momento. LocalStorage può essere utilizzato anche per memorizzare temporaneamente password e altri dati sensibili, sapendo che un eventuale corruzione dei dati può essere imputata
ad una manipolazione eseguita dall’utente. Questa assunzione può essere considerata valida
solamente in caso l’applicazione sia progettata in modo da non consentire l’inclusione di codice JavaScript estraneo al suo interno. Se fosse possibile inserire codice JavaScript malevolo
nelle pagine dell’applicazione, tale codice dannoso verrebbe eseguito all’interno del dominio
dell’applicazione, ed avrebbe quindi libero accesso ai dati contenuti nel LocalStorage.
42
4.2 Sistemi di persistenza strutturati
I sistemi di persistenza strutturati locali sono stati introdotti nell’ambito dello standard
HTML5, e permettono allo sviluppatore la memorizzazione di oggetti complessi, contenenti
vari attributi relazionati ad una chiave. Attualmente, come per i database remoti, esistono due grandi categorie: i database relazionali ed i database non relazionali o noSQL. A
differenza di un sistema di persistenza non strutturato, che viene utilizzato principalmente
come area più o meno temporanea per immagazzinare impostazioni, variabili e quant’altro,
in un sistema di persistenza strutturato vengono mantenuti tutti i dati necessari ad un
funzionamento più avanzato dell’applicazione.
Riprendendo l’esempio dei moduli distribuiti su più pagine fatto per LocalStorage, possiamo immaginare che il riepilogo di tutti gli ordini eseguiti e convalidati dal server venga
mantenuto in un database locale contenente una serie di record rappresentanti gli ordini,
con campi quali l’importo, i prodotti scelti, la data di consegna e via dicendo. Mentre un
sistema di memorizzazione non strutturato non fornisce la possibilità di eseguire ricerche,
perché comprende solamente il rapporto tra chiave e valore, un sistema di memorizzazione
strutturato consente di impostare specifici parametri di ricerca, rendendo quindi possibile
l’estrazione di un determinato sottoinsieme di dati, accomunati da alcune caratteristiche.
4.2.1 Database remoti
I database remoti sono presenti dagli albori del web. In un certo senso l’intero ambiente
web nasce per fornire un accesso pubblico a dati contenuti in sistemi di memorizzazione.
I database remoti si dividono in due tipologie: relazionali e non relazionali. I database
relazionali si basano sul concetto di entità, e rappresentano i legami tra i dati inseriti come
relazioni tra le entità definite. In un database relazionale viene definito inizialmente uno
schema, o struttura, che determina per ogni entità le sue caratteristiche, il numero ed il
tipo degli attributi, i vincoli che deve soddisfare per essere ritenuta integra e le relazioni
con altre entità. Tutti i dati che vengono inseriti all’interno di un database di questo tipo
devono rispecchiare la struttura definita per le varie entità, garantendo l’integrità generale
del database.
In un database non relazionale invece viene a perdere d’importanza la rigidità dello schema,
che è si presente, ma in una versione molto più flessibile. I dati inseriti devono rispecchiare
un sottoinsieme minimo di attributi, necessari al funzionamento basilare del sistema di archiviazione. Tali attributi sono principalmente indici necessari per rendere più efficienti le
ricerche, e mantenere quindi la competitività con i tempi di accesso dei database relazionali. Un dato inserito in un database di questo tipo non viene considerato propriamente un
record, in quanto non segue rigidamente lo schema dell’entità. Il concetto stesso di entità
viene a mancare, in quanto introducendo la possibilità di variare il numero e tipo di attributi non è più possibile asserire fermamente che un dato oggetto sia un istanza di una certa
entità. Viene quindi introdotto il concetto di collezione, inteso come un insieme di elementi
con alcune caratteristiche in comune.
Per comprendere meglio la differenza tra questi due concetti si veda un entità come uno
43
schema preciso, che definisce dei parametri per ogni oggetto che possono assumere differenti
valori. Se un certo oggetto non possiede certi parametri o ne possiede altri non elencati
nello schema, esso non è un istanza della tale entità. In una collezione invece viene definito
un sottoinsieme minimo di caratteristiche comuni, necessarie al funzionamento ottimale del
database. Se un oggetto soddisfa le proprietà di base esso può far parte della collezione, ma
in realtà le caratteristiche salienti del tale oggetto potrebbero non essere contemplate nel
sottoinsieme di parametri definito dalla collezione.
I database non relazionali hanno conosciuto un periodo di grande interesse con la diffusione di social network e motori di ricerca. Devono il loro successo alla rapidità in cui
forniscono l’informazione richiesta, e alla possibilità di ottimizzare la dimensione dell’archivio (grazie allo schema flessibile) e alla possibilità di distribuire i propri dati su più server
gestendo agevolmente l’integrità dei dati.
Nell’ambiente web entrambe le categorie sono ben rappresentate da soluzioni molto semplici
da implementare. Generalmente i linguaggi di programmazione lato server utilizzano delle
funzioni predisposte dai vari DBMS (Database Management System) per interfacciarsi con
i database e impartire i comandi necessari all’utilizzo dei dati in essi contenuti.
4.2.2 WebSQL
WebSQL è un progetto nato all’interno dello standard HTML5 con l’obbiettivo di fornire un
database relazionale all’interno del browser. L’implementazione di questo sistema è molto
semplice, dato che la maggior parte dei browser utilizzano al loro interno un database relazionale per memorizzare i dati necessari per le proprie funzionalità, quali la memorizzazione
dei preferiti, delle password e altri dati utente e dei valori di autocompletamento dei moduli. Fornire un database relazionale si può quindi ridurre ad implementare delle funzioni
che regolino l’accesso ad un area del database sottostante appositamente predisposta per il
dominio dell’applicazione.
L’implementazione attuale si basa sul noto database SQLite, presente in numerose applicazioni e sistemi integrati. Tale database è accessibile via JavaScript tramite opportune
funzioni asincrone, che consentono di far eseguire codice SQL all’interno della propria porzione di database. SQLite è un database che offre buone funzionalità, essendo comunque
molto semplice. Per ottenere rapidità di calcolo e semplicità risulta però inefficace nel gestire l’integrità referenziale dei dati, non permettendo l’impostazione di vincoli stringenti
tra due o più entità. Risulta quindi un database dalle ottime capacità, se si tengono in
considerazione i suoi limiti.
44
4.2.3 IndexedDB
IndexedDB è la proposta non relazionale fornita in sostituzione a WebSQL. In questo caso
non è stato sufficiente includere un database non relazionale all’interno del browser ed esporne le funzionalità, si è invece partiti dal basso, costruendo il DBMS in JavaScript, ottenendo
quindi un sistema ottimizzato per il funzionamento all’interno del browser. Attualmente
IndexedDB è l’unico database locale ufficialmente supportato dal W3C, in seguito a un
lungo dibattito durato mesi che ha visto WebSQL perdere importanza fino al suo completo
abbandono ufficiale.
Essendo progettato appositamente per l’interazione con JavaScript, questo sistema di memorizzazione offre delle funzionalità di manipolazione molto semplici, e guadagna in velocità
di esecuzione demandando allo sviluppatore il compito di mettere in relazione tra di loro
gli elementi contenuti all’interno delle collection.
4.3 Studio dei database locali
4.3.1 Contesto d’utilizzo di un database locale
Un applicazione web potrebbe trarre un grande vantaggio dall’utilizzo dei database locali.
Mantenere a portata di funzione tutta una serie di dati relativi all’applicazione web potrebbe rendere più veloce il loro utilizzo, ed ottimizzare le richieste verso il server.
Considerando poi le applicazioni offline, l’utilizzo di database locali diventa quasi un prerequisito, senza il quale non sarebbe possibile creare applicazioni non banali.
Un database locale può fornire all’applicazione lo spazio per memorizzare i dati ad essa
relativi, ma richiede una distinzione aggiuntiva tra i dati che possono essere memorizzati
localmente e quelli che invece non possono essere memorizzati in posizioni differenti dal
server, spesso per problemi di sicurezza.
Prima di utilizzare un database locale per la memorizzazione di informazioni occorre comprendere appieno la variazione al concetto di affidabilità del dato memorizzato. Utenti con
capacità avanzate possono modificare i dati memorizzati senza troppe difficoltà, rendendo
quindi insicuro il loro utilizzo in alcuni contesti. Per certe tipologie di dati non è possibile
accettare un livello di affidabilità cosı̀ basso.
Un esempio per chiarire questo concetto può essere dato da un ipotetico applicativo aziendale, in cui in base al ruolo di un determinato utente esso ha accesso alla modifica dei dati
aziendali. Un utente malintenzionato potrebbe modificare senza problemi il proprio ruolo,
garantendosi l’accesso autorizzato ai dati aziendali, locali e remoti.
Questo esempio rende evidente come, approcciandosi ai database locali, può essere necessario mettere in discussione le buone intenzioni dell’utente dell’applicazione.
45
4.3.2 Differenze tra IndexedDB e WebSQL
I due database locali presentati offrono funzionalità molto differenti, che derivano da limiti
e caratteristiche ben specifici. Per comprendere quale database utilizzare per il proprio
progetto occorre comprendere la natura dei due DBMS, cercando di individuare quali siano
le caratteristiche che più si addicono al progetto da sviluppare.
Durante il dibattito tra i due sistemi di memorizzazione sono stati prodotti molti esempi di utilizzo comparato, reperibili facilmente in rete [5]. Tali esempi analizzano sia la
creazione dei database, delle collezioni o entità, sia il loro utilizzo per quanto riguarda la
selezione e la modifica dei dati.
Di seguito viene presentato un esempio di utilizzo di un database WebSQL e IndexedDB.
Lo scopo del confronto è mettere in mostra i passaggi necessari allo sviluppatore per interrogare il database ed ottenere i dati richiesti in un formato utile per essere utilizzati
nell’applicazione. La query in esempio si pone l’obbiettivo di ottenere l’elenco dei bambini
che hanno mangiato caramelle e la quantità.
WebSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var db = window . openDatabase ( " CandyDB " , " 1 " ,
" My candy store database " ,
1024) ;
db . readTransaction ( function ( tx ) {
tx . executeSql ( " SELECT name , COUNT ( candySales . kidId ) " +
" FROM kids " +
" LEFT JOIN candySales " +
" ON kids . id = candySales . kidId " +
" GROUP BY kids . id ; " ,
function ( tx , results ) {
var display = document . getElementById ( " purchaseList " ) ;
var rows = results . rows ;
for ( var index = 0; index < rows . length ; index ++) {
var item = rows . item ( index ) ;
display . textContent += " , " + item . name + " bought "
+
item . count + " pieces " ;
}
}) ;
}) ;
Listing 4.1: esempio di utilizzo WebSQL
46
IndexedDB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
IndexedDB
candyEaters = [];
function displayCandyEaters ( event ) {
var display = document . getElementById ( " purchaseList " ) ;
for ( var i in candyEaters ) {
display . textContent += " , " + candyEaters [ i ]. name + "
bought " +
candyEaters [ i ]. count + " pieces " ;
}
};
var request = window . indexedDB . open ( " CandyDB " ,
" My candy store database " ) ;
request . onsuccess = function ( event ) {
var db = event . result ;
var transaction = db . transaction ([ " kids " , " candySales " ]) ;
transaction . oncomplete = displayCandyEaters ;
var kidCursor ;
var saleCursor ;
var salesLoaded = false ;
var count ;
var kidsStore = transaction . objectStore ( " kids " ) ;
kidsStore . openCursor () . onsuccess = function ( event ) {
kidCursor = event . result ;
count = 0;
attemptWalk () ;
}
var salesStore = transaction . objectStore ( " candySales " ) ;
var kidIndex = salesStore . index ( " kidId " ) ;
kidIndex . openObjectCursor () . onsuccess = function ( event ) {
saleCursor = event . result ;
salesLoaded = true ;
attemptWalk () ;
}
function attemptWalk () {
if (! kidCursor || ! salesLoaded )
return ;
if ( saleCursor && kidCursor . value . id == saleCursor . kidId
) {
count ++;
saleCursor . continue () ;
}
else {
candyEaters . push ({ name : kidCursor . value . name , count :
count }) ;
kidCursor . continue () ;
}
}
}
Listing 4.2: esempio di utilizzo IndexedDB
47
Esaminando i due listati proposti si può facilmente notare una certa complessità nel codice di IndexedDB. Mentre WebSQL permette di definire i parametri di ricerca e demanda
al database la selezione dei dati realmente utili, con IndexedDB non è possibile effettuare
questo tipo di operazione, e lo sviluppatore deve prendersi la responsabilità di effettuare di
persona la selezione dei dati da utilizzare, esprimendo il tutto tramite chiamate JavaScript.
Da questa considerazione possiamo comprendere per quali sviluppatori sono stati progettati i due DBMS: mentre uno sviluppatore lato server si potrebbe trovare a proprio agio
con il linguaggio SQL, avendo invece difficoltà ad esprimere i singoli vincoli in JavaScript,
uno sviluppatore lato client potrebbe trovarsi molto più agevolato nell’utilizzo del codice
JavaScript piuttosto che nell’utilizzo del linguaggio SQL.
Una differenza sostanziale che attualmente non viene considerata, ma che potrebbe diventare molto importante in futuro, riguarda la granularità di blocco dei due database.
Sono già in via di sviluppo i WebWorker, entità molto simili ai thread della programmazione
concorrente. Questi WebWorker offrirebbero la possibilità di avere più processi JavaScript
in esecuzione per ogni pagina, portando dunque tutti i vantaggi della programmazione
concorrente all’interno del browser. Lavorare in un contesto multithread all’interno delle
pagine rende necessario limitare gli accessi concorrenti al database. Nel caso in cui diversi
thread della pagina accedano concorrentemente all’archivio dati potrebbero essere utilizzati
dei meccanismi di sincronizzazione, dal momento che i thread condividerebbero un area
di memoria comune fornita dalla pagina iniziale. Nel caso in cui siano diverse pagine ad
accedere concorrentemente al database, senza condividere memoria tra di loro, un blocco
del database potrebbe rivelarsi necessario per evitare gravi problematiche di inconsistenza.
WebSQL non fornisce una politica di blocco sufficientemente granulare, mentre uno degli
obbiettivi di IndexedDB è quello di fornire meccanismi di blocco con una granularità più
avanzata, in grado di bloccare solamente il record attualmente in uso, e garantire quindi
l’utilizzo corretto dei dati.
4.3.3 IndexedDB nel dettaglio
Dal momento che i database non relazionali sono meno conosciuti a livello tecnico rispetto
ai normali database vale la pena addentrarsi maggiormente nella descrizione, aggiungendo
alcuni dettagli utili a comprendere meglio gli esempi.
Ogni database non relazionale è identificato da un nome, e contiene al suo interno uno
o più Object Stores. Ogni Object Stores rappresenta una collezione di oggetti, e definisce
almeno una proprietà chiave che ogni oggetto memorizzato deve contenere, implicitamente
o esplicitamente.
Ad esempio nel caso di un agenda telefonica ogni contatto dovrebbe avere un proprio identificativo, questo identificativo è la proprietà che accomuna e indicizza tutti gli oggetti
memorizzati.
Gli oggetti possono essere strutturati, ma non sono vincolati ad uno schema definito rigidamente. Ogni oggetto può presentare delle proprietà aggiuntive, e se queste sono frequenti
tra gli oggetti della collezione è possibile includerli nella definizione della collezione. L’O-
48
bject Stores può utilizzare queste proprietà per definire degli indici in grado di consentire
una ricerca ed un filtraggio più rapidi.
Ritornando all’esempio dell’agenda è possibile immaginare che alcuni contatti abbiano informazioni aggiuntive che altri contatti non hanno, ad esempio indirizzo, email, eccetera.
Utilizzando questi indici aggiuntivi è possibile selezionare dei sottoinsiemi della collezione,
oppure esprimere all’interno delle ricerche valori particolari per questi indici, ottenendo dei
risultati soddisfacenti in termini di tempi di calcolo.
Ogni database IndexedDB è abbinato ad un controllo di versione. Il versionamento del
database è lasciato allo sviluppatore, che si deve occupare via JavaScript di aggiornare il
numero di versione quando vengono applicate modifiche. L’utilizzo di tale indice di versione
permette il confronto di copie dello stesso database al fine di capire rapidamente quale sia
la più recente.
Ogni operazione, dall’apertura, alla creazione, all’interrogazione e alla modifica di dati è
realizzata in maniera asincrona. In caso si verifichi un errore è possibile rilevare gli eventi
di errore ed invocare un ripristino dell’ultimo stato consistente del database.
La disponibilità di funzioni di controllo asincrone, combinata con l’espressività di JavaScript, consente allo sviluppatore di gestire completamente ogni evento relativo al database,
che con un normale DBMS relazionale rimarrebbe invece racchiuso all’interno del sistema
di gestione interno. Questa possibilità apre la strada ad un utilizzo più avanzato dei dati
memorizzati, completamente integrato con il linguaggio di manipolazione dei dati stessi
(JavaScript in questo caso).
4.3.4 La scelta di supportare solamente IndexedDB: motivazioni e critiche
Attualmente alcune importanti realtà all’interno del W3C (principalmente Mozilla e Windows) hanno espresso pesanti critiche a WebSQL (proposto e realizzato inizialmente da
Apple, adottato da Google per il suo browser Chrome ed il browser di Android), che hanno
portato all’adozione di IndexedDB (proposto da Mozilla).
Alcune delle principali obbiezioni all’utilizzo di WebSql:
• Il linguaggio SQL non è realmente uno standard, vi sono differenze tra gli standard ufficiali, ed ogni implementazione attuale offre delle estensioni non standard per sopperire
alle mancanze del linguaggio;
• SQL è un linguaggio ad alto livello. Se un programmatore volesse semplicemente
inserire e rimuovere dati in una tabella, non sarebbe necessario forzarlo ad utilizzare
un linguaggio di programmazione complesso;
• WebSQL si basa su SQLite, ma SQLite non implementa uno standard specifico, e
sarebbe difficile creare una versione di SQL standard per il web, dato che non si è
ancora riusciti a crearne una standard per gli altri ambienti della programmazione;
49
• SQL è un sistema completamente differente, non collegato ai linguaggi in cui viene
normalmente integrato;
• È facile per programmatori inesperti inserire difetti dal punto di vista della sicurezza
nelle espressioni SQL;
• È facile per programmatori inesperti sviluppare query oltremodo complesse, tali da
rendere impossibile al sistema la risoluzione delle stesse in un lasso di tempo ragionevole;
• Sebbene sia possibile scrivere SQL in grado di funzionare con DBMS quali Oracle,
PostgreSQL e MySQL, è veramente difficile scrivere del codice per un utilizzo avanzato
senza conoscere le caratteristiche ed i difetti dei tre sistemi.
In queste critiche si può comprendere come la figura principale dello sviluppatore di applicazioni web venga intesa come evoluzione del normale sviluppatore web, e non come un
evoluzione dello sviluppatore di applicazioni standard. Si assume in vari ambienti di trovarsi
di fronte a sviluppatori le cui nozioni di programmazione siano piuttosto basilari, mentre
le potenzialità delle tecnologie in via di sviluppo potrebbero portare verso l’ambiente web
anche sviluppatori esperti, con un bagaglio di conoscenze non indifferente.
È importante notare come WebSQL sia supportato sia dai dispositivi Android sia da quelli
Apple, garantendosi quindi una posizione predominante nel settore mobile, ultimamente in
rapida espansione.
4.3.5 Scegliere il proprio database offline
Da un analisi iniziale ci si potrebbe chiedere perché non mantenere entrambe le possibilità,
fornendo quindi agli sviluppatori le due alternative, ottenendo quindi il massimo della copertura dei casi d’applicazione. L’importanza di questa scelta secondo Mozilla e Microsoft
è legata allo sviluppo futuro delle applicazioni. Pian piano si sta assistendo alla conversione
quasi completa di una serie di applicazioni desktop in applicazioni web. Avvertendo la pressione di una passaggio cosı̀ radicale, i maggiori componenti del W3C vorrebbero innanzitutto
avere le idee chiare sulle basi da utilizzare, per evitare di dover modificare i propri progetti
in corso d’opera. Pretendere certezze dal mondo del web, nato e cresciuto evolvendosi per
conto proprio, un ambiente in cui gli standard spesso hanno inseguito le implementazioni, è
forse chiedere troppo. Questa pretesa però può far riflettere su come stia cambiando anche
la prospettiva di utilizzo del web in un ottica più produttiva. Il potenziamento dei motori
JavaScript, la diffusione di una miriade di librerie di terze parti altamente innovative, ed
il successo di applicazioni web largamente diffuse stanno gradualmente modificando l’idea
del web visto come strumento di diffusione di informazioni per elevarlo al ruolo di vero e
proprio ambiente di lavoro, ricco di alternative ed in costante movimento.
4.3.6 Variazioni al concetto di sicurezza del dato nel database locale
Come già esposto precedentemente, quando il DBMS è residente in un server si trova in
un ambiente relativamente sicuro, in cui gli accessi sono controllati, ed è possibile vinco-
50
lare logicamente le operazioni da eseguire sui dati in base al ruolo dell’utente, sia a livello
di database, sia a livello di linguaggio di manipolazione. Una sicurezza del genere viene
completamente scardinata nel momento in cui l’utente ha accesso fisicamente ai dati memorizzati nel database.
Al di là del rischio di violazione dei dati locali, vi è il problema dell’esposizione completa
dello schema e dei dati del database all’utente. In caso il database locale fosse una porzione
esatta del database remoto e non una vista adattata, verrebbero esposti i dettagli implementativi dello schema all’utente, il quale se malintenzionato potrebbe sfruttarli per forzare
falle o alterare i dati presenti mantenendo comunque una parvenza di integrità.
Un terzo punto di riflessione riguarda infine la possibilità di accesso a dati inadeguati per
l’utente. Anche in caso l’utente non abbia intenzione di alterare la struttura di memorizzazione o i dati in sé, è possibile che abbia accesso a dati necessari per il funzionamento
dell’applicazione, ma non adeguati all’utente. Esporre completamente i propri dati potrebbe
rappresentare un serio problema in alcuni contesti applicativi.
4.4 Confronto con i database remoti
Anche lato server stanno avvenendo alcuni cambiamenti importanti. Per anni l’utilizzo di
database non relazionali è stato una scelta di nicchia, applicata solamente in casi specifici in
cui fosse necessario gestire una gran mole di dati. Ultimamente sono state messe in discussione alcune caratteristiche dei database relazionali che ne vincolano alcune funzionalità,
diventate improvvisamente molto importanti. I database non relazionali hanno suscitato
un grande interesse e sono stati sviluppati una serie di DBMS non relazionali molto interessanti. Attualmente buona parte dei servizi web più importanti può essere ricollegata ad
un database non relazionale: Google offre ai suoi utenti DataStore, Linkedin ha sviluppato
il progetto Voldemort, Facebook si appoggia su Cassandra, un progetto nato internamente a Facebook e incubato poi dall’Apache Fundation, la quale ha all’attivo altri progetti
riguardanti DBMS della stessa tipologia, come ad esempio CouchDB.
4.4.1 Relazionali
Il modello relazionale è stato da sempre la variante più diffusa tra i DBMS. Si tratta di
un modello logico di rappresentazione dei dati basato sull’algebra relazionale e sulla teoria
degli insiemi ed è strutturato attorno al concetto di relazione (detta anche tabella). Una
relazione è un insieme di attributi legati da vincoli di integrità. Questo concetto teorico
di relazione viene a concretizzarsi nella definizione di una tabella in cui ogni campo ha dei
vincoli precisi da rispettare.
Oltre ai vincoli sui singoli attributi, vi sono poi vincoli che coinvolgono gruppi di attributi,
anche appartenenti a entità differenti. Quando il database è consistente significa che tutti
i suoi vincoli sono soddisfatti, e quindi esso si trova in uno stato di integrità referenziale.
51
4.4.2 Non relazionali
I database non relazionali vengono utilizzati in applicazioni in cui i vincoli strutturali dei
database relazionali non portano a vantaggi sensibili. Alcuni nomi già citati sono Google
e Facebook, ma sono veramente tanti i progetti web importanti a poggiare su questo tipo
di database per fornire i propri servizi. Contrariamente ai principali DBMS relazionali,
abbastanza simili come funzionamento, differenziati principalmente da performances, costo
e tipo di licenza, ogni DBMS non relazionale ha particolarità che ne esaltano alcuni aspetti,
rendendolo adatto a particolari casi d’applicazione. Ci sono DBMS che fanno della scalabilità o della tolleranza ai guasti il punto chiave, altri che sono basati su linguaggi altamente
concorrenti come Erlang e si rendono una scelta importante in ambienti multicore. Una
terza categoria è composta dai database progettati per un alta resa in ambienti distribuiti.
Tutte queste caratteristiche rendono la scelta del DBMS da utilizzare un passaggio impegnativo, da affrontare con un Architect Designer competente.
Per un confronto più tecnico tra i principali DBMS non relazionali è utile analizzare alcuni
degli studi già eseguiti da esperti del settore [6][7].
4.4.3 Teorema di CAP
Nel momento in cui ci si addentra nel mondo dei database non relazionali può capitare di
perdere di vista alcuni importanti principi, e dare per buone quasi tutte le promesse che
questi sistemi propongono in maniera cosı̀ attrattiva. Per poter valutare correttamente ogni
sistema può essere utile avere come riferimento nell’analisi della caratteristiche il teorema
di CAP.
l teorema di CAP (Consistency, Availability, Partitions tollerance) è stato espresso nel
1998 da Eric A. Brewer. Esso sostiene che un sistema di dati condivisi è generalmente caratterizzato dalle tre proprietà che creano l’acronimo: consistenza, disponibilità e tolleranza
al partizionamento di rete. L’affermazione principale di questo teorema è che si possono
soddisfare contemporaneamente solamente due proprietà su tre.
Realizzando due caratteristiche per volta otteniamo i seguenti sistemi:
• consistenza e disponibilità: si tratta di sistemi localizzati che non risentono di
partizionamento di rete, come un LDAP, un database centralizzato o un file system.
La consistenza è garantita da transazioni two phase commit.
• consistenza e tolleranza al partizionamento: sistemi distribuiti, con un elevato partizionamento di rete, in cui il valore della consistenza dei dati è di primaria
importanza, a scapito della eventuale elevata latenza e bassa disponibilità (dovuta a
politiche di locking pessimistiche).
• disponibilità e tolleranza al partizionamento: la disponibilità è il valore principale per questo genere di sistemi. Per questo utilizzano protocolli più ottimistici e un
approccio best-effort. Un esempio classico sono i DNS.
52
I grandi siti web dei nostri giorni sono caratterizzati da architetture altamente scalabili e
profondamente distribuite, con politiche di prossimità geografica. Essi puntano chiaramente
sulla garanzia di alte prestazioni ed estrema disponibilità dei dati. Si trovano quindi nella
terza delle situazioni citate.
Lo sforzo di garantire dati consistenti porta questi sistemi ad adottare approcci differenti
per l’accesso e l’aggiornamento dei dati.
Si passa dall’approccio transazionale ACID (Atomicity, Consistency, Isolation, Durability)
che garantisce la consistenza, ad un approccio BASE (Basic Available, Soft-state, Eventual
consistency) che mette in primo piano la disponibilità.
53
5 Sincronizzazione dei dati
Un applicazione offline non isolata deve necessariamente fornire un sistema di sincronizzazione, senza il quale non sarebbe possibile mantenere coerenti tra loro la versione locale e
quella remota dell’applicazione, dando all’utente la sensazione di lavorare sempre all’interno
della stessa applicazione.
La sincronizzazione è un argomento che non viene ampiamente trattato in maniera generale
o teorica, questo perché ogni strategia di sincronizzazione efficace rappresenta un sistema
unico, calibrato minuziosamente sul tipo di dati da memorizzare e sul tipo di interazioni che
li definiscono o li manipolano. È importante capire che non esiste una strategia generica
in grado di mantenere una perfetta aderenza alle caratteristiche del particolare problema.
Esistono una serie di principi di consistenza, un insieme di tecniche e strumenti che possono
essere utilizzati, ma tutto ciò va modellato in funzione del particolare caso di applicazione.
Le applicazioni web presentano alcune caratteristiche importanti che come detto rappresentano i parametri sulla cui base possono essere prese alcune considerazioni. Prima di
tutto è importante considerare che i database locali non hanno un canale di comunicazione
diretto né tra di loro, né con la controparte remota. Questa considerazione elimina dall’insieme delle possibilità buona parte delle strategie di replicazione automatiche fornite dai
DBMS. Un secondo fattore da tenere in considerazione è la facilità con cui l’utente ha accesso al database. Un terzo dettaglio da non sottovalutare parte da una considerazione ben
più ampia: lo scenario d’uso di un applicazione offline. Un applicazione offline potrebbe
essere eseguita agevolmente su di un dispositivo mobile, che a differenza di un computer
fisso o di un portatile può essere perso o sottratto con estrema facilità. Da quest’ultima
considerazione deriva la necessità di non memorizzare dati importanti se si ha motivo di
credere che possano essere sottoposti ad un attacco fisico.
I meccanismi che si occupano della sincronizzazione possono essere divisi in due categorie:
modelli di sincronizzazione fisica o orientati ai dati, e modelli di sincronizzazione applicativa o orientati ai processi. La differenza tra queste due tipologie di meccanismi risiede nel
diverso punto di inserimento all’interno dell’applicazione.
Mentre un sistema di sincronizzazione orientato ai dati si occupa di eseguire periodicamente un confronto globale tra lo stato del database remoto e di quello locale, un sistema di
sincronizzazione orientato ai processi segue passo passo il funzionamento dell’applicazione,
e mantiene in un area riservata le istruzioni aggiuntive progettate per agevolare la sincronizzazione.
54
Indubbiamente un sistema orientato ai dati risulta essere il più semplice da implementare
in un ambiente di programmazione standard, e in applicazioni complesse potrebbe rivelarsi
anche molto più efficiente, dal momento che sgrava l’applicazione dal compito di mantener
traccia di tutte le operazioni compiute.
Vi sono tuttavia due grandi problemi in questo approccio: per prima cosa non vi è un canale dedicato che colleghi i due database, e ciò rappresenta un grave problema, in quanto a
volte rende impossibile arrivare ad un analisi approfondita dei dati. In secondo luogo i dati
mantenuti in memoria localmente sono solamente un sottoinsieme dei dati archiviati nella
controparte remota, che potrebbe anche avere la necessità di ricombinarli insieme prima di
sincronizzarli con il database locale.
5.1 Sincronizzazione fisica di database
Nonostante la sincronizzazione fisica di due database non si sia dimostrata la scelta più
adatta alla soluzione del problema della sincronizzazione tra database locali e remoti, vale
la pena di studiarne le caratteristiche in relazione al problema in esame.
In fase di definizione del progetto di stage era stata valutata come ipotesi la sincronizzazione a basso livello del database client e server. Studiando le caratteristiche dei due
database sono stati messi in luce alcuni aspetti problematici che rendono attualmente improponibile una sincronizzazione effettiva mediante tecniche basilari, utilizzate diffusamente
in altri ambiti quali la sincronizzazione di file tra più computer.
5.1.1 Ipotesi iniziale: interpretazione in ottica fisica (file)
Inizialmente si era considerato il problema partendo da una situazione esistente già affrontata con successo in altri ambiti: la replicazione di file tra più computer. In questo tipo di
sincronizzazione paritetica vi sono alcune ipotesi che difficilmente possono essere adattate al
concetto client-server, il quale è espressamente gerarchico. Nella replicazione i due sistemi
vengono sincronizzati costantemente in modo da rappresentare (in un momento in cui il
sistema è in uno stato coerente) due versioni identiche dello stesso database.
Nel progetto in esame invece la necessità è di un altro tipo: mantenere coerenti tra loro due
versioni differenti di database che condividono secondo alcune specifiche politiche una serie
di dati.
5.1.2 Problematiche riscontrate lato client
Le problematiche riscontrate in un approccio di tipo replicativo sono prima di tutto tecnologiche e in secondo piano applicative. L’aspetto applicativo, sebbene sia di indubbia
importanza, passa in secondo piano valutando le limitazioni tecnologiche imposte, principalmente relative all’impossibilità di accedere fisicamente ai dati memorizzati nel browser.
Per meglio comprendere questo aspetto si può ragionare sulla differenza tra un ambiente di replicazione server side e un ambiente di sincronizzazione client-server: nel primo caso
55
abbiamo a che fare con due database noti (generalmente dello stesso tipo), con meccanismi
di replicazione preesistenti, messi in comunicazione attraverso un canale stabile e controllato. Nel secondo caso abbiamo invece un sistema server su cui possono essere fatte delle
assunzioni (probabilmente limitate), ed un sistema client residente in un browser, il quale espone solo parzialmente le caratteristiche del database. Il canale di comunicazione è
composto dalle varie componenti dell’ambiente web: il browser, il linguaggio JavaScript,
il protocollo HTML ed infine il linguaggio lato server. Nessuna di queste componenti può
offrire un canale di comunicazione diretto tra i due database.
Mentre nella replicazione lato server si hanno alcune libertà (ad esempio è possibile esaminare i log del database dall’interno dell’applicazione), le quali possono consentire la creazione
di punti d’accesso per i meccanismi di sincronizzazione, nell’ambiente client-server non si
ha accesso ai reali componenti del sistema, ma solamente alle interfacce esposte dagli strati
che lo compongono. In un contesto di questo tipo una limitazione tecnologia impone un
limite spesso invalicabile.
5.1.3 Problematiche riscontrate lato server
I principali DBMS prevedono politiche di replicazione dedicate, utilizzabili quindi solamente tra database compatibili. Questi sistemi di replicazione possono lavorare a basso livello,
eventualmente sincronizzando a basso livello i dati che contengono. Esistono da tempo algoritmi efficienti già impiegati per la sincronizzazione dei file che possono essere adattati a
questo scopo.
Nel momento in cui si cerca di sincronizzare due sistemi aventi specifiche differenti, bisogna
retrocedere di un passo a causa delle differenti interfacce e protocolli, e spostarsi ad un
livello più alto, tornando ad avere a che fare con la componente logica.
Nel caso migliore è possibile elaborare il registro delle transazioni effettuate, estraendo le
operazioni eseguite successivamente all’ultima data di sincronizzazione e replicandole nell’altro database. In questo modo si può ottenere una copia consistente senza dover esportare
e importare completamente i dati memorizzati. In caso non sia fornita la possibilità di analizzare il registro delle transazioni si può inserire uno strato logico posizionato all’accesso
del database, che registri le operazioni eseguite creando un log funzionale delle variazioni
necessarie per la sincronizzazione.
In caso non fosse possibile manipolare la logica dell’applicazione in questo modo si ricade
nell’ultimo caso, in cui è necessaria un esportazione ed importazione massiccia dei dati.
Tali operazioni rappresentano sempre un problema perché introducono pericolose anomalie
all’interno dei sistemi, e devono essere eseguite in specifici momenti in cui il blocco del database non provoca danni. Tali sistemi di sincronizzazione sono conosciuti come BulkExport
e BulkImport (letteralmente esportazione ed importazione massicci).
Nell’ipotesi in esame ci troviamo circa nel secondo caso: non è possibile accedere ad un
registro delle transazioni né client side né server side (anche se in questo ambiente potrebbe
essere possibile aggirare l’ostacolo avendo sufficiente libertà di movimento nel server), non
è consentito un accesso diretto al database, e gestire una copia massiccia dei dati da client
56
a server presenta una grave problematica dal punto di vista dell’efficienza, oltre a scontrarsi
con il problema del partizionamento e della rivalidazione server side, discussi nei paragrafi
5.1.4 e 5.1.5.
5.1.4 Partizionamento dei dati
Nel caso delle applicazioni web non è possibile mantenere localmente una copia esatta del
database remoto, poiché ogni client ha accesso ad un set ristretto di dati. A causa di problematiche legate alla sicurezza non è poi ipotizzabile esporre all’utente dati non pertinenti
con il suo ruolo specifico.
Considerata questa premessa i dati contenuti nel server possono essere organizzati in tre
macro categorie: dati privati dell’applicazione, dati pubblici dell’applicazione e dati utente.
La prima categoria contiene i dati necessari al funzionamento dell’applicazione, ma normalmente non accessibili dall’utente che la utilizza. In un’applicazione che gestisce ordini e
transazioni può essere un pericolo esporre chiavi software e altre informazioni che possano
permettere una forzatura del sistema. Il concetto di base è chiaro: alcuni meccanismi dell’applicazione sono e devono rimanere fuori dalla portata di qualsiasi utente, che potrebbe
altrimenti utilizzarli per scopi non previsti.
Nella seconda categoria troviamo i dati a cui l’utente può accedere, ad esempio l’elenco
dei clienti dell’azienda o l’elenco degli ordini in lavorazione. All’interno di questa categoria potremmo dividere ulteriormente le informazioni in base a politiche d’accesso mirate:
potremmo magari esporre al responsabile di una sezione solamente i dati relativi ai propri
dipendenti.
Nella terza categoria possiamo posizionare i dati di proprietà dell’utente, ad esempio ordini
creati personalmente (su cui si dovrebbe avere una maggiore possibilità di manipolazione).
In quest’ultima sezione andrebbero inserite anche le dinamiche di controllo e i permessi dell’utente, che vincolano le operazioni che possono essere eseguite sui propri dati e su quelli
della sezione pubblica condivisa.
L’accesso alle tre categorie viene normalmente regolato da specifici meccanismi di autenticazione e validazione delle operazioni. Nella versione locale dell’applicazione non possiamo
più fare affidamento su questa sicurezza, e le tre sezioni collassano quindi in una singola
sezione, quella appunto dei dati locali.
Si rende quindi necessario applicare una scelta precisa su quali dati memorizzare, tenendo
in considerazione ciò che è strettamente necessario al funzionamento dell’applicazione e ciò
che è strettamente pertinente all’utente. Questa selezione determina il partizionamento dei
dati remoti da sincronizzare con la controparte locale.
57
5.1.5 Rivalidazione delle azioni eseguite client side
Un ipotetica applicazione potrebbe seguire un principio di funzionamento fortemente orientato verso la componente offline: ogni modifica eseguita dall’utente viene applicata prima
alla versione locale del database e successivamente riportata dal lato server. In questo approccio possono essere individuate una serie di problematiche complesse, legate soprattutto
all’integrità logica dei dati, ed alla coerenza delle applicazioni rispetto ai permessi di manipolazione degli stessi da parte dell’utente.
A causa della scarsa sicurezza lato client e della possibilità dell’utente di manipolare i dati
ignorando completamente le logiche applicative, è necessaria una rivalidazione completa dei
dati lato server. Questa limitazione ha delle importanti ripercussioni dal punto di vista
della sincronizzazione, poiché porta a scartare completamente una sincronizzazione di tipo
fisico in favore di quella di tipo logico.
In caso si scegliesse di utilizzare un approccio fisico alla replicazione, ci si troverebbe ad
avere nel server le due versioni (in un ottica decisamente semplicistica). A questo punto
tentare di eseguire una validazione delle operazioni eseguite potrebbe essere un operazione
decisamente ardua, specie considerando che nel frattempo i permessi di manipolazione dei
dati potrebbero aver subito dei cambiamenti. Avendo le due versioni a confronto sarebbe
estremamente difficile capire quali operazioni hanno portato la versione iniziale ad evolvere
localmente fino a diventare la versione attualmente fornita dal client, senza contare che una
volta verificata la legalità delle operazioni si avrebbe la problematica di relazionarle con
istanze temporali differenti del database remoto.
Una sincronizzazione logica avanzata potrebbe fornire in anticipo solamente le modifiche
apportate, rendendo quindi più semplice per il server la verifica dell’effettiva legalità delle operazioni, anche confrontandole con delle politiche di manipolazione aggiornate nel
frattempo.
5.2 Sincronizzazione a livello applicativo
Valutando le problematiche di una sincronizzazione diretta tra il database server side e la
versione locale dello stesso, si può optare per una sincronizzazione a livello di logica applicativa. Tale soluzione prevede in sostanza la registrazione delle variazioni subite dai database
in formato SQL, variazioni che verranno poi applicate reciprocamente per ottenere una versione unificata. Mantenendo da parte le modifiche da applicare si possono proporre due
tipologie differenti di sincronizzazione: trasparente e tramite cache temporanea.
Nella sincronizzazione trasparente l’intera parte di trasferimento verso il server ed applicazione delle modifiche è completamente nascosta all’utente, che quindi può ignorare se lo
stato della propria applicazione è online o offline poiché gli vengono fornite esattamente
le stesse funzionalità. Un sistema di questo tipo ha delle grandi proprietà di tolleranza ai
guasti.
58
Nella sincronizzazione tramite cache temporanea l’utente ha una netta distinzione tra i
dati provenienti dal server e quelli generati localmente. Si può ipotizzare quindi che il database locale funzioni come un area temporanea, che conserva tutte le modifiche applicate
dall’utente in attesa di inoltrarle al server.
Un esempio potrebbe riguardare la creazione di un’applicazione di gestione di un magazzino: gli ordini creati in modalità offline vengono mantenuti in un area locale chiaramente
identificabile. Alla prima possibilità di connessione gli ordini potrebbero essere caricati richiedendo all’utente un ulteriore approvazione. Una dinamica del genere permetterebbe ad
esempio di presentare un riepilogo prima dell’effettivo inoltro dell’ordine, quando sono disponibili nuovi dati provenienti dal server, in modo da verificare ad esempio che la quantità
di merce richiesta sia effettivamente disponibile, o che le specifiche del prodotto non siano
variate nel frattempo.
5.2.1 Sincronizzazione trasparente
L’obbiettivo della sincronizzazione trasparente è quello di fornire un’applicazione offline speculare alla versione online, in grado quindi di esporre le stesse funzionalità e lavorare sugli
stessi dati. Questo approccio è particolarmente funzionale in un ambiente in cui ogni utente
lavora in un area disgiunta del database, mentre presenta delle complicazioni importanti in
caso il sistema sviluppato preveda un minimo di collaborazione o di condivisione di dati.
Ripensando ad esempio all’applicazione descritta precedentemente possiamo trarre alcune
considerazioni. Se l’utente non potesse distinguere gli ordini memorizzati localmente da
quelli inoltrati al server si creerebbe una confusione che potrebbe portare ad una inconsistenza logica, nel momento in cui magari si attende una mail di risposta dal magazzino per
un ordine che si ritiene già inoltrato. In un caso simile avere la sicurezza che il dato ordine
sia effettivamente stato inoltrato al server è un punto fondamentale.
Ragionando poi sulla rivalidazione dei dati lato server ci si potrebbe trovare nel caso in
cui il server rifiuti le modifiche, e quindi vengano cancellate anche nella versione locale dell’applicazione. L’utente che considera certe operazioni eseguite e che le vede all’improvviso
sparire dal flusso d’esecuzione del programma potrebbe sentirsi abbastanza disorientato.
Per evitare questo effetto bisognerebbe inserire una serie di notifiche che avvisino l’utente
che ciò che dava per fatto (e su cui magari nel frattempo ha applicato ulteriori modifiche) è
stato in realtà rifiutato dal server. Da questo punto di vista mantenere distinte le operazioni
eseguite solo localmente potrebbe rappresentare la scelta migliore in termini di chiarezza
nei confronti dell’utente. Ricordando che la chiarezza consente un utilizzo più soddisfacente
e produttivo è facile capire come l’adozione di componenti offline in grado di rappresentare
chiaramente il proprio funzionamento sia un punto chiave per il successo dell’applicazione.
5.2.2 Sincronizzazione incrementale mediante copie delta
In fase di progettazione si potrebbero mantenere due aree distinte del database: una dedicata all’ultima copia sincronizzata del server, ed una dedicata alle modifiche da applicare.
Non applicando direttamente le modifiche nella versione locale sarebbe possibile mantenerla
59
rigidamente sincronizzata con il server, realizzando quindi una sincronizzazione monodirezionale, decisamente più semplice da gestire. In sostanza localmente verrebbero memorizzati
due database, una copia di quello remoto in un determinato stato, senza modifiche aggiuntive, ed un database secondario contenente le differenze che si riscontrerebbero confrontando
il database locale con lo stato remoto di riferimento. Dividendo il database locale in questo modo sarebbe possibile aggiornare la copia locale del database remoto senza curarsi di
perdere le modifiche ad esso applicate durante il funzionamento offline. Si trasformerebbe
dunque la sincronizzazione bidirezionale (in un qualche modo paritetica) in una comunicazione monodirezionale da server a client, con un canale secondario dedicato alle differenze
da applicare.
Ad un aggiornamento della logica applicativa sarebbe inoltre possibile verificare la legalità
delle operazioni prima di tutto localmente, notificando all’utente l’impossibilità di completare determinate procedure a causa della variazione dei propri permessi.
Le modifiche locali da inoltrare al server verrebbero in questo modo validate prima localmente, eventualmente potrebbero essere sottoposte all’utente per una conferma, ed inoltrate al
server per l’applicazione definitiva. In caso la rivalidazione lato server risulti accettata sarebbe possibile ottenere una nuova copia del database remoto, del quale farebbero ormai parte
anche i propri dati. Mano a mano che le modifiche proposte al server vengono accettate,
esse possono essere rimosse dal database temporaneo, garantendo quindi un ottimizzazione
di spazio.
5.3 Strumenti di sincronizzazione
5.3.1 Utilizzo di marcature di sincronizzazione
In un ipotetico ambiente in cui l’applicazione client e quella server vengono utilizzate dallo
stesso singolo utente, potrebbero essere considerate delle politiche di sincronizzazione basate
sul log delle operazioni, o su un indice progressivo delle query eseguite. Utilizzando questo
codice progressivo sarebbe possibile confrontare in maniera rapida e semplice il livello di
sincronizzazione tra client e server. Purtroppo l’ipotesi di un’applicazione monoutente si
applica ad un numero di casi molto ristretto.
In un contesto più diffuso, in cui una serie di utenti lavorano sullo stesso database, si
presenta il problema del partizionamento dei dati: ogni utente ha la possibilità di accedere ad una singola porzione dei dati contenuti: i propri, o quelli ad essi strettamente collegati.
Le tecniche di sincronizzazione in questo ambiente includono la possibilità di creare un
indice di progressione personale per le query eseguite dal tale utente, che indichi quindi
l’avanzamento della propria partizione. Confrontando questo indice con quello archiviato
localmente sarebbe possibile valutare il livello di sincronizzazione della propria porzione di
dati ed avviare i meccanismi di trasferimento in caso ci si trovi ad essere rimasti ad una
versione precedente, o vi siano operazioni mancanti nel server. Gli aggiornamenti dell’indice potrebbero essere realizzati tramite alcuni trigger dove presenti, limitando quindi la
necessità di operare in maniera invasiva sulle query già predisposte per l’applicazione.
60
Purtroppo nel momento in cui le partizioni personali del database si trovano a non rispettare più limiti precisi, o ad essere addirittura condivise, si rende necessaria la costruzione
di trigger da attivare in cascata, poiché la modifica di un record condiviso tra molti utenti
comporta l’aumento di indice di progressione non solo per l’utente che l’ha scatenato, ma
anche per tutti gli utenti con visibilità diretta o indiretta sul tale record. Nel caso in cui
l’aggiornamento includa già in cascata l’aggiornamento di altri valori le cose si complicano
ulteriormente. Dalle semplici considerazioni espresse finora si può capire come l’introduzione
della condivisione dei dati comporta un esplosione di problematiche non indifferenti.
5.3.2 DBDigest
Una tecnica di controllo di versione differente potrebbe consistere nell’eseguire una query
di selezione sui dati (o su una parte dei dati) a cui l’utente ha accesso, ed estrarne un digest
(riassunto) da trasferire al dispositivo client, il quale potrebbe eseguire la stessa query e
confrontare i valori ottenuti. Creare una query di questo tipo è relativamente semplice: è
sufficiente combinare insieme i comandi SQL CONCAT, GROUP CONCAT ed MD5 per
trasformare una proiezione di dati ottenuti da un comando SELECT in un digest estremamente compatto e facile da trasferire. Elaborando inoltre comandi come LIMIT e OFFSET
è possibile ipotizzare dei marcatori differenziati per blocchi di dati, permettendo quindi
una sorta di ispezione ad albero dei blocchi di dati, permettendo quindi un controllo delle
variazioni più specifico.
Una query di db digest con 64 caratteri, contenente anche un meccanismo di autenticazione
può essere di questo tipo:
1
2
3
4
5
6
7
8
9
SELECT CONCAT (
MD5 ( GROUP_CONCAT ( notes . title ) ) ,
MD5 ( GROUP_CONCAT ( notes . body ) )
)
FROM ‘ notes ‘
LEFT JOIN user
ON notes . user_id = user . user_id
WHERE notes . user_id =1
AND user . password = MD5 ( ’ password ’ )
Listing 5.1: esempio di DBDigest generico
In sostanza si esegue una selezione, si serializzano i valori dei record concatenandoli in gruppo, ottenendo una proiezione su una singola riga. A questo punto si esegue un digest con
MD5 del campo e si concatenano tutti i valori forniti per ottenere una stringa più o meno
lunga in base al numero di campi coinvolti.
Questo marcatore può essere ricalcolato in seguito a particolari eventi del server, o su
richiesta del client, in base alle necessità dell’applicazione. Utilizzando questa tipologia di
marcatori sarebbe possibile ottimizzare i confronti tra il database remoto e la controparte
locale, agendo non più sulle operazioni che ne provocando le variazioni, ma direttamente
sull’effetto che tali variazioni hanno sui dati.
61
5.3.3 Marcatori di operazioni incrementali legati a MVCC
L’utilizzo di un marcatore di digest permetterebbe di rilevare differenze tra la versione client
e quella server, ma non offre nessun vantaggio in fase di sincronizzazione, poiché non fornisce la possibilità di individuare efficientemente quale area del database abbia subito delle
modifiche. Questa mancanza rende necessario un controllo successivo o un importazione in
blocco dei dati contenuti nella tabella in esame.
Un punto di appoggio molto importante per implementare delle politiche di sincronizzazione efficaci potrebbe essere l’utilizzo di un marcatore di transazione relativo ai record.
Buona parte dei DBMS che implementano MVCC (Multi Version Concurrency Control, il
sistema di concorrenza che ne regola il funzionamento in ambienti multithread) marcano
per necessità del sistema ogni record con il codice dell’ultima transazione che lo ha modificato. Avere accesso a questo campo fornisce una possibilità veramente comoda: poter
selezionare tutti i record di una tabella modificati da un determinato momento in poi. Una
volta ottenuta una selezione cosı̀ specifica si può puntare a un sistema di sincronizzazione il
più efficiente possibile, in grado di recuperare solamente le differenze tra database remoto
e database locale, anche contestualizzate all’interno di una specifica finestra temporale.
I DBMS che forniscono nativamente queste informazioni sono PostgreSQL, Oracle e MySql
(con motore di memorizzazione InnoDB). SQLServer permette la creazione di un campo
di tipo timestamp che viene aggiornato automaticamente dal sistema ad ogni modifica del
record [8].
Da questo punto in poi viene assunto il DBMS PostgreSQL come riferimento per la ricerca delle caratteristiche e delle funzioni a supporto della sincronizzazione. Le funzionalità
avanzate offerte da PostgreSQL sono generalmente offerte anche dagli altri BDMS, seppur
in maniera meno accessibile ed in alcuni casi perfino nascosta.
In PostgreSql è possibile recuperare i marcatori MVCC leggendo il campo nascosto xmin.
Questo campo contiene il codice (XID) dell’ultima transazione che ha alterato il record.
La query in esempio confronta due tabelle, ordinandole in base ai record che hanno subito
l’ultimo aggiornamento. Il primo record della selezione conterrà sempre il codice attuale
della transazione di selezione (quindi l’ultimo XID generato).
1
2
3
4
5
6
7
8
9
SELECT cod , ’ clienti ’ AS table , xmin :: text :: bigint , xmax
FROM clienti
UNION
SELECT cod , ’ articoli ’ AS table , xmin :: text :: bigint , xmax
FROM articoli
UNION
SELECT ’ this_xid ’ , ’ -- ’ , txid_current () , ’ -- ’
ORDER BY xmin DESC
Listing 5.2: esempio di accesso al marcatore MVCC
62
Creando una VIEW con un istruzione di questo tipo sarebbe poi possibile ottenere l’elenco
ordinato degli ultimi aggiornamenti avvenuti nel database. Utilizzando la VIEW, o una
SELECT aggiuntiva è possibile eseguire una query che estragga solamente i valori necessari
alla sincronizzazione. Una query d’esempio che non utilizza VIEW potrebbe essere di questo
tipo:
1
2
3
4
5
6
7
8
9
10
11
12
SELECT * FROM (
SELECT cod , ’ clienti ’ AS table , xmin :: text :: bigint , xmax
FROM clienti
UNION
SELECT cod , ’ articoli ’ AS table , xmin :: text :: bigint , xmax
FROM articoli
UNION
SELECT ’ this_xid ’ , ’ -- ’ , txid_current () , ’ -- ’
ORDER BY
xmin DESC
) AS transaction_merge WHERE xmin > 1101696
Listing 5.3: selezione dei record modificati dopo una determinata transazione
Utilizzare una VIEW avrebbe però un vantaggio secondario: in caso di aggiornamento dello
schema sarebbe sufficiente modificare il comando costituente della VIEW, senza badare alla
modifica dell’insieme di query necessarie alla sincronizzazione.
Mantenendo una struttura simile a quella proposta sarebbe molto semplice implementare
un meccanismo di sincronizzazione: si tratterebbe semplicemente di interrogare la tabella
dei risultati, richiedendo poi per ognuno la tabella di appartenenza (2’ campo) e la chiave
primaria (1’ campo) per ottenere il record da modificare o inserire.
Utilizzando un meccanismo simile combinato con funzioni di raggruppamento e comandi
di tipo DISTINCT si otterrebbe l’elenco delle tabelle variate. Una volta ottenuto l’elenco sarebbe possibile procedere tabella per tabella alla creazione di specifiche query di
importazione.
5.3.4 Problematiche legate al Vacuum
A causa del limite di lunghezza del codice di transazione, il sistema può reggere fino a 4
miliardi di transazioni prima di riciclare i codici, i quali però vengono ristretti a 2 miliardi
per necessità del sistema (viene implementato un sistema circolare). Nel momento in cui
viene raggiunto l’ultimo codice disponibile il conteggio riparte da zero, trasformando quindi
tutte le operazioni passate in operazioni future. In questo caso nessuna transazione ha più
accesso ai dati di tali record, poiché essendo considerati futuri vengono resi inaccessibili dal
sistema MVCC.
Per risolvere il problema dell’accumulo di record inutilizzabili e del ricalcolo dei codici
di transazione (fenomeno conosciuto come XID Wraparound [9]) PostgreSql mette a disposizione una funzione chiamata Vacuum che si occupa di “pulire” il database da tutti i
parametri temporanei aggiuntivi generati dal sistema MVCC.
63
L’utilizzo della funzione Vacuum, con lo scopo di riallineare tutti i codici di transazione
dei record, forzando quindi tutte le transazioni a comparire come passate, presenterebbe
delle complicazioni nel caso la versione locale del database voglia sfruttare i codici MVCC.
Nel momento in cui il database remoto riparte con il conteggio delle transazioni, il database
locale non avrebbe modo di capire cosa sia avvenuto, e si troverebbe quindi in uno stato di
inconsistenza. Anche avendo la facoltà di determinare una chiamata alla funzione Vacuum,
non avrebbe idea di quali operazioni siano intercorse tra la sua ultima data di sincronizzazione ed il punto di Vacuum. In questo caso l’unica soluzione è verificare in altro modo
quali dati siano cambiati, e nel caso peggiore riscaricare l’intero database.
L’implementazione di MVCC fornita da PostgreSql ha delle caratteristiche che rendono
il codice di transazione complesso da utilizzare, ma meno problematico. L’operazione di
Vacuum, necessaria per riallineare i codici, non azzera i valori di un numero spropositato
di record, ma sposta semplicemente avanti il punto di partenza, ragionando in un ottica
circolare.
Per progettare un sistema di sincronizzazione tenendo conto di questa caratteristica bisogna confrontare l’ultimo codice di transizione archiviato localmente con l’ultimo codice
di transazione remoto. Se il codice di transazione remoto è inferiore a quello mantenuto in
locale significa che il codice ha superato il limite ed è ricominciato da zero. A questo punto
vanno eseguite non solo tutte le operazioni successive al codice dell’ultima transazione sincronizzata in locale, ma anche tutte quelle con un XID inferiore all’ultimo memorizzato.
Il caso peggiore in cui ci si può venire a trovare è però quello in cui il database remoto
ha effettuato talmente tante transazioni da aver non solo azzerato il contatore, ma anche
superato il codice locale. In questo caso rilevare un errore di questo tipo risulta difficoltoso,
e risulta ancora più difficile allineare le due copie dei database senza effettuare una copia
globale dei dati memorizzati nel server.
Per creare una politica di sincronizzazione efficace è necessario richiedere al server il codice dell’ultima transazione eseguita mediante il comando SQL SELECT txid_current().
Ottenuto il valore della transazione corrente è possibile verificare se esso sia successiva all’ultimo XID memorizzato localmente (in questo caso si intende lo XID proveniente dal
server, dal momento che il database client non implementa MVCC), ed in caso lo si trovi
successivo si procede all’inserimento o aggiornamento locale di tutti i record compresi nell’arco temporale tra le due transazioni. Se lo si trova inferiore si procede come già detto
all’aggiornamento di tutti i record con XID superiore all’ultimo aggiornato, per poi passare
all’aggiornamento di tutti i record con valore inferiore all’ultima transazione eseguita dal
database remoto.
64
5.3.5 Aggiunta di timestamp ai fini della sincronizzazione
Se fosse possibile affidarsi ai trigger (molto diffusi nei database relazionali standard), una
soluzione molto comoda sarebbe quella di marcare un apposito campo con il timestamp
dell’ultima modifica ad ogni operazione che si esegue sul record. Questa politica permetterebbe di eseguire dei confronti più semplici basati appunto sulle date di sincronizzazione,
ed eviterebbe problemi di overflow, almeno fino al 19 gennaio 2038 [10].
L’utilizzo di trigger finalizzati all’aggiornamento dei timestamp potrebbe essere comunque evitata, modificando le query di inserimento o modifica in modo da settare a NULL i
valori del determinato campo, che con un appropriato valore di default verrebbero automaticamente settati al timestamp attuale.
5.3.6 Inserire uno strato logico nel driver del database
In caso non fosse possibile utilizzare correttamente i trigger come base per l’implementazione
dei meccanismi di sincronizzazione, sarebbe possibile predisporre una propria implementazione di questi meccanismi, inserendosi se possibile nelle funzioni contenute nei driver del
database. Avere accesso a questo tipo di funzioni permetterebbe allo sviluppatore la modifica di ogni query eseguita nel database, o su di una porzione specifica dello stesso. Le query
modificate potrebbero includere dei dati aggiuntivi per la sincronizzazione, quali l’utente
responsabile dell’ultima transazione ed appunto il timestamp.
Una modifica di questo tipo potrebbe essere molto semplice da implementare su sistemi
preesistenti, nei quali andare a modificare le centinaia di query potrebbe non rappresentare
una scelta sicura e condivisibile.
5.4 Sincronizzare le eliminazioni
Ragionando sui meccanismi di sincronizzazione è facile notare come sia possibile gestire più
o meno facilmente inserimenti e modifiche di dati, mentre si rimane senza appigli per quanto
riguarda i dati eliminati.
Dal momento che tali dati non esistono più non vengono considerati dai DBDigest, né dalle
politiche di sincronizzazione basate su marcatori di transizione come MVCC, né da meccanismi in grado di sfruttare il timestamp di un record. La gestione delle eliminazioni richiede
a tutti gli effetti una gestione a sé stante.
Sempre a causa del funzionamento interno del sistema MVCC vengono mantenuti in memoria anche record obsoleti che andrebbero eliminati. Avere accesso anche a questi record
permetterebbe la determinazione non solo delle righe modificate, ma anche di quelle cancellate in un determinato arco temporale, permettendo quindi una sincronizzazione completa.
Vi è però un problema: questi record eliminati logicamente (dead-rows nella documentazione PostgreSQL) non sono accessibili con la stessa facilità con cui si accede ai campi nascosti
contenenti l’indice della transazione.
65
Esporre questi particolari dati potrebbe creare dei problemi di integrità, poiché esisterebbero ad esempio più valori per la stessa chiave. I DBMS in genere consentono l’accesso a
questi dati solamente con funzioni rigidamente controllate, e solo per scopi interni di debug.
Il problema dell’accesso ai record eliminati non è da sottovalutare poiché perdere l’accesso
ai dati eliminati rende necessario l’utilizzo di sistemi alternativi in grado di registrare le
cancellazioni.
5.4.1 Eliminazioni logiche
Una prima soluzione al problema potrebbe essere quella di utilizzare delle eliminazioni logiche, basate su di un apposito flag, che marchi il record come obsoleto. Utilizzando questa
tecnica sarebbe possibile sostituire le operazioni di eliminazione con operazioni di modifica
e si guadagnerebbe la possibilità di utilizzare il campo nascosto con il codice di transazione
per ottenere l’istante di eliminazione del record.
In realtà vi sono alcuni problemi legati a questo sistema: innanzitutto è necessario modificare tutte le query di lettura in modo che lavorino solamente con dati logicamente non
obsoleti, ed in caso ci si trovi a modificare un software con un parco query molto esteso,
magari contenente anche query complesse, il rischio di introdurre errori potrebbe diventare
alto.
Un problema secondario è la modifica delle query di inserimento e cancellazione, che devono
essere modificate a tempo d’esecuzione in caso esista già il record in esame.
Per quanto riguarda la cancellazione le possibilità sono due: o si elimina completamente il comando delete dalle query possibili e lo si trasforma in un comando di modifica, o si
impostano dei trigger che vengano attivati prima dell’eliminazione ed agiscano sul flag in
modo analogo. Il problema principale di questa tecnica deriva dalla debolezza dei trigger,
che non sono supportati da tutti i DBMS, e dove supportati spesso hanno implementazioni
differenti. In base alle implementazioni vi sono poi tutta una serie di situazioni in cui il
trigger non viene attivato, portando quindi il sistema in uno stato di inconsistenza logica.
Un secondo problema non indifferente è l’occupazione della chiave primaria. Se il record
è eliminato logicamente la chiave primaria risulta comunque occupata, e quindi un inserimento di un nuovo record con la stessa chiave primaria comporterebbe un problema di
integrità. A questo punto la chiave primaria del record eliminato deve essere modificata contestualmente al flag, magari aggiungendo un marcatore alla fine. Si potrebbe in alternativa
modificare l’operazione di inserimento facendo in modo che verifichi la possibilità di inserire
l’elemento ex novo, ed in caso contrario elimini il precedente, o lo modifichi assegnandogli
i propri valori.
66
5.4.2 Tabelle cestino
Il problema del parco query esteso da modificare potrebbe essere risolto almeno in parte con
due stratagemmi: il primo prevede la creazione di tabelle cestino, e il secondo la creazione di
apposite view che mascherino i dati, in modo da mantenere tutte le informazioni aggiuntive
ed i dati obsoleti nella tabella sottostante, invisibili tanto all’applicazione, quanto all’utente.
L’utilizzo di una tabella cestino prevede la sostituzione delle operazioni di eliminazione
con operazioni di spostamento da una tabella a quella cestino, in cui vengono memorizzati
tutti i dati obsoleti. Questo meccanismo permette l’utilizzo delle query di selezione e modifica originali, e riduce quindi l’impatto nel sistema preesistente. Un problema che sorge
però è legato alla dimensione del database, in quanto la duplicazione della maggior parte
delle tabelle potrebbe comportare una difficoltà di gestione e soprattutto di manutenzione,
poiché ad ogni modifica dei campi della tabella principale, deve essere eseguita una modifica
speculare della tabella cestino.
5.4.3 View di facciata
Una seconda alternativa per ridurre l’impatto di modifica richiesto dall’utilizzo delle eliminazioni logiche sfrutta il meccanismo delle VIEW per sostituire le tabelle preesistenti con
delle tabelle virtuali, che elaborino coerentemente i dati provenienti dalle entità originali,
a cui viene modificato il nome aggiungendo ad esempio un “ F” per indicare che si tratta
della tabella fisica.
Per implementare questa tecnica si dovrebbe prima di tutto modificare il nome della tabella,
aggiungendo il marcatore fisico, dopo di che si passa all’aggiunta dei campi di eliminazione
logica, timestamp e quant’altro. Una volta modificata la struttura originale della tabella si crea una vista che esegua una SELECT dei dati contenuti nella tabella sottostante,
escludendo i dati eliminati logicamente e nascondendo i campi aggiuntivi. Se come nome
della vista si sceglie il nome originale della tabella, tutte le funzioni di lettura non dovranno
aggiornare la propria definizione dei campi, né delle logiche di selezione, garantendo quindi
la piena compatibilità con le query preesistenti.
In questo modo i dati sono mantenuti tutti all’interno della stessa unità e si elimina la
necessità di una seconda tabella. Rimane però il problema delle query originali che comportano modifiche ed eliminazioni. Queste query devono essere riscritte in modo da lavorare
con la tabella fisica e non con la view, oppure devono essere impostati correttamente alcuni trigger in grado di attivarsi al tentativo di inserimento, modifica ed eliminazione sulla
VIEW. Tali operazioni andrebbero quindi indirizzate verso la tabella fisica sottostante.
67
5.4.4 Log delle eliminazioni
Una soluzione semplice ed estremamente efficace potrebbe essere il mantenere semplicemente un apposito registro per le eliminazioni. In una tabella (unica per ogni database)
potrebbero essere inseriti record contenenti un numero ristretto di campi: la chiave primaria
originale del record, il nome della tabella (che combinato con la chiave primaria originale
diventerebbe la chiave primaria della tabella di registro), il codice di transazione corrente o
il timestamp ed infine se necessario il codice dell’utente che ha eseguito l’eliminazione.
Mantenere un log temporizzato con le sole eliminazioni permetterebbe di abbandonare le
doppie tabelle e le viste, sfruttando semplicemente i codici di transazione per ottenere dalle
tabelle i dati variati e inseriti, e dalla tabella registro i record cancellati. In caso un record
cancellato venga reinserito successivamente nella tabella di origine, per evitare che il sistema
rischi di confondersi, non capendo se inserire od eliminare il proprio valore, sarebbe sempre
possibile analizzare i codici di transazione per capire quale operazione è stata eseguita per
ultima e sceglierla come più affidabile.
68
6 Conclusioni
Questo stage mi ha dato modo di approfondire nel dettaglio le caratteristiche delle applicazioni offline, che se ben progettate risultano essere estremamente funzionali. La realizzazione
di vari prototipi mi ha dato modo di valutare concretamente le difficoltà in cui ci si imbatte
nel momento in cui si abbandona il filone principale dello sviluppo web.
Ragionare in un ottica pienamente offline comporta delle modifiche sostanziali al modo
di progettare un applicazione web, ed il modo migliore di comprendere queste modifiche è
sicuramente addentrarsi nel dettaglio nello sviluppo, cercando di simulare alcuni dei sistemi
già esistenti per comprenderne le funzionalità.
I grandi scontri che avvengono tra le principali compagnie internazionali, riguardanti il
futuro del web, lasciano intendere come questo ambiente abbia le carte in regola per rappresentare un punto di convergenza per alcuni ambiti della programmazione. La ricerca
delle motivazioni alla base di questi scontri permette di capire quanto ogni parte protegga
i propri progetti e tenga al loro sviluppo.
L’approccio tecnico che ho mantenuto durante la ricerca degli aspetti problematici e le
relative soluzioni mi ha dato modo di andare oltre le comuni istruzioni per l’uso ed addentrarmi alla ricerca delle caratteristiche interne più particolari, anche se spesso si sono
rivelate inutilizzabili per le normali applicazioni web.
Pur non essendo arrivato a definire un modello completo di applicazione offline, ho acquisito
un bagaglio di conoscenze tale da permettermi di capire e distinguere le varie applicazioni
offline, suddividendole in categorie differenti in base al loro scopo e alla loro implementazione. Comprendere le problematiche alla base di certe scelte richiede capacità di analisi, e
l’ideazione di soluzioni, specialmente nell’ambito della sincronizzazione, richiede una certa
dose di inventiva e creatività.
Ritrovarsi ad immaginare le applicazioni di domani, e rendersi conto che vi si è già fatto
qualche passo all’interno, è di sicuro una gran soddisfazione.
69
Bibliografia
[1] Standard W3C per il file manifest e l’application cache
http://www.w3.org/TR/2011/WD-html5-20110525/offline.html.
[2] Standard W3C per il database locale WebSQL
http://www.w3.org/TR/webdatabase/.
[3] Standard W3C per il database locale IndexedDB
http://www.w3.org/TR/IndexedDB/.
[4] Specifiche degli eventi di caching relativi all’application cache
http://www.whatwg.org/specs/web-apps/current-work/#appcacheevents.
[5] Confronto tra i database locali a cura dei laboratori Mozilla.
http://hacks.mozilla.org/2010/06/comparing-indexeddb-and-webdatabase/,
http://blog.harritronics.com/2011/04/more-thoughts-on-indexeddb-and-web-sql.
html.
[6] Confronto tra database non relazionali: Cassandra, MongoDB, CouchDB, Redis
http://kkovacs.eu/cassandra-vs-mongodb-vs-couchdb-vs-redis.
[7] Confronto tra database relazionali e non relazionali
http://www.readwriteweb.com/enterprise/2009/02/
is-the-relational-database-doomedp2.php.
[8] Informazioni specifiche relative al sistema di controllo di concorrenza MVCC ed alla
sua implementazione nei vari DBMS
http://devcenter.heroku.com/articles/postgresql-concurrency,
http://www.sqlteam.com/forums/topic.asp?TOPIC_ID=166957,
http://dev.
mysql.com/doc/refman/5.0/en/innodb-multi-versioning.html,
http://msdn.microsoft.com/en-us/library/cc645937(v=SQL.100).aspx,
http://www.postgresql.org/docs/6.3/static/c0503.htm,
http://msdn.microsoft.com/en-us/library/ms182776.aspx.
[9] Informazioni relative all’implementazione PostgreSql della routine di Vacuum
http://www.postgresql.org/docs/current/static/routine-vacuuming.html#
VACUUM-FOR-WRAPAROUND
[10] Considerazioni sul tempo limite offerto da Unix Timestamp
http://en.wikipedia.org/wiki/Year_2038_problem.
70
Scarica