Queste API nascono per dare la possibilità di creare e manipolare un database di ispirazione NoSQL memorizzato all'interno del browser dell'utente. Ogni database, identificato da un nome, può contenere un numero arbitrario di Object Store
, letteralmente contenitori di oggetti: ad esempio un ipotetico database ‘libreria' potrebbe ospitare i seguenti Object Store
: Libri, Clienti. Sia ‘Libri' che ‘Clienti' non sono altro che strutture paragonabili ad array associativi ordinati, dove ogni coppia chiave-valore rappresenta un oggetto, quindi in questo caso o un libro o un cliente.
All'interno di un Object Store
è possibile eseguire le normali operazioni di inserimento, modifica, eliminazione e ricerca; le API sono state pensate per ragionare con record in un formato JSON-like ma nulla vieta di memorizzare oggetti Javascript di fattura diversa, purché serializzabili.
Come funzionano le API
Utilizziamo Chromium per un tour operativo di questo interessante set di API; iniziamo preparando un documento HTML5 e creando il database:
<!doctype html>
<html>
<head>
<title> WebLibreria: gestionale per librerie </title>
<script>
setup = function(){
if ('webkitIndexedDB' in window){
indexedDB = webkitIndexedDB;
IDBCursor = webkitIDBCursor;
IDBKeyRange = webkitIDBKeyRange;
IDBTransaction = webkitIDBTransaction;
}else if ('moz_indexedDB' in window){
indexedDB = moz_indexedDB;
}
}
init = function(){
setup();
var request = indexedDB.open("WebLibreria", "Il gestionale per librerie");
}
</script>
</head>
<body onload="init();">
</body>
</html>
Essendo, ad oggi, queste features ancora sperimentali Chromium (e Firefox) prefissano le classi di riferimento con la particella ‘webkit': la funzione ‘setup
' serve solamente per creare degli alias che abbiano nomi più aderenti alle specifiche. Mano a mano che queste API si avvicineranno allo status di standard assisteremo alla rimozione di questi prefissi e potremo commentare la funzione setup. La creazione del database avviene attraverso l'istruzione ‘indexedDB.open
', che accetta come parametro il nome e la descrizione dell'oggetto che stiamo creando; in caso un database con lo stesso nome e lo stesso dominio di origine sia già presente nel browser allora verrà utilizzato quest'ultimo.
A questo punto possiamo intercettare gli eventi di avvenuta creazione della connessione e di errore durante la procedura gestendoli nel modo che meglio ci aggrada:
init = function(){
setup();
var request = indexedDB.open("WebLibreria", "Il gestionale per librerie");
request.onsuccess = function(){console.log("Connessione Ok!");}
request.onerror = function(){console.log("Ops, errore");}
}
Eseguiamo la pagina all'interno di Chromium prestando attenzione alla linguetta 'console' all'interno dei Developer Tools (CTRL + SHIFT + J
) e avremo conferma dell'avvenuta connessione/creazione al database (figura 1):
Ora che abbiamo stabilito una connessione è essenziale determinare se l'utente sia alla sua prima visita, perché in questo caso è necessario procedere con la creazione degli object store e della struttura necessari. Per gestire questo meccanismo le API mettono a disposizione il concetto di versione, impostata a null per un database appena creato, che può essere utilizzata proprio capire quando sia necessario apportare modifiche alla struttura. Ad esempio:
...
init = function(){
setup();
var request = indexedDB.open("WebLibreria", "Il gestionale per librerie");
request.onsuccess = controllaVersione;
request.onerror = function(){console.log("Ops, errore");}
}
controllaVersione = function(event){
window.database = event.result;
if(database.version != "1.0"){
var request = database.setVersion("1.0");
request.onsuccess = aggiornaLoSchema;
request.onerror = function(){console.log("Ops, errore");}
}else{
// il database è già aggiornato alla versione più recente
}
}
aggiornaLoSchema = function(){
console.log("Qui posso aggiornare lo schema");
}
...
La funzione aggiornaLoSchema
viene invocata solamente nel caso in cui la versione del database sul browser dell'utente sia diversa da quella attesa dal javascript (in questo caso la ‘1.0'); in particolare registrare una funzione sul callback ‘onsuccess
' del metodo setVersion
è l'unico modo in cui sia consentito dalle specifiche modificare la struttura del database.
Se ora provate questo codice all'interno del browser, nella linguetta console vedrete comparire la scritta ‘Qui posso aggiornare lo schema', se poi ricaricate nuovamente la pagina invece noterete che tale messaggio scompare: il database è infatti già nella versione “1.0”, quindi il flusso del programma transita attraverso il blocco else, che è al momento vuoto. Per cancellare il database creato e poter così ripetere la procedura è necessario svuotare completamente la cache, chiudere Chromium e riaprirlo (figura 2):
A questo punto creiamo la struttura del database: per prima cosa dobbiamo aprire una transazione, successivamente istruiamo il database alla creazione dell'object store
‘libri', con chiave sul campo ‘isbn', e di due indici, rispettivamente per i campi ‘titolo' e ‘autore'. Approfittiamone anche per creare la funzione recuperaLibri
, che verrà invocata sia al completamento della struttura sia nel caso il database sia già alla versione desiderata:
...
controllaVersione = function(event){
window.database = event.result;
if(database.version != "1.0"){
var request = database.setVersion("1.0");
request.onsuccess = aggiornaLoSchema;
request.onerror = function(){console.log("Ops, errore");}
}else{
recuperaLibri();
}
}
aggiornaLoSchema = function(){
window.transazione = event.result;
transazione.oncomplete = recuperaLibri;
transazione.onabort = function(){console.log("Ops, errore");}
var libri = database.createObjectStore("libri", "isbn", false);
var titolo = libri.createIndex("titolo", "titolo", false);
var autore = libri.createIndex("autore", "autore", false);
}
recuperaLibri = function(){
// recupera l'elenco dei libri e stampali a video
}
...
La funzione createObjectStore
richiede 3 parametri, di cui solamente il primo, il nome, obbligatorio; il secondo parametro rappresenta il nome della proprietà del record che vogliamo funga da chiave all'interno dell'object store
. Il terzo parametro imposta invece la proprietà autoincrement
; nel caso non sia presente una chiave nel record in inserimento e autoincrement = true
, una chiave verrà generata in automatico. Le due chiamate alla funzione createIndex
provvedono alla creazione di due indici, utili per ricerche e query, sui campi ‘titolo' e ‘autore'; il terzo parametro impostato a false
consente la presenza di valori duplicati. Ora che abbiamo creato la struttura del database stampiamo a video l'elenco, per ora vuoto, dei libri registrati e creiamo un piccola form per l'inserimento di un nuovo volume:
...
recuperaLibri = function(){
window.transazione = database.transaction(["libri"],
IDBTransaction.READ_WRITE, 0);
var request = transazione.objectStore("libri").openCursor();
request.onsuccess = stampaLibri;
request.onerror = function(){console.log("Ops, errore");}
}
stampaLibri = function(){
var cursor = event.result;
if( cursor != null){
document.getElementById("catalogo_libri").insertAdjacentHTML('beforeend',
"<li>" + cursor.value.autore + ": " + cursor.value.titolo +
" (ISBN: "+ cursor.value.isbn +"); </li>");
cursor.continue();
}
}
aggiungiLibro = function(){
// aggiungiamo un nuovo libro all'object store
}
</script>
</head>
<body onload="init();">
<h1> WebLibreria: gestionale per librerie</h1>
<section>
<h1>Elenco dei libri</h1>
<ul id="catalogo_libri">
</ul>
</section>
<aside>
<h1>Aggiungi un nuovo libro</h1>
<form name="aggiungi_libro" onsubmit="aggiungiLibro(this); return false;">
<fieldset name="info_libro">
<legend>Dati richiesti:</legend>
<label>Titolo:
<input name="titolo" type="text" required placeholder="es: Dalla terra alla luna">
</label>
<label>Autore:
<input name="autore" type="text" required placeholder="es: Jules Verne">
</label>
<label>ISBN:
<input name="isbn" type="text" required placeholder="es: 8862221320">
</label>
<input type="submit" value="Aggiungi">
</fieldset>
</form>
</aside>
</body>
</html>
Possiamo tranquillamente tralasciare la spiegazione della nuova porzione di codice HTML aggiunto: trattasi semplicemente di un form che all'invio chiama una funzione, ancora da sviluppare, per l'aggiunta di un nuovo libro. Molto più interessanti sono invece recuperaLibri
e stampaLibri
. In recuperaLibri
viene creata una nuova transazione che coinvolge l'object store
‘libri'; il secondo parametro indica il tipo di transazione: purtroppo lettura/scrittura (IDBTransaction.READ_WRITE
) è l'unica opzione supportata ad oggi; il terzo valore rappresenta invece il timeout della transazione: lo 0 utilizzato significa mai. Vediamo l'istruzione successiva:
var request = transazione.objectStore("libri").openCursor();
La funzione openCursor
inizializza un puntatore, detto anche cursore, al primo record dell'object store
‘libri'. La funzione stampaLibri
, che viene chiamata appena il cursore è stato creato e popola una lista con i libri in catalogo, agisce come una specie di ciclo, infatti il metodo continue non fa nient'altro che richiamare nuovamente stampaLibri, posizionando però il cursore al record successivo; questo fino a quando non si giunge alla fine dei risultati di ricerca, a quel punto il valore del cursore diviene null
e un apposito if
si preoccupa dell'abbandono della funzione.
Completiamo il nostro progetto con la funzione aggiungiLibro
:
...
aggiungiLibro = function(data){
var elements = data.elements
window.transazione = database.transaction(["libri"],
IDBTransaction.READ_WRITE, 0);
var request = transazione.objectStore("libri").put({
titolo: elements['titolo'].value,
autore: elements['autore'].value,
isbn: elements[ 'isbn'].value
}, elements[ 'isbn'].value));
request.onsuccess = function(){pulisciLista(); recuperaLibri();}
request.onerror = function(){console.log("Ops, errore");}
}
pulisciLista = function(){
document.getElementById("catalogo_libri").innerHTML ="";
}
</script>
Il metodo interessante in questo caso è il put
, che si preoccupa di aggiungere al database, previa apertura di una transazione appropriata, un nuovo record con gli elementi ricevuti dal form (da notare il secondo parametro, corrispondente alla chiave del record). Da notare che esiste anche un metodo add
che differisce nell'impedire la creazione di un record la cui chiave sia già presente nel database.
Eseguiamo un ultima volta l'applicazione, proviamo ad inserire un paio di libri ed ammiriamone il risultato (figura 3):
Una funzione di ricerca
Aggiungiamo questa funzione al progetto:
...
ricercaLibro = function(autore){
pulisciLista();
window.transazione = database.transaction(["libri"],
IDBTransaction.READ_WRITE, 0);
var request = transazione.objectStore("libri").index('autore').openCursor(
IDBKeyRange.bound(autore,autore+"z",true,true), IDBCursor.NEXT);
request.onsuccess = stampaLibri;
request.onerror = function(){console.log("Ops, errore");}
}
</script>
L'unica differenza rispetto alla già analizzata recuperaLibri
sta nel fatto che, in questo caso, con il metodo ‘index(‘autore')
' indichiamo al database che la selezione dovrà essere fatta utilizzando come discriminante l'indice ‘autore' creato sull'omonimo campo. In particolare tale selezione dovrà recuperare tutti i record il cui autore inizia con una stringa passata al metodo. Per provare questa funzione di ricerca creiamo un nuovo frammento HTML in coda alla form di inserimento:
...
</form>
<h1>Cerca un libro per autore</h1>
<form name="cerca_per_autore"
onsubmit="ricercaLibro(this.elements['keyword'].value); return false;">
<fieldset name="campi_ricerca">
<legend>Ricerca per autore:</legend>
<label>Inizio del nome:
<input name="keyword" type="search" required placeholder="es: Franc">
</label>
<input type="submit" value="Ricerca">
</fieldset>
</form>
</aside>
...
Ricarichiamo l'applicazione e proviamo ad inserire una valida stringa di ricerca (figura 4):
Conclusione
Le Indexed Database API sono uno strumento estremamente potente che consentono di beneficiare di un vero e proprio database all'interno del browser dell'utente. Le stesse funzionalità, anche se in una sintassi più orientata a SQL, venivano offerte anche dalle promettenti Web SQL Database API, abbandonate dal 4 dicembre 2010 per motivi di sicurezza e di, ironia della sorte, troppa uniformità di implementazione da parte degli user-agent (avevano tutti scelto SQLlite come back-end per questo tipo di specifiche).
Prima di passare alla prossima lezione ricordo che le stesse API che abbiamo usato in forma asincrona all'interno di questo progetto sono disponibili anche nel formato sincrono; per maggiori informazioni in questo senso rimando alla documentazione ufficiale.
Per testare il tutto potete partire dalla demo.
Tabella del supporto sui browser
API e Web Applications | |||||
---|---|---|---|---|---|
Indexed Database API | 9.0+ (parziale) | 4.0+ (parziale) | 5.0+ (parziale) | 8.0+ (parziale) | No |