IndexedDB JavaScript

Edoardo Midali
Edoardo Midali

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.