IndexedDB JavaScript

IndexedDB è un database NoSQL integrato nei browser moderni che permette di memorizzare grandi quantità di dati strutturati lato client. A differenza di localStorage che è limitato a stringhe e dimensioni ridotte, IndexedDB può gestire oggetti JavaScript complessi, file, blob e supporta transazioni asincrone per performance elevate.
Quando Usare IndexedDB
IndexedDB è ideale per applicazioni che necessitano di memorizzare dati complessi offline, come applicazioni di produttività, giochi, editor grafici o qualsiasi app che deve funzionare senza connessione internet. È particolarmente utile quando hai bisogno di memorizzare più di 5-10MB di dati o quando devi eseguire query complesse sui dati.
// Confronto delle capacità di storage
// localStorage: ~5-10MB, solo stringhe
// sessionStorage: ~5-10MB, solo stringhe
// IndexedDB: centinaia di MB/GB, oggetti complessi, query avanzate
Concetti Fondamentali
IndexedDB organizza i dati in database che contengono object stores (simili alle tabelle) dove vengono memorizzati gli oggetti. Ogni operazione avviene all’interno di una transazione e tutte le operazioni sono asincrone.
Database e Object Stores
// Un database può contenere multiple object stores
// Ogni object store memorizza oggetti con una chiave primaria
const strutturaDati = {
database: "MiaApp",
version: 1,
stores: {
utenti: { keyPath: "id" },
prodotti: { keyPath: "codice" },
ordini: { keyPath: "numeroOrdine" },
},
};
Apertura e Creazione Database
L’apertura di un database IndexedDB è sempre asincrona e gestita tramite eventi:
function aprireDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open("MiaAppDB", 1);
// Evento per errori
request.onerror = function () {
reject("Errore nell'apertura del database");
};
// Evento per successo
request.onsuccess = function () {
const db = request.result;
resolve(db);
};
// Evento per upgrade/creazione
request.onupgradeneeded = function () {
const db = request.result;
// Crea object store per utenti
if (!db.objectStoreNames.contains("utenti")) {
const userStore = db.createObjectStore("utenti", { keyPath: "id" });
// Crea indici per ricerche veloci
userStore.createIndex("email", "email", { unique: true });
userStore.createIndex("nome", "nome", { unique: false });
}
// Crea object store per prodotti
if (!db.objectStoreNames.contains("prodotti")) {
const productStore = db.createObjectStore("prodotti", {
keyPath: "id",
});
productStore.createIndex("categoria", "categoria", { unique: false });
productStore.createIndex("prezzo", "prezzo", { unique: false });
}
};
});
}
L’evento onupgradeneeded si attiva quando il database viene creato per la prima volta o quando viene aggiornato a una versione superiore.
Operazioni CRUD
Aggiungere Dati
async function aggiungiUtente(utente) {
const db = await aprireDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction(["utenti"], "readwrite");
const store = transaction.objectStore("utenti");
const request = store.add(utente);
request.onsuccess = function () {
resolve("Utente aggiunto con successo");
};
request.onerror = function () {
reject("Errore nell'aggiunta dell'utente");
};
});
}
// Utilizzo
const nuovoUtente = {
id: 1,
nome: "Mario Rossi",
email: "mario@email.com",
età: 30,
};
aggiungiUtente(nuovoUtente);
Leggere Dati
async function ottieniUtente(id) {
const db = await aprireDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction(["utenti"], "readonly");
const store = transaction.objectStore("utenti");
const request = store.get(id);
request.onsuccess = function () {
if (request.result) {
resolve(request.result);
} else {
resolve(null); // Utente non trovato
}
};
request.onerror = function () {
reject("Errore nella lettura dell'utente");
};
});
}
// Ottenere tutti gli utenti
async function ottieniTuttiUtenti() {
const db = await aprireDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction(["utenti"], "readonly");
const store = transaction.objectStore("utenti");
const request = store.getAll();
request.onsuccess = function () {
resolve(request.result);
};
request.onerror = function () {
reject("Errore nella lettura degli utenti");
};
});
}
Aggiornare Dati
async function aggiornaUtente(utente) {
const db = await aprireDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction(["utenti"], "readwrite");
const store = transaction.objectStore("utenti");
const request = store.put(utente); // put aggiorna o inserisce
request.onsuccess = function () {
resolve("Utente aggiornato con successo");
};
request.onerror = function () {
reject("Errore nell'aggiornamento dell'utente");
};
});
}
Eliminare Dati
async function eliminaUtente(id) {
const db = await aprireDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction(["utenti"], "readwrite");
const store = transaction.objectStore("utenti");
const request = store.delete(id);
request.onsuccess = function () {
resolve("Utente eliminato con successo");
};
request.onerror = function () {
reject("Errore nell'eliminazione dell'utente");
};
});
}
Ricerca con Indici
Gli indici permettono di eseguire ricerche veloci su campi diversi dalla chiave primaria:
async function cercaUtentiPerNome(nome) {
const db = await aprireDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction(["utenti"], "readonly");
const store = transaction.objectStore("utenti");
const index = store.index("nome");
const request = index.getAll(nome);
request.onsuccess = function () {
resolve(request.result);
};
request.onerror = function () {
reject("Errore nella ricerca per nome");
};
});
}
// Ricerca con range
async function utentiInFasciaEta(minAge, maxAge) {
const db = await aprireDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction(["utenti"], "readonly");
const store = transaction.objectStore("utenti");
// Usa cursor per ricerca personalizzata
const request = store.openCursor();
const risultati = [];
request.onsuccess = function (event) {
const cursor = event.target.result;
if (cursor) {
const utente = cursor.value;
if (utente.età >= minAge && utente.età <= maxAge) {
risultati.push(utente);
}
cursor.continue();
} else {
resolve(risultati);
}
};
request.onerror = function () {
reject("Errore nella ricerca per età");
};
});
}
Wrapper Semplificato
Per semplificare l’uso quotidiano, puoi creare una classe wrapper:
class DBManager {
constructor(dbName, version = 1) {
this.dbName = dbName;
this.version = version;
this.db = null;
}
async init() {
this.db = await this.openDB();
return this;
}
openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
request.onupgradeneeded = (event) => {
const db = event.target.result;
this.setupStores(db);
};
});
}
setupStores(db) {
// Override in sottoclassi per configurare stores
}
async add(storeName, data) {
const transaction = this.db.transaction([storeName], "readwrite");
const store = transaction.objectStore(storeName);
return new Promise((resolve, reject) => {
const request = store.add(data);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async get(storeName, key) {
const transaction = this.db.transaction([storeName], "readonly");
const store = transaction.objectStore(storeName);
return new Promise((resolve, reject) => {
const request = store.get(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async getAll(storeName) {
const transaction = this.db.transaction([storeName], "readonly");
const store = transaction.objectStore(storeName);
return new Promise((resolve, reject) => {
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async update(storeName, data) {
const transaction = this.db.transaction([storeName], "readwrite");
const store = transaction.objectStore(storeName);
return new Promise((resolve, reject) => {
const request = store.put(data);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async delete(storeName, key) {
const transaction = this.db.transaction([storeName], "readwrite");
const store = transaction.objectStore(storeName);
return new Promise((resolve, reject) => {
const request = store.delete(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
}
// Utilizzo del wrapper
class AppDB extends DBManager {
setupStores(db) {
if (!db.objectStoreNames.contains("utenti")) {
const userStore = db.createObjectStore("utenti", { keyPath: "id" });
userStore.createIndex("email", "email", { unique: true });
}
}
}
// Uso semplificato
const appDB = new AppDB("MiaApp");
await appDB.init();
await appDB.add("utenti", { id: 1, nome: "Mario", email: "mario@test.com" });
const utente = await appDB.get("utenti", 1);
console.log(utente);
Considerazioni Importanti
Performance: IndexedDB è ottimizzato per grandi dataset ma le operazioni sono sempre asincrone.
Limiti di Storage: Dipendono dal browser e dallo spazio disponibile, generalmente molto superiori a localStorage.
Sicurezza: I dati sono accessibili solo dallo stesso origin (stesso dominio, protocollo e porta).
Persistenza: I dati rimangono fino a quando l’utente non li cancella manualmente o l’app non li rimuove.
Browser Support: Supportato in tutti i browser moderni, con alcune differenze nelle implementazioni specifiche.
IndexedDB rappresenta una soluzione potente per applicazioni web che necessitano di storage client-side robusto e performante, essenziale per creare esperienze offline ricche e responsive.
