Utiliser IndexedDB

Ce guide couvre les bases de l'API IndexedDB. Nous utilisons la bibliothèque IndexedDB Promised de Jake Archibal, qui est très semblable à l'API IndexedDB, mais qui utilise des promesses, que vous pouvez await pour une syntaxe plus concise. Cela simplifie l'API tout en conservant sa structure.

Qu'est-ce que IndexedDB ?

IndexedDB est un système de stockage NoSQL à grande échelle qui permet de stocker quasiment tous les éléments du navigateur de l'utilisateur. En plus des actions habituelles de recherche, d'obtention et de déplacement, IndexedDB prend également en charge les transactions et convient parfaitement au stockage de grandes quantités de données structurées.

Chaque base de données IndexedDB est unique à une origine (généralement le domaine ou le sous-domaine du site), ce qui signifie qu'elle ne peut pas être consultée par une autre origine. Ses limites de stockage des données sont généralement élevées, voire inexistantes, mais différents navigateurs gèrent les limites et l'éviction des données différemment. Pour en savoir plus, consultez la section Complément d'informations.

Termes des bases de données indexées

Database (Base de données)
Niveau le plus élevé d'IndexedDB. Elle contient les magasins d'objets, qui à leur tour contiennent les données que vous souhaitez conserver. Vous pouvez créer plusieurs bases de données avec les noms de votre choix.
Magasin d'objets
Bucket individuel pour stocker des données, semblable aux tables dans les bases de données relationnelles. En règle générale, il existe un magasin d'objets pour chaque type (et non le type de données JavaScript) que vous stockez. Contrairement aux tables de base de données, les types de données JavaScript d'un magasin n'ont pas besoin d'être cohérents. Par exemple, si une application possède un magasin d'objets people contenant des informations sur trois personnes, les propriétés de l'âge de ces personnes peuvent être 53, 'twenty-five' et unknown.
Index
Type de magasin d'objets permettant d'organiser les données dans un autre magasin d'objets (appelé magasin d'objets de référence) en fonction d'une propriété individuelle des données. L'index permet de récupérer les enregistrements du magasin d'objets par cette propriété. Par exemple, si vous stockez des personnes, vous voudrez peut-être les récupérer plus tard en fonction de leur nom, de leur âge ou de leur animal préféré.
Opération
Interaction avec la base de données.
Transaction
Wrapper autour d'une opération ou d'un groupe d'opérations qui garantit l'intégrité de la base de données. Si l'une des actions d'une transaction échoue, aucune d'entre elles n'est appliquée et la base de données revient à l'état dans lequel elle se trouvait avant le début de la transaction. Toutes les opérations de lecture ou d'écriture dans IndexedDB doivent faire partie d'une transaction. Cela permet des opérations atomiques de lecture-modification-écriture sans risque de conflits avec d'autres threads agissant simultanément sur la base de données.
Cursor
Mécanisme permettant d'effectuer des itérations sur plusieurs enregistrements d'une base de données.

Vérifier la compatibilité avec IndexedDB

IndexedDB est presque compatible de manière universelle. Toutefois, si vous utilisez des navigateurs plus anciens, il n'est pas conseillé de prendre en charge la détection de fonctionnalités au cas où. Le moyen le plus simple consiste à vérifier l'objet window:

function indexedDBStuff () {
  // Check for IndexedDB support:
  if (!('indexedDB' in window)) {
    // Can't use IndexedDB
    console.log("This browser doesn't support IndexedDB");
    return;
  } else {
    // Do IndexedDB stuff here:
    // ...
  }
}

// Run IndexedDB code:
indexedDBStuff();

Ouvrir une base de données

Avec IndexedDB, vous pouvez créer plusieurs bases de données avec le nom de votre choix. Si une base de données n'existe pas lorsque vous essayez de l'ouvrir, elle est automatiquement créée. Pour ouvrir une base de données, utilisez la méthode openDB() de la bibliothèque idb:

import {openDB} from 'idb';

async function useDB () {
  // Returns a promise, which makes `idb` usable with async-await.
  const dbPromise = await openDB('example-database', version, events);
}

useDB();

Cette méthode renvoie une promesse qui se résout en un objet de base de données. Lorsque vous utilisez la méthode openDB(), indiquez un nom, un numéro de version et un objet d'événements pour configurer la base de données.

Voici un exemple de méthode openDB() en contexte:

import {openDB} from 'idb';

async function useDB () {
  // Opens the first version of the 'test-db1' database.
  // If the database does not exist, it will be created.
  const dbPromise = await openDB('test-db1', 1);
}

useDB();

Vérifiez la compatibilité avec IndexedDB en haut de la fonction anonyme. Cette action met fin à la fonction si le navigateur n'est pas compatible avec IndexedDB. Si la fonction peut continuer, elle appelle la méthode openDB() pour ouvrir une base de données nommée 'test-db1'. Dans cet exemple, l'objet "event" facultatif a été omis pour simplifier les choses, mais vous devez le spécifier pour effectuer toute tâche pertinente avec IndexedDB.

Utiliser des magasins d'objets

Une base de données IndexedDB contient un ou plusieurs magasins d'objets, qui comportent chacun une colonne pour une clé et une autre pour les données associées à cette clé.

Créer des magasins d'objets

Une base de données IndexedDB bien structurée doit disposer d'un magasin d'objets pour chaque type de données devant être conservées. Par exemple, un site qui conserve des profils utilisateur et des notes peut comporter un magasin d'objets people contenant des objets person et un magasin d'objets notes contenant des objets note.

Pour garantir l'intégrité de la base de données, vous ne pouvez créer ou supprimer des magasins d'objets dans l'objet "events" que dans un appel openDB(). L'objet "Événements" expose une méthode upgrade() qui vous permet de créer des magasins d'objets. Appelez la méthode createObjectStore() dans la méthode upgrade() pour créer le magasin d'objets:

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('example-database', 1, {
    upgrade (db) {
      // Creates an object store:
      db.createObjectStore('storeName', options);
    }
  });
}

createStoreInDB();

Cette méthode utilise le nom du magasin d'objets et un objet de configuration facultatif qui vous permet de définir diverses propriétés pour le magasin d'objets.

Voici un exemple d'utilisation de createObjectStore():

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db1', 1, {
    upgrade (db) {
      console.log('Creating a new object store...');

      // Checks if the object store exists:
      if (!db.objectStoreNames.contains('people')) {
        // If the object store does not exist, create it:
        db.createObjectStore('people');
      }
    }
  });
}

createStoreInDB();

Dans cet exemple, un objet "Événements" est transmis à la méthode openDB() pour créer le magasin d'objets. Comme précédemment, le travail de création du magasin d'objets est effectué dans la méthode upgrade() de l'objet événement. Toutefois, comme le navigateur génère une erreur si vous essayez de créer un magasin d'objets existant, nous vous recommandons d'encapsuler la méthode createObjectStore() dans une instruction if qui vérifie si le magasin d'objets existe. Dans le bloc if, appelez createObjectStore() pour créer un magasin d'objets nommé 'firstOS'.

Définir des clés primaires

Lorsque vous définissez des magasins d'objets, vous pouvez définir la manière dont les données sont identifiées de manière unique dans le magasin à l'aide d'une clé primaire. Vous pouvez définir une clé primaire en définissant un chemin d'accès de clé ou à l'aide d'un générateur de clés.

Un chemin d'accès de clé est une propriété qui existe toujours et qui contient une valeur unique. Par exemple, dans le cas d'un magasin d'objets people, vous pouvez choisir l'adresse e-mail comme chemin d'accès de la clé:

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('people')) {
        db.createObjectStore('people', { keyPath: 'email' });
      }
    }
  });
}

createStoreInDB();

Cet exemple crée un magasin d'objets appelé 'people' et attribue la propriété email comme clé primaire dans l'option keyPath.

Vous pouvez également utiliser un générateur de clés tel que autoIncrement. Le générateur de clés crée une valeur unique pour chaque objet ajouté au magasin d'objets. Par défaut, si vous ne spécifiez pas de clé, IndexedDB crée une clé et la stocke séparément des données.

L'exemple suivant crée un magasin d'objets appelé 'notes' et définit l'attribution automatique de la clé primaire en tant que numéro incrémentiel:

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('notes')) {
        db.createObjectStore('notes', { autoIncrement: true });
      }
    }
  });
}

createStoreInDB();

L'exemple suivant est semblable à l'exemple précédent, mais cette fois, la valeur d'incrémentation automatique est explicitement attribuée à une propriété nommée 'id'.

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('logs')) {
        db.createObjectStore('logs', { keyPath: 'id', autoIncrement: true });
      }
    }
  });
}

createStoreInDB();

Le choix de la méthode à utiliser pour définir la clé dépend de vos données. Si vos données possèdent une propriété toujours unique, vous pouvez la définir comme keyPath pour appliquer cette unicité. Sinon, utilisez une valeur d'incrémentation automatique.

Le code suivant crée trois magasins d'objets illustrant les différentes manières de définir des clés primaires dans ces magasins:

import {openDB} from 'idb';

async function createStoresInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('people')) {
        db.createObjectStore('people', { keyPath: 'email' });
      }

      if (!db.objectStoreNames.contains('notes')) {
        db.createObjectStore('notes', { autoIncrement: true });
      }

      if (!db.objectStoreNames.contains('logs')) {
        db.createObjectStore('logs', { keyPath: 'id', autoIncrement: true });
      }
    }
  });
}

createStoresInDB();

Définir des index

Les index sont une sorte de magasin d'objets utilisé pour récupérer des données du magasin d'objets de référence par une propriété spécifiée. Un index se trouve dans le magasin d'objets de référence et contient les mêmes données, mais utilise la propriété spécifiée comme chemin de clé au lieu de la clé primaire du magasin de référence. Les index doivent être créés lorsque vous créez vos magasins d'objets et peuvent être utilisés pour définir une contrainte unique sur vos données.

Pour créer un index, appelez la méthode createIndex() sur une instance de magasin d'objets:

import {openDB} from 'idb';

async function createIndexInStore() {
  const dbPromise = await openDB('storeName', 1, {
    upgrade (db) {
      const objectStore = db.createObjectStore('storeName');

      objectStore.createIndex('indexName', 'property', options);
    }
  });
}

createIndexInStore();

Cette méthode crée et renvoie un objet d'index. La méthode createIndex() de l'instance du magasin d'objets utilise le nom du nouvel index comme premier argument, et le deuxième argument fait référence à la propriété des données que vous souhaitez indexer. Le dernier argument vous permet de définir deux options qui déterminent le fonctionnement de l'index: unique et multiEntry. Si unique est défini sur true, l'index n'autorise pas les valeurs en double pour une seule clé. Ensuite, multiEntry détermine le comportement de createIndex() lorsque la propriété indexée est un tableau. S'il est défini sur true, createIndex() ajoute une entrée dans l'index pour chaque élément du tableau. Sinon, elle ajoute une seule entrée contenant le tableau.

Exemple :

import {openDB} from 'idb';

async function createIndexesInStores () {
  const dbPromise = await openDB('test-db3', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('people')) {
        const peopleObjectStore = db.createObjectStore('people', { keyPath: 'email' });

        peopleObjectStore.createIndex('gender', 'gender', { unique: false });
        peopleObjectStore.createIndex('ssn', 'ssn', { unique: true });
      }

      if (!db.objectStoreNames.contains('notes')) {
        const notesObjectStore = db.createObjectStore('notes', { autoIncrement: true });

        notesObjectStore.createIndex('title', 'title', { unique: false });
      }

      if (!db.objectStoreNames.contains('logs')) {
        const logsObjectStore = db.createObjectStore('logs', { keyPath: 'id', autoIncrement: true });
      }
    }
  });
}

createIndexesInStores();

Dans cet exemple, les magasins d'objets 'people' et 'notes' possèdent des index. Pour créer les index, commencez par attribuer le résultat de createObjectStore() (un objet de magasin d'objets) à une variable afin de pouvoir appeler createIndex() au niveau de celle-ci.

Comment utiliser les données

Cette section explique comment créer, lire, mettre à jour et supprimer des données. Ces opérations sont toutes asynchrones et utilisent des promesses où l'API IndexedDB utilise des requêtes. Cela simplifie l'API. Au lieu d'écouter les événements déclenchés par la requête, vous pouvez appeler .then() sur l'objet de base de données renvoyé par la méthode openDB() pour démarrer les interactions avec la base de données, ou await sa création.

Toutes les opérations de données dans IndexedDB sont effectuées au sein d'une transaction. Chaque opération se présente sous la forme suivante:

  1. Obtenir un objet de base de données.
  2. Ouvrir la transaction sur la base de données.
  3. Ouvrir le magasin d'objets lors de la transaction.
  4. Effectuer une opération sur le stockage d'objets

Une transaction peut être considérée comme un wrapper sécurisé pour une opération ou un groupe d'opérations. Si l'une des actions d'une transaction échoue, un rollback est effectué pour toutes les actions. Les transactions sont spécifiques à un ou plusieurs magasins d'objets, que vous définissez lorsque vous ouvrez la transaction. Ils peuvent être en lecture seule ou en lecture/écriture. Cela indique si les opérations à l'intérieur de la transaction lisent les données ou modifient la base de données.

Créer des données

Pour créer des données, appelez la méthode add() sur l'instance de base de données et transmettez les données que vous souhaitez ajouter. Le premier argument de la méthode add() est le magasin d'objets auquel vous souhaitez ajouter les données. Le deuxième argument est un objet contenant les champs et les données associées que vous souhaitez ajouter. Voici l'exemple le plus simple, dans lequel une seule ligne de données est ajoutée:

import {openDB} from 'idb';

async function addItemToStore () {
  const db = await openDB('example-database', 1);

  await db.add('storeName', {
    field: 'data'
  });
}

addItemToStore();

Chaque appel add() se produit dans une transaction. Par conséquent, même si la promesse est résolue, cela ne signifie pas nécessairement que l'opération a fonctionné. Pour vous assurer que l'opération d'ajout a bien été effectuée, vous devez vérifier si l'intégralité de la transaction est terminée à l'aide de la méthode transaction.done(). Il s'agit d'une promesse qui se résout lorsque la transaction se termine elle-même et qui est rejetée si la transaction a généré des erreurs. Vous devez effectuer cette vérification pour toutes les opérations d'écriture, car c'est votre seul moyen de savoir que les modifications apportées à la base de données ont effectivement eu lieu.

Le code suivant montre comment utiliser la méthode add() dans une transaction:

import {openDB} from 'idb';

async function addItemsToStore () {
  const db = await openDB('test-db4', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('foods')) {
        db.createObjectStore('foods', { keyPath: 'name' });
      }
    }
  });
  
  // Create a transaction on the 'foods' store in read/write mode:
  const tx = db.transaction('foods', 'readwrite');

  // Add multiple items to the 'foods' store in a single transaction:
  await Promise.all([
    tx.store.add({
      name: 'Sandwich',
      price: 4.99,
      description: 'A very tasty sandwich!',
      created: new Date().getTime(),
    }),
    tx.store.add({
      name: 'Eggs',
      price: 2.99,
      description: 'Some nice eggs you can cook up!',
      created: new Date().getTime(),
    }),
    tx.done
  ]);
}

addItemsToStore();

Une fois que vous avez ouvert la base de données (et créé un magasin d'objets si nécessaire), vous devez ouvrir une transaction en appelant la méthode transaction(). Cette méthode utilise un argument pour le magasin sur lequel vous souhaitez effectuer la transaction, ainsi que le mode. Dans ce cas, nous souhaitons écrire dans le magasin. Cet exemple spécifie donc 'readwrite'.

L'étape suivante consiste à commencer à ajouter des articles à la boutique dans le cadre de la transaction. Dans l'exemple précédent, nous traitons trois opérations sur le magasin 'foods' qui renvoient chacune une promesse:

  1. Ajout d'un enregistrement pour un sandwich savoureux.
  2. Ajout d'un enregistrement pour des œufs.
  3. Signalant que la transaction est terminée (tx.done)

Toutes ces actions étant basées sur des promesses, nous devons attendre qu'elles se terminent toutes. Transmettre ces promesses à Promise.all est un moyen pratique et ergonomique d'effectuer cette opération. Promise.all accepte un tableau de promesses et se termine lorsque toutes les promesses qui lui sont transmises sont résolues.

Pour les deux enregistrements ajoutés, l'interface store de l'instance de transaction appelle add() et lui transmet les données. Vous pouvez appliquer (await) l'appel Promise.all pour qu'il se termine une fois la transaction terminée.

Lire des données

Pour lire des données, appelez la méthode get() sur l'instance de base de données que vous récupérez à l'aide de la méthode openDB(). get() prend le nom du magasin et la valeur de clé primaire de l'objet que vous souhaitez récupérer. Voici un exemple de base:

import {openDB} from 'idb';

async function getItemFromStore () {
  const db = await openDB('example-database', 1);

  // Get a value from the object store by its primary key value:
  const value = await db.get('storeName', 'unique-primary-key-value');
}

getItemFromStore();

Comme avec add(), la méthode get() renvoie une promesse. Vous pouvez donc l'await si vous le souhaitez, ou utiliser le rappel .then() de la promesse.

L'exemple suivant utilise la méthode get() sur le magasin d'objets 'foods' de la base de données 'test-db4' pour obtenir une seule ligne à l'aide de la clé primaire 'name':

import {openDB} from 'idb';

async function getItemFromStore () {
  const db = await openDB('test-db4', 1);
  const value = await db.get('foods', 'Sandwich');

  console.dir(value);
}

getItemFromStore();

La récupération d'une seule ligne de la base de données est assez simple: ouvrez la base de données et spécifiez le magasin d'objets et la valeur de clé primaire de la ligne à partir de laquelle vous souhaitez obtenir des données. Étant donné que la méthode get() renvoie une promesse, vous pouvez l'exécuter (await).

Mettre à jour des données

Pour mettre à jour des données, appelez la méthode put() sur le magasin d'objets. La méthode put() est semblable à la méthode add() et peut également être utilisée à la place de add() pour créer des données. Voici un exemple de base d'utilisation de put() pour mettre à jour une ligne d'un magasin d'objets en fonction de sa valeur de clé primaire:

import {openDB} from 'idb';

async function updateItemInStore () {
  const db = await openDB('example-database', 1);

  // Update a value from in an object store with an inline key:
  await db.put('storeName', { inlineKeyName: 'newValue' });

  // Update a value from in an object store with an out-of-line key.
  // In this case, the out-of-line key value is 1, which is the
  // auto-incremented value.
  await db.put('otherStoreName', { field: 'value' }, 1);
}

updateItemInStore();

Comme d'autres méthodes, cette méthode renvoie une promesse. Vous pouvez également utiliser put() dans le cadre d'une transaction. Voici un exemple utilisant le magasin 'foods' précédent, qui met à jour le prix du sandwich et des œufs:

import {openDB} from 'idb';

async function updateItemsInStore () {
  const db = await openDB('test-db4', 1);
  
  // Create a transaction on the 'foods' store in read/write mode:
  const tx = db.transaction('foods', 'readwrite');

  // Update multiple items in the 'foods' store in a single transaction:
  await Promise.all([
    tx.store.put({
      name: 'Sandwich',
      price: 5.99,
      description: 'A MORE tasty sandwich!',
      updated: new Date().getTime() // This creates a new field
    }),
    tx.store.put({
      name: 'Eggs',
      price: 3.99,
      description: 'Some even NICER eggs you can cook up!',
      updated: new Date().getTime() // This creates a new field
    }),
    tx.done
  ]);
}

updateItemsInStore();

La manière dont les éléments sont mis à jour dépend de la façon dont vous définissez une clé. Si vous définissez un keyPath, chaque ligne du magasin d'objets est associée à une clé intégrée. L'exemple précédent met à jour des lignes en fonction de cette clé. Lorsque vous modifiez des lignes dans cette situation, vous devez spécifier cette clé pour mettre à jour l'élément approprié dans le magasin d'objets. Vous pouvez également créer une clé hors ligne en définissant une autoIncrement comme clé primaire.

Supprimer des données

Pour supprimer des données, appelez la méthode delete() sur le magasin d'objets:

import {openDB} from 'idb';

async function deleteItemFromStore () {
  const db = await openDB('example-database', 1);

  // Delete a value 
  await db.delete('storeName', 'primary-key-value');
}

deleteItemFromStore();

Comme pour add() et put(), vous pouvez utiliser ceci dans le cadre d'une transaction:

import {openDB} from 'idb';

async function deleteItemsFromStore () {
  const db = await openDB('test-db4', 1);
  
  // Create a transaction on the 'foods' store in read/write mode:
  const tx = db.transaction('foods', 'readwrite');

  // Delete multiple items from the 'foods' store in a single transaction:
  await Promise.all([
    tx.store.delete('Sandwich'),
    tx.store.delete('Eggs'),
    tx.done
  ]);
}

deleteItemsFromStore();

La structure de l'interaction avec la base de données est la même que pour les autres opérations. N'oubliez pas de vérifier que l'ensemble de la transaction est terminé en incluant la méthode tx.done dans le tableau que vous transmettez à Promise.all.

Obtenir toutes les données

Jusqu'à présent, vous n'avez récupéré les objets qu'un par un dans le magasin. Vous pouvez également récupérer toutes les données, ou un sous-ensemble, d'un magasin d'objets ou d'un index à l'aide de la méthode getAll() ou de curseurs.

La méthode getAll()

Le moyen le plus simple de récupérer toutes les données d'un magasin d'objets consiste à appeler getAll() sur le magasin ou l'index d'objets, comme ceci:

import {openDB} from 'idb';

async function getAllItemsFromStore () {
  const db = await openDB('test-db4', 1);

  // Get all values from the designated object store:
  const allValues = await db.getAll('storeName');

  console.dir(allValues);
}

getAllItemsFromStore();

Cette méthode renvoie tous les objets du magasin d'objets, sans aucune contrainte. C'est le moyen le plus direct d'obtenir toutes les valeurs d'un magasin d'objets, mais aussi le moins flexible.

import {openDB} from 'idb';

async function getAllItemsFromStore () {
  const db = await openDB('test-db4', 1);

  // Get all values from the designated object store:
  const allValues = await db.getAll('foods');

  console.dir(allValues);
}

getAllItemsFromStore();

Cet exemple appelle getAll() sur le magasin d'objets 'foods'. Cette opération renvoie tous les objets de 'foods', triés en fonction de la clé primaire.

Utiliser des curseurs

Les curseurs constituent un moyen plus flexible de récupérer plusieurs objets. Un curseur sélectionne chaque objet d'un magasin d'objets ou d'un index un par un, ce qui vous permet d'utiliser les données une fois qu'elles sont sélectionnées. Comme les autres opérations de base de données, les curseurs fonctionnent dans les transactions.

Pour créer un curseur, appelez openCursor() sur le magasin d'objets dans le cadre d'une transaction. En utilisant le magasin 'foods' des exemples précédents, voici comment faire avancer un curseur dans toutes les lignes de données d'un magasin d'objets:

import {openDB} from 'idb';

async function getAllItemsFromStoreWithCursor () {
  const db = await openDB('test-db4', 1);
  const tx = await db.transaction('foods', 'readonly');

  // Open a cursor on the designated object store:
  let cursor = await tx.store.openCursor();

  // Iterate on the cursor, row by row:
  while (cursor) {
    // Show the data in the row at the current cursor position:
    console.log(cursor.key, cursor.value);

    // Advance the cursor to the next row:
    cursor = await cursor.continue();
  }
}

getAllItemsFromStoreWithCursor();

Dans ce cas, la transaction est ouverte en mode 'readonly' et sa méthode openCursor est appelée. Dans une boucle while ultérieure, les propriétés key et value de la ligne située à la position actuelle du curseur peuvent être lues, et vous pouvez agir sur ces valeurs de la manière qui convient le mieux à votre application. Lorsque vous êtes prêt, vous pouvez appeler la méthode continue() de l'objet cursor pour passer à la ligne suivante. La boucle while se termine lorsque le curseur atteint la fin de l'ensemble de données.

Utiliser des curseurs avec des plages et des index

Les index vous permettent d'extraire les données d'un magasin d'objets en fonction d'une propriété autre que la clé primaire. Vous pouvez créer un index sur n'importe quelle propriété, qui devient le keyPath de l'index, spécifier une plage au niveau de cette propriété et obtenir les données de la plage à l'aide de getAll() ou d'un curseur.

Définissez votre plage à l'aide de l'objet IDBKeyRange et de l'une des méthodes suivantes:

Les méthodes upperBound() et lowerBound() spécifient les limites supérieure et inférieure de la plage.

IDBKeyRange.lowerBound(indexKey);

soit :

IDBKeyRange.upperBound(indexKey);

Elles utilisent chacune un argument: la valeur keyPath de l'index pour l'élément que vous souhaitez spécifier comme limite supérieure ou inférieure.

La méthode bound() spécifie à la fois une limite supérieure et une limite inférieure:

IDBKeyRange.bound(lowerIndexKey, upperIndexKey);

La plage de ces fonctions est inclusive par défaut, ce qui signifie qu'elle inclut les données spécifiées comme limites de la plage. Pour omettre ces valeurs, spécifiez la plage comme exclusive en transmettant true comme deuxième argument pour lowerBound() ou upperBound(), ou comme troisième et quatrième arguments de bound(), respectivement pour les limites inférieure et supérieure.

L'exemple suivant utilise un index sur la propriété 'price' du magasin d'objets 'foods'. Le magasin est désormais associé à un formulaire avec deux entrées pour les limites supérieure et inférieure de la plage. Utilisez le code suivant pour trouver les aliments dont les prix sont compris entre ces limites:

import {openDB} from 'idb';

async function searchItems (lower, upper) {
  if (!lower === '' && upper === '') {
    return;
  }

  let range;

  if (lower !== '' && upper !== '') {
    range = IDBKeyRange.bound(lower, upper);
  } else if (lower === '') {
    range = IDBKeyRange.upperBound(upper);
  } else {
    range = IDBKeyRange.lowerBound(lower);
  }

  const db = await openDB('test-db4', 1);
  const tx = await db.transaction('foods', 'readonly');
  const index = tx.store.index('price');

  // Open a cursor on the designated object store:
  let cursor = await index.openCursor(range);

  if (!cursor) {
    return;
  }

  // Iterate on the cursor, row by row:
  while (cursor) {
    // Show the data in the row at the current cursor position:
    console.log(cursor.key, cursor.value);

    // Advance the cursor to the next row:
    cursor = await cursor.continue();
  }
}

// Get items priced between one and four dollars:
searchItems(1.00, 4.00);

L'exemple de code commence par obtenir les valeurs des limites et vérifie si elles existent. Le bloc de code suivant détermine la méthode à utiliser pour limiter la plage en fonction des valeurs. Dans l'interaction avec la base de données, ouvrez le magasin d'objets sur la transaction comme d'habitude, puis ouvrez l'index 'price' sur le magasin d'objets. L'index 'price' vous permet de rechercher des articles par prix.

Le code ouvre ensuite un curseur sur l'index et transmet la plage. Le curseur renvoie une promesse représentant le premier objet de la plage, ou undefined si la plage ne comporte aucune donnée. La méthode cursor.continue() renvoie un curseur représentant l'objet suivant et poursuit la boucle jusqu'à ce que vous atteigniez la fin de la plage.

Gestion des versions des bases de données

Lorsque vous appelez la méthode openDB(), vous pouvez spécifier le numéro de version de la base de données dans le deuxième paramètre. Dans tous les exemples de ce guide, la version a été définie sur 1, mais une base de données peut être mise à niveau vers une nouvelle version si vous devez la modifier d'une manière ou d'une autre. Si la version spécifiée est ultérieure à la version de la base de données existante, le rappel upgrade de l'objet événement s'exécute, ce qui vous permet d'ajouter de nouveaux magasins d'objets et index à la base de données.

L'objet db dans le rappel upgrade possède une propriété oldVersion spéciale, qui indique le numéro de version de la base de données à laquelle le navigateur a accès. Vous pouvez transmettre ce numéro de version dans une instruction switch pour exécuter des blocs de code dans le rappel upgrade en fonction du numéro de version de la base de données existante. Exemple :

import {openDB} from 'idb';

const db = await openDB('example-database', 2, {
  upgrade (db, oldVersion) {
    switch (oldVersion) {
      case 0:
        // Create first object store:
        db.createObjectStore('store', { keyPath: 'name' });

      case 1:
        // Get the original object store, and create an index on it:
        const tx = await db.transaction('store', 'readwrite');
        tx.store.createIndex('name', 'name');
    }
  }
});

Dans cet exemple, la dernière version de la base de données est définie sur 2. Lorsque ce code s'exécute pour la première fois, la base de données n'existe pas encore dans le navigateur. oldVersion est donc 0, et l'instruction switch commence à case 0. Dans l'exemple, un magasin d'objets 'store' est ajouté à la base de données.

Important: Dans les instructions switch, il y a généralement un break après chaque bloc case, mais cela n'est délibérément pas utilisé ici. De cette manière, si la base de données existante a quelques versions de retard ou si elle n'existe pas, le code continue d'être exécuté sur le reste des blocs case jusqu'à ce qu'il soit à jour. Ainsi, dans l'exemple, le navigateur continue de s'exécuter via case 1, créant ainsi un index name sur le magasin d'objets store.

Pour créer un index 'description' sur le magasin d'objets 'store', mettez à jour le numéro de version et ajoutez un nouveau bloc case comme suit:

import {openDB} from 'idb';

const db = await openDB('example-database', 3, {
  upgrade (db, oldVersion) {
    switch (oldVersion) {
      case 0:
        // Create first object store:
        db.createObjectStore('store', { keyPath: 'name' });

      case 1:
        // Get the original object store, and create an index on it:
        const tx = await db.transaction('store', 'readwrite');
        tx.store.createIndex('name', 'name');

      case 2:
        const tx = await db.transaction('store', 'readwrite');
        tx.store.createIndex('description', 'description');
    }
  }
});

Si la base de données que vous avez créée dans l'exemple précédent existe toujours dans le navigateur, oldVersion est défini sur 2 lors de son exécution. Le navigateur ignore case 0 et case 1, et exécute le code dans case 2, ce qui crée un index description. Ensuite, le navigateur dispose d'une base de données à la version 3 contenant un magasin d'objets store avec les index name et description.

Complément d'informations

Les ressources suivantes fournissent plus d'informations et de contexte sur l'utilisation d'IndexedDB.

Documentation IndexedDB

Limites de stockage des données