Un approccio più strutturato ed efficiente nella gestione di grandi quantità di dati non può prescindere da una qualche forma di database. Ed in effetti un tentativo di introdurre il supporto di un motore di database locale accessibile da JavaScript è stato fatto con le specifiche di Web SQL Database. Purtroppo già nel mondo SQL non esiste una vera omogeneità nell'aderenza a quello che dovrebbe essere uno standard affermato, come dimostra la moltitudine di dialetti SQL esistenti. Proporre un supporto standard all'SQL nel mondo Web non ha fatto che complicare le cose per cui il progetto di definire delle specifiche accettate da tutti è presto naufragato e di fatto Web SQL Database non è più supportato dal W3C.
Al suo posto è stato proposto Indexed Database API, noto anche come IndexedDB.
Cos'è IndexedDB
IndexedDB non è un database relazionale ma un Object Store. Comprendere la differenza è fondamentale per poter utilizzare al meglio IndexedDB. Infatti, mentre un database relazionale organizza i dati in tabelle composte da colonne e righe e prevede un linguaggio specializzato per gestire i dati (tipicamente SQL), un Object Store consente direttamente la persistenza di oggetti e prevede la definizione di indici per un loro recupero efficiente.
Tra le altre caratteristiche di IndexedDB segnaliamo la natura transazionale delle operazioni e la disponibilità sia di API sincrone che asincrone.
Vediamo come interagire con un database tramite IndexedDB. Al solito, la verifica del supporto di IndexedDB da parte del browser corrente va fatta semplicemente verificando che l'oggetto omonimo sia disponibile:
if (!window.indexedDB) {
console.log("Il tuo browser non supporta indexedDB");
}
Open
Possiamo quindi effettuare l'apertura di un database tramite il metodo open(), come mostrato di seguito:
var request = window.indexedDB.open("dati", 1);
Abbiamo specificato come parametri del metodo open() il nome del database e il numero di versione. Come vedremo più avanti, quest'ultimo è fondamentale nella creazione e nella modifica della struttura di un Object Store.
Il metodo open()
opera in maniera asincrona restituendo un oggetto di tipo IDBRequest sul quale possiamo definire dei gestori di evento. In particolare possiamo gestire i seguenti eventi:
Evento | Descrizione |
---|---|
success | si verifica quando l'apertura del database è andata a buon fin |
error | si verifica quando c'è un problema nell'apertura del database |
upgradeneeded | indica la situazione in cui un database non esiste o quando la sua versione è differente da quella specificata |
La gestione di questi eventi avviene assegnando ad alcune proprietà dell'oggetto di tipo IDBRequest il relativo gestore. Ad esempio, possiamo assegnare la seguente funzione da eseguire in corrispondenza dell'apertura con successo di un database:
request.onsuccess = function(event) {
db = event.target.result;
...
};
Notiamo come l'oggetto corrispondente al database appena aperto è ricavato da un proprietà dell'oggetto event passata al gestore dal sistema.
Per gestire situazioni d'errore assegniamo un'apposita funzione alla proprietà onerror:
request.onerror = function(event) {
console.log("Si è verificato un errore nell'apertura del DB");
};
Creare un Object Store
Nella creazione di un nuovo database o nella modifica di un database esistente possiamo gestire l'evento upgradeneeded per creare, ad esempio, il nostro Object Store:
request.onupgradeneeded = function(event) {
var db = event.target.result;
if(db.objectStoreNames.contains("utenti")) {
db.deleteObjectStore("utenti");
}
var store = db.createObjectStore("utenti", {keyPath: "id"});
};
Come possiamo vedere, per prima cosa recuperiamo il database da modificare o appena creato. Quindi verifichiamo se il nostro Object Store, cioè il contenitore di oggetti, esiste già. Nel nostro esempio se esiste un Object Store dal nome utenti
lo eliminiamo dal database.
Infine creiamo l'Object Store tramite il metodo createObjectStore() a cui passiamo il nome dello store e un oggetto che ci consente di definire delle importanti proprietà opzionali. Nel caso specifico abbiamo definito la proprietà id
come chiave univoca dello store. Questo vuol dire che tutti gli oggetti che saranno memorizzati nello store dovranno avere questa proprietà e che non possono esserci più oggetti nello store con lo stesso valore per questa proprietà.
La gestione dell'evento upgradeneeded
è l'unica maniera per modificare la struttura di un database, aggiungendo o togliendo Object Store o definendo indici. Pertanto, se vogliamo modificare il nostro database dobbiamo cambiare la sua versione in modo da scatenare questo evento.
Nota: Se il database che intendiamo aprire tramite open()
esiste ed è della stessa versione indicata, l'evento upgradeneeded
non verrà generato.
L'esecuzione "differita" di Open()
Prima di proseguire nell'esplorazione delle API di IndexedDB, evidenziamo una particolarità del metodo open(). La sua esecuzione non è immediata, ma avviene all'uscita dell'esecuzione della funzione in cui viene invocata. Questo è il motivo per cui possiamo invocare questo metodo ed assegnare subito dopo i gestori di evento all'oggetto restituito.
In genere lo schema seguito nella gestione di un database IndexedDB è quello mostrato di seguito:
var dati = {};
dati.version = 1;
dati.open = function() {
var request = window.indexedDB.open("dati", this.version);
request.onupgradeneeded = function(event) {
var db = event.target.result;
if(db.objectStoreNames.contains("utenti")) {
db.deleteObjectStore("utenti");
}
var store = db.createObjectStore("utenti", {keyPath: "id"});
}
request.onsuccess = function(event) {
dati.db = event.target.result;
}
request.onerror = function(event) {
console.log("Si è verificato un errore nell'apertura del DB");
}
};
Nell'esempio, la variabile dati
rappresenta il nostro database e il suo metodo open()
si occupa di aprirlo o crearlo definendo i relativi eventi.
Add e put, aggiungere oggetti all'Object Store
Vediamo ora come aggiungere un oggetto all'Object Store utenti
:
dati.addUtente = function(utente) {
var db = dati.db;
var trans = db.transaction(["utenti"], "readwrite");
var store = trans.objectStore("utenti");
var request = store.add({
"id": utente.id,
"nome": utente.nome,
"cognome": utente.cognome,
});
request.onsuccess = function(e) {
console.log("Utente inserito correttamente!");
}
request.onerror = function(e) {
console.log("Si è verificato un errore nell'inserimento di un utente!");
}
};
Come possiamo vedere, creiamo prima di tutto una transazione in lettura e scrittura sull'Object Store utenti
. Ogni operazione su un Object Store deve essere eseguita all'interno di un transazione. Da questa otteniamo un riferimento all'Object Store di cui invochiamo il metodo add() specificando l'oggetto da aggiungere. I gestori di evento permettono di gestire opportunamente l'esito dell'operazione.
Per la modifica di un oggetto esistente l'approccio è del tutto analogo. Al posto del metodo add() utilizziamo il metodo put():
dati.updateUtente = function(utente) {
var db = dati.db;
var trans = db.transaction(["utenti"], "readwrite");
var store = trans.objectStore("utenti");
var request = store.put({
"id": utente.id,
"nome": utente.nome,
"cognome": utente.cognome,
});
request.onsuccess = function(e) {
console.log("Utente inserito correttamente!");
}
request.onerror = function(e) {
console.log("Si è verificato un errore nell'inserimento di un utente!");
}
};
In realtà avremmo potuto utilizzare il metodo put()
anche per inserire un nuovo utente, quindi al posto di add()
. Infatti, la differenza sostanziale tra i due metodi consiste nel fatto che add()
richiede che non sia presente sullo store un altro oggetto con la stessa chiave, mentre put()
non pone questo vincolo: se esiste un oggetto lo aggiorna altrimenti ne inserisce uno nuovo.
Get, recuperare gli oggetti dall'Object Store
Per recuperare l'utente in base al suo id
possiamo scrivere il seguente codice:
dati.getUtente = function(idUtente) {
var db = dati.db;
var trans = db.transaction(["utenti"], "readonly");
var store = trans.objectStore("utenti");
var request = store.get(idUtente);
request.onsuccess = function(e) {
console.log("Utente trovato: " + e.target.result.nome + " " + e.target.result.cognome);
}
request.onerror = function(e) {
console.log("Si è verificato un errore nella ricerca dell'utente");
}
};
Sempre partendo da una transazione sull'Object Store utenti
abbiamo utilizzato il metodo get()
dello store passando il valore della chiave in base alla quale recuperare l'utente.
Delete, eliminare un oggetto dall'Object Store
L'eliminazione di un oggetto dallo store è altrettanto semplice:
dati.deleteUtente = function(idUtente) {
var db = dati.db;
var trans = db.transaction(["utenti"], "readwrite");
var store = trans.objectStore("utenti");
var request = store.delete(idUtente);
request.onsuccess = function(e) {
console.log("Utente eliminato");
}
request.onerror = function(e) {
console.log("Si è verificato un errore durante l'eliminazione dell'utente");
}
};
In questo caso utilizziamo il metodo delete() passando la chiave di identificazione dell'oggetto da eliminare.
Sfruttare gli indici degli IndexedDB
Come il nome stesso suggerisce, IndexedDB consente di creare indici per recuperare gli oggetti presenti nello store in maniera efficiente. Mentre negli esempi che abbiamo visto finora abbiamo sfruttato la chiave primaria per recuperare gli oggetti dallo store, se vogliamo recuperare un oggetto utilizzando il valore di un'altra proprietà dobbiamo definire e utilizzare un indice.
CreateIndex
Supponiamo ad esempio di voler effettuare una ricerca nello store utenti
tramite il cognome
. Come prima cosa, il database deve avere un indice definito su questa proprietà. Lo possiamo creare come mostrato di seguito:
var store = db.createObjectStore("utenti", {keyPath: "id"});
store.createIndex("indiceCognome", "cognome", {unique: false});
Dopo aver creato lo store, utilizziamo il suo metodo createIndex() passando come primo parametro il nome dell'indice, come secondo parametro il nome della proprietà da indicizzare e come terzo parametro un oggetto che rappresenta le opzioni di creazione dell'indice. Nel nostro caso abbiamo specificato che l'indice non è univoco, cioè possono esistere nello store utenti con lo stesso cognome.
Per utilizzare un indice nelle ricerche procediamo nel seguente modo:
dati.getUtentePerCognome = function(cognome) {
var db = dati.db;
var trans = db.transaction(["utenti"], "readonly");
var store = trans.objectStore("utenti");
var indice = store.index("indiceCognome");
var request = indice.get(cognome);
request.onsuccess = function(e) {
console.log("Utente trovato: " + e.target.result.nome + " " + e.target.result.cognome);
}
request.onerror = function(e) {
console.log("Si è verificato un errore nella ricerca dell'utente");
}
};
Come possiamo vedere, dopo aver recuperato l'indice dello store tramite il metodo index(), invochiamo il suo metodo get()
specificando come parametro il cognome dell'utente.
Il resto della gestione dell'esito della ricerca avviene come al solito.