IndexedDB

Storage vs IndexedDB

Le Storage (sessionStorage, localStorage) permet d’enregistrer des données côté client sous forme de clé/valeur (chaînes de caractères). Tous les accès au storage sont synchrones et donc bloquants, il n’est donc adapté que pour de petites quantités de données. IndexedDB permet également d’enregistrer des données côté client mais pas de la même manière:

Exemple IndexedDB

IndexedDB utilise le principe “same-origin” (même origine), ce qui signifie qu’une base de donnée ne peut être consultée que par une seule origine: le domaine/sous-domaine avec laquelle elle a été créée.


Limites de stockage

L’espace de stockage maximal du navigateur est dynamique, il est basé sur la taille du disque dur.

Il existe deux limites

Si la limite de groupe est dépassée, ou si l’éviction d’origine ne crée pas assez d’espace libre, le navigateur lance QuotaExceededError.


1. Créer une base de données

La première étape consiste à créer la base de données si elle n’existe pas ou si on fait une montée de version.

window.indexedDB      = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction;
window.IDBKeyRange    = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange;

const VERSION = 1,
      DB_NAME = "myDb";

var openReq = window.indexedDB.open(DB_NAME, VERSION), db;

// Créer ou monter en version la base de données
/* Doc: https://www.w3.org/TR/IndexedDB-2/#introduction */
openReq.onupgradeneeded = function(e) {
  db = openReq.result;
  // Créer le schéma ici
};

// La BDD n'a pas pu être ouverte
openReq.onerror = function(e) {
  console.error("Error loading database");
};

// La BDD est ouverte
openReq.onsuccess = function(e) {
  db = openReq.result;
  // Accéder à la BDD
}

Montée en version

Lors d’une montée en version, si un autre onglet est chargé avec la base de données (avec l’ancienne version), alors l’événement onblocked est levé. L’utilisateur doit fermer tous les autres onglets ouverts sur le site pour que que onupgradeneeded soit appelé. Pour les autres onglets, l’événément onversionchange est déclenché sur l’instance.

openReq.onblocked = function(event) {
  // On veut mettre à jour la BDD mais un autre onglet est chargé avec l'ancienne version
  alert("Veuillez fermer tous les ongles ouverts sur ce site!");
};
  
openReq.onupgradeneeded = function(event) {
  // Toutes les autres bases de données ont été fermées. Mettre à jour
}  
  
openReq.onsuccess = function(event) {
  db = event.target.result;

  // Un autre onglet essaie de mettre à jour la BDD
  db.onversionchange = function(event) {
    db.close();
    alert("A new version of this page is ready. Please reload!");
  }

  // Accéder à la BDD
}

2. Créer le schéma

Les tables de la base de données doivent être crées lorsque cette dernière est installée (onupgradeneeded).
Au moment de créer une table (un objectStore), on choisit le champ à utiliser comme clé primaire et on peut ajouter des index pour effectuer des recherches/tris de la table. Pour le reste, les colonnes n’ont pas à être définies, on peut ajouter les champs qu’on veut au moment de l’insertion.

3. Accéder à la base de donnée

Lorsque la base de donnée existe, soit parce qu’on vient de la créer, soit parce qu’elle existait déjà, le callback de openReq.onsuccess est appelé et on peut accéder à son contenu: créer, modifier, supprimer ou accéder au données.

Ajouter des entrées

Si l’on essaie de créer une entrée avec un id qui existe déjà, une erreur est levée (Key already exists in the object store.).

Récupérer le contenu d’une table

// Récupérer l'objectStore en lecture
var objectStore = db.transaction(["myTableName"], "readonly").objectStore("myTableName");
var n = 0;

// Boucler sur le contenu de l'objecStore via un curseur
objectStore.openCursor().onsuccess = function(event) {
  var it = event.target.result;

  // Fin de la liste
  if(!it) {
    console.log(n + ' entries');
    return;
  }
  n++;

  // Entrée en cours
  var item = it.value;
  console.log(item);

  // Passer à l'itération suivante
  it.continue();
}

Pour trier le résultat, voir la section Rechercher plusieurs entrées.

Récupérer une entrée via son id

On peut récupérer une entrée via son id.
Si l’id spécifié n’existe pas, la méthode onsuccess est appelée mais request.result est undefined.

var id = 2;

// Récupérer l'objectStore en lecture (mode par défaut)
var objectStore = db.transaction(["myTableName"]).objectStore("myTableName");

// Récupérer l'entrée avec l'id donné
objectStore.get(id).onsuccess = function(e) {
  var item = request.result;

  console.log(item);
};

Mettre à jour une entrée

put met à jour une entrée si l’id existe déjà en base de donnée ou en crée une nouvelle.

var id = 2;

// Récupérer l'objectStore en lecture/écriture
var objectStore = db.transaction(["myTableName"], "readwrite").objectStore("myTableName");

// Récupérer l'entrée en base de donnée ou créer un objet de toute pièce
var item = {
    id: id,
    title: "Hello You"
};
objectStore.put(item).onsuccess = function(e) {
  console.log("ok");
}

Supprimer une entrée

// Récupérer l'objectStore en lecture/écriture
var objectStore = db.transaction(["myTableName"], "readwrite").objectStore("myTableName");

// Supprimer une entrée
objectStore.delete(2).onsuccess = function(e) {
  console.log("ok");
};

Il est important de toujours laisser la base de donnée dans un état cohérent. Par exemple, si vous effacez des entrées dans une transaction et en ajoutez dans une autre transaction, il y a un risque que l’utilisateur ferme son navigateur entre les deux et se retrouve avec une base de données vide. Pour éviter cela, utiliser une seule transaction - laquelle sera entièrement annulée si le navigateur est fermé.

Rechercher une entrée

Il est possible de rechercher une ou des entrée(s) en utilisant un index.
Le résultat sera undefined si aucune entrée ne correspond.

// Récupérer l'index "title" de l'objectStore en lecture
var index = db.transaction(["myTableName"])
            .objectStore("myTableName")
            .index("title");

// Rechercher une entrée
index.get("Hello World").onsuccess = function(e) {
  var item = e.target.result;
  console.log(item);
}

Rechercher plusieurs entrées

Pour rechercher plusieurs entrées qui correspondent à un critère et non plus une, plutôt que d’appeler get sur l’index, on appelle openCursor. Les entrées retournées sont triées par ordre croissant sur l’index.

// Récupérer l'index "title" de l'objectStore en lecture
var index = db.transaction(["myTableName"])
            .objectStore("myTableName")
            .index("title");

// Récupérer tous les résultats de l'index
index.openCursor().onsuccess = function(e) {
  var cursor = e.target.result;
  if(!cursor) {
    return;
  }

  // Entrée en cours
  var title = cursor.key,
      id    = cursor.primaryKey,
      item  = cursor.value;
  console.log(id, title, item);

  // Continuer l'itération
  cursor.continue();
};

L’exemple ci-dessus retourne toutes les entrées de la table. Pour limiter les entrées retournées on passe un intervalle à openCursor (méthodes pour créer un intervalle plus bas)

// Toutes les entrées dont le title vaut "Hello World"
var range = IDBKeyRange.only("Hello World");

index.openCursor(range).onsuccess = function(e) {
  // ...
}

Par défaut, le résultat est retourné par ordre croissant. Pour itérer en mode décroissant, passer la valeur "prev" en 2ème argument (vaut "next" par défaut).

index.openCursor(range, "prev").onsuccess = function(e) {
  // ...
}

Pour récupérer la liste des entrées sans doublons (donc en excluant les entrées qui ont le même index qu’une autre), utiliser "nextunique" (ou "prevunique" pour l’ordre décroissant)

index.openCursor(range, "nextunique").onsuccess = function(e) {
  // ...
}

Intervalle (IDBKeyRange)

Un intervalle de clé peut être une seule valeur ou un intervalle avec des bornes inférieure et supérieure.
Les bornes peuvent être inclues ou exclues.
Pour créer un intervalle, plusieurs méthodes sont disponibles:

Il est possible de tester si une clé est contenue dans un intervalle de clé ou non avec la méthode includes:

console.log(range.includes('Charlie'));

Tri et localisation

Par défaut, IndexedDB ne prend pas en charge l’internationalisation des chaînes de tri, et est trié comme s’il s’agit d’un texte anglais. Par exemple, a, à, b est trié a, b, à. Pour qu’un index soit trié selon une locale spécifique, il est nécessaire de préciser à la création du schéma la locale à utiliser.

store.createIndex("title", "title", { locale: "fr" });