IndexedDB, hai davvero bisogno di un Database Remoto?

Come IndexedDB ci viene in aiuto nella gestione della persistenza offline dei nostri applicativi

Andrea Di Blasi
10 min readSep 23, 2023

Gli snippets di codice riportati nell’articolo sono visionabili a questo repo Github

Il titolo è chiaramente una provocazione, in quasi la totalità dei casi la risposta risulterà affermativa. Tuttavia con questo articolo vorrei fornirti un’alternativa grazie alla quale puoi gestire grandi quantità di dati direttamente nel browser senza dover dipendere da un database remoto. Sto parlando di IndexedDB, un’API web avanzata che consente di archiviare e gestire dati in modo efficiente, scalabile e affidabile.

Introduzione

Image by fullvector on Freepik

Nell’ambito dello sviluppo web, spesso ci si affida a DB remoti per archiviare e leggere dati. Questa è la soluzione più comune, e consente agli applicativi di gestire l’accesso alle informazioni da diverse piattaforme e dispositivi. Tuttavia, ci sono situazioni in cui questa soluzione potrebbe non essere la scelta ideale.

Immagina di dover sviluppare un’applicazione web che necessiti di funzionare anche in assenza di connessione, e in aggiunta richieda affidabilità e un accesso veloce alle informazioni anche senza la disponibilità di una rete; in questo scenario, l’utilizzo di componenti online sarebbe un problema, dato che ovviamente dipenderebbero costantemente da internet per funzionare. Ecco come IndexedDB si propone di fare al fine di fornire una soluzione per affrontare questa sfida.

Come anticipato, è un API web di basso livello che consente di archiviare grandi quantità di dati direttamente nel browser dell’utente, offrendo una capacità di archiviazione che varia a seconda del Browser scelto (firefox non ha un limite sulla sua dimensione) superando le restrizioni di spazio di soluzioni più comunemente adottate come il localStorage.

In aggiunta a ciò, vedremo che IndexedDB offre anche la possibilità di eseguire ricerche e query complesse sui dati, consentendo un accesso rapido ed efficiente alle informazioni desiderate. Inoltre, gli elementi conservati al suo interno sono salvati e non vengono cancellati nemmeno alla chiusura del browser, garantendone la persistenza (tranne in alcuni casi che vedremo tra poco).

Principi e componenti

IndexedDB è un sistema di archiviazione di tipo transazionale, dove i dati vengono conservati su base chiave/valore. Tuttavia, a differenza dei database relazionali, IndexedDB è un “JavaScript-based object-oriented database”; ciò significa che al suo interno può essere salvato qualsiasi tipo di oggetto non strutturato. Questa flessibilità lo particolarmente adatto a memorizzare dati complessi e eterogenei.

Indexed DB visto dalla console

Parlando dei componenti che è necessario conoscere quando si lavora con IndexedDB, il primo elemento chiave è uno o più Database. Come potrai intuire, un database in IndexedDB è un contenitore logico di dati. Puoi creare più Database all’interno dello stesso applicativo al fine di organizzare i dati in modo coerente.

All’interno di ciascun database, troviamo gli “object store” che sono concettualmente paragonabili alle tabelle in un database SQL. Gli object store contengono gli oggetti, ciascuno dei quali ha una propria chiave univoca.

Per facilitare le ricerche su questi dati, IndexedDB supporta anche il concetto degli indici. Essi sono una parte fondamentale dell’architettura, poiché consentono di creare strutture di dati ottimizzate per query specifiche. Immagina di gestire un insieme di oggetti “Libri” memorizzati all’interno di un object store dedicato. Ora, supponiamo che tu abbia l’esigenza di effettuare una ricerca per trovare tutti i libri di uno specifico genere. In questa situazione, la creazione di un indice basato su quest’attributo dell’entità ti permetterà di effettuare una ricerca veloce ed efficiente.

Ecco come risulterebbe l’implementazione lato codice

// Opens (or creates) a database called "BookshopDB" with version 1
const request = indexedDB.open('BookshopDB', 1);

// manage the event fired when the database is created or upgraded
request.onupgradeneeded = (event) => {
const db = event.target.result;
// create an object store called "Books" with an index called "genre"
const bookStore = db.createObjectStore('Books', { keyPath: 'isbn' });
bookStore.createIndex('genre', 'genre', { unique: false });
// insert some data into the object store
bookStore.add({ isbn: '12345', title: 'The Great Gatsby', genre: 'Novels' });
bookStore.add({ isbn: '67890', title: '1984', genre: 'Distopy' });
};
// manage the event fired when the database is opened with success
request.onsuccess = (event) => {
const db = event.target.result;
// execute a transaction to read data from the object store
const transaction = db.transaction(['Books'], 'readwrite');
const bookStore = transaction.objectStore('Books');
// execute a request to get all the books with genre "Novels"
const genreIndex = bookStore.index('genre');
const request = genreIndex.getAll('Novels');
request.onsuccess = (event) => {
const novels = event.target.result;
console.log('Novels: ', novels);
};
transaction.oncomplete = () => {
db.close();
};
};

Questo esempio crea un database chiamato “BookshopDB” con un object store chiamato “Books” che contiene libri con attributi “isbn”, “titolo” e “genre”. Viene quindi creato un indice “genre” per permettere la ricerca ottimizzata per genere. Successivamente, viene eseguita una ricerca per trovare tutti i libri con il genere “Novels”.

Operazioni concorrenti sullo stesso DB

Come potrai notare dal nostro esempio pratico, c’è una cosa che spicca: le “transazioni”. In IndexedDB esse sono davvero importanti, perché rendono sicuro e coerente il modo in cui gestiamo i dati.

Immagina cosa succederebbe se non ci fossero. Due finestre separate del browser, o perfino due schede dello stesso browser, potrebbero cercare di modificare o cancellare il medesimo record comtemporaneamente. Ne verrebbe fuori un problema non indifferente, con dati sovrapposti e la conseguente perdita in termini di affidabilità.

Le transazioni possono essere di due tipi principali:

Transazioni di Lettura (Read-Only): queste sono utilizzate quando si effettuano solo operazioni di lettura sui dati, ad esempio quando si eseguono query o ricerche; sono meno restrittive e possono essere operate contemporaneamente con altre transazioni dello stesso tipo.

Transazioni di Scrittura (Read-Write): queste transazioni vengono utilizzate quando si desidera apportare modifiche ai dati, ad esempio aggiungerli, modificarli o eliminarli. Sono esclusive e bloccano l’accesso concorrente a un oggetto o a un object store durante la loro esecuzione.

Ecco un esempio di come potresti utilizzare le transazioni in IndexedDB per aggiungere un nuovo libro al database:

const request = indexedDB.open("BookshopDB", 1);
request.onsuccess = (event) => {
const db = event.target.result;
// Start a new transaction
const transaction = db.transaction(["Books"], "readwrite");
const bookStore = transaction.objectStore("Books");
// Add a new book to the object store
const newBook = {
isbn: "54321",
title: "Pride and Prejudice",
genre: "Novels",
};
bookStore.add(newBook);
transaction.oncomplete = () => {
console.log("Book added!");
db.close();
};
};

In questo esempio, viene aperta una transazione di scrittura per aggiungere un nuovo libro all’object store “Books”. La transazione garantisce che l’operazione di aggiunta venga completata prima di chiudere la connessione con il database.

Vantaggi e Svantaggi

Finora, abbiamo esplorato solo i vantaggi di IndexedDB, ma sarebbe un errore considerarlo un rimedio universale a tutti i problemi.

Come ogni tecnologia, anche IndexedDB ha i suoi limiti, ed è fondamentale tenerli a mente quando si valuta se può essere la scelta giusta per il proprio specifico caso d’uso:

  • uno dei principali punti a sfavore lavorando con IndexedDB è la curva di apprendimento, difatti le API non sono così immediate e richiedono una buona dose di tempo per essere approfondite. E’ importante sottolineare che non sono state implementate sfruttando promise o async/await, ormai lo standard nella programmazione JavaScript. Invece, IndexedDB utilizza callback e eventi, il che può rendere il codice più complesso da leggere e gestire. Oltre a ciò, gli sviluppatori devono dedicare del tempo per comprendere i concetti fondamentali come database, object store, indici e transazioni. Non si tratta di nulla di esageratamente complesso come abbiamo potuto vedere fino ad ora, ma è comunque da tenere in considerazione.
  • variazioni negli schemi e le migrazioni dei dati possono rappresentare una sfida non indifferente; ciò differisce dai DB “standard” dove esistono strategie consolidate e automatizzate per semplificare il processo di migrazione. IndexedDB richiede un approccio più manuale, questo si basa sul concetto di Versioni. Ogni volta che si apre un database è necessario specificare una versione; se questa differisce da quella già esistente, scatta l’evento onupgradeneeded. In questa fase verrà specificato come lo schema del database deve essere modificato per adattarsi alla nuova versione, dando la possibilità di aggiungere, rimuovere o modificare object store e indici. Proprio per il fatto di essere un’operazione di impronta manuale, la comprensione approfondita di come lavorare con le versioni è essenziale per evitare problemi di compatibilità e garantire una transizione indolore tra diverse versioni del database.
IndexedDB | Can I use… Support tables for HTML5, CSS3, etc
  • altro punto a sfavore di IndexedDB è il fatto che non sia pienamente supportato da tutti i browser, anche se la situazione è migliorata negli ultimi anni. La sua adozione è stata progressiva, e mentre la maggior parte dei browser moderni offre un supporto adeguato, potrebbero sorgere problemi di compatibilità in alcune situazioni o con versioni più datate dei browser. Inoltre, quando si tratta delle restrizioni legate ai browser, è essenziale considerare che gli utenti dell’applicazione, trattandosi di una tecnologia completamente basata sul client (senza considerare le possibilità di sync che vedremo dopo), potrebbero cancellare i dati memorizzati al suo interno, oppure semplicemente navigare sfruttando la modalità anonima. Questo aspetto rappresenta una considerazione cruciale nell’ambito dell’utilizzo di IndexedDB, poiché può impattare sul corretto funzionamento degli applicativi; per questo motivo è necessario prevedere meccanismi che possano inibire questi problemi, come ad esempio sistemi di failover o di sincronizzazione dei dati, al fine di garantire la continuità e la robustezza delle applicazioni anche in presenza di cancellazioni accidentali delle informazioni da parte degli utenti.

Librerie e Sync

Abbiamo parlato ampiamente di Indexed DB e di come interagire nativamente con esso sfruttando le API esposte, tuttavia, nell’ecosistema dello sviluppo web, è comune fare affidamento su librerie e framework per semplificare e ottimizzare il processo di sviluppo con IndexedDB.

Queste offrono spesso astrazioni che permettono una gestione semplificata delle transazioni e soluzioni preconfezionate per alcune delle sfide che abbiamo discusso.

Ecco alcune librerie popolari utilizzate con IndexedDB:

Dexie.js: questa libreria fornisce un’API più semplice e comprensibile per lavorare con IndexedDB. Dexie semplifica la gestione delle transazioni e offre un sistema di query più intuitivo per interrogare i dati.

PouchDB: PouchDB è una scelta popolare specialmente quando il focus è relativo alla sincronizzazione dei dati dalla sorgente al remoto. Questa libreria offre una sincronizzazione bidirezionale tra il database client e un database CouchDB o un altro database compatibile con CouchDB.

La scelta di utilizzare una libreria o di lavorare direttamente con le API native di IndexedDB dipenderà dalle esigenze del tuo progetto. Le librerie possono sicuramente velocizzare lo sviluppo e rendere più semplice l’interazione con esso. Tuttavia, è comunque fondamentale capire il suo funzionamento interno per poter risolvere e analizzare problemi specifici che potrebbero emergere nel suo utilizzo. Perciò, ricorda sempre che le astrazioni sono strumenti preziosi, ma non devono mai sostituire una solida comprensione dei fondamentali.

Per quanto riguarda i meccanismi di sincronizzazione, necessari per garantire continuità negli applicativi basati su indexedDB, è importante chiarire che sfortunatamente esso non ha meccanismi nativi per la condivisione dei dati tra dispositivi o tra il browser e un server remoto. Anche in questo caso l’utilizzo di librerie ci fornisce un aiuto essenziale, permettendoci di risparmiare una mole di tempo non indifferente rispetto all’implementazione di logiche personalizzate.

Ora sporchiamoci le mani sul codice, implementando la stessa operazione sfruttando le librerie discusse:

Dexie

// Create a new Dexie instance
const db = new Dexie("BookshopDB");

// define schema for the database
db.version(1).stores({
books: "++id, title, author",
});

// Add a book to the database
db.books
.add({ title: "Pride and Prejudice", author: "Jane Austen" })
.then(() => {
console.log("Book added!");
})
.catch((error) => {
console.error("Error adding book", error);
});

// Get all books from the database
db.books
.toArray()
.then((books) => {
console.log("Books in the Database:", books);
})
.catch((error) => {
console.error("Error reading books:", error);
});

Pouch DB

// Create a new PouchDB instance
const db = new PouchDB("bookshopDB");

// Add a document to the database
db.put({
_id: "1",
title: "Lord of the Rings",
author: "J.R.R. Tolkien",
})
.then(() => {
console.log("Document added!");
})
.catch((error) => {
console.error("Error adding Document", error);
});

// Retrieve a document from the database by its id
db.get("1")
.then((documento) => {
console.log("Document:", documento);
})
.catch((error) => {
console.error("Error getting Document:", error);
});

Nativo

// Create or open the database
const request = indexedDB.open("BookshopDB", 1);

// Manage event of database creation or upgrade
request.onupgradeneeded = (event) => {
const db = event.target.result;

// create an object store called "books" with index "author"
const bookStore = db.createObjectStore("books", { keyPath: "id" });
bookStore.createIndex("author", "author", { unique: false });

// Insert a book into the database
bookStore.add({
id: 1,
title: "Pride and Prejudice",
author: "Jane Austen",
});
};

// Manage event of database opening
request.onsuccess = (event) => {
const db = event.target.result;

// Start a transaction to read data from the database
const transaction = db.transaction(["books"], "readonly");
const bookStore = transaction.objectStore("books");

// Retrieve a book from the database by its id
const getRequest = bookStore.get(1);

getRequest.onsuccess = (event) => {
const book = event.target.result;
console.log("Book:", book);
};

transaction.oncomplete = () => {
db.close();
};
};

Come è possibile notare, l’implementazione nativa è più dettagliata e richiede una gestione più esplicita delle transazioni e degli eventi. Se si preferisce un approccio più diretto e l’obiettivo è ottimizzare il controllo sul database, l’implementazione nativa offre un’opzione solida. Tuttavia, per progetti più complessi o per ridurre la complessità dello sviluppo, l’utilizzo di librerie come Dexie.js o PouchDB può semplificare notevolmente il processo di interazione con IndexedDB, garantendo anche una semplicità non indifferente nella gestione della sincronia dei dati. La scelta perciò dipenderà dalle esigenze specifiche del progetto e dalle preferenze del team di sviluppo.

Spero di averti fornito informazioni utili su IndexedDB e sul suo potenziale in contesto web. Sicuramente è uno strumento non privo di complessità (ahimè come la maggior parte degli argomenti nel nostro settore), tuttavia può essere considerato un componente decisamente valido per l’archiviazione di dati sfruttando direttamente la potenza del browser, e garantendo una serie di vantaggi non indifferenti, specialmente in riferimento a situazioni in cui la connettività di rete può essere limitata o instabile.

Che tu scelga l’approccio nativo o no, ora hai una solida base da cui partire per sfruttare appieno questa tecnologia.

Grazie per aver letto l’articolo.

--

--