Mit IndexedDB arbeiten

In diesem Leitfaden werden die Grundlagen der IndexedDB API behandelt. Wir verwenden die Bibliothek IndexedDB Promised von Jake Archibald, die der IndexedDB API sehr ähnlich ist, aber Promise verwendet, für die Sie await für eine präzisere Syntax verwenden können. Dies vereinfacht die API und behält ihre Struktur bei.

Was ist IndexedDB?

IndexedDB ist ein umfangreiches NoSQL-Speichersystem, mit dem so ziemlich alles im Browser des Nutzers gespeichert werden kann. Neben den üblichen Such-, Get- und Put-Aktionen unterstützt IndexedDB auch Transaktionen und eignet sich gut zum Speichern großer Mengen strukturierter Daten.

Jede IndexedDB-Datenbank ist eindeutig einem Ursprung (in der Regel die Websitedomain oder Subdomain) zugeordnet, d. h., auf sie kann nicht auf einen anderen Ursprung zugegriffen werden. Die Limits für die Datenspeicherung sind in der Regel sehr hoch, sofern sie überhaupt vorhanden sind. Die Limits und die Datenbereinigung unterscheiden sich aber von Browsern. Weitere Informationen finden Sie im Abschnitt Weitere Informationen.

IndexedDB-Begriffe

Datenbank
Die höchste IndexedDB-Ebene. Sie enthält die Objektspeicher, die wiederum die Daten enthalten, die Sie beibehalten möchten. Sie können mehrere Datenbanken mit beliebigen Namen erstellen.
Objektspeicher
Ein einzelner Bucket zum Speichern von Daten, ähnlich wie Tabellen in relationalen Datenbanken. Normalerweise gibt es einen Objektspeicher für jeden Typ (nicht den JavaScript-Datentyp) der zu speichernden Daten. Im Gegensatz zu Datenbanktabellen müssen die JavaScript-Datentypen in einem Speicher nicht einheitlich sein. Wenn eine App beispielsweise einen people-Objektspeicher mit Informationen zu drei Personen hat, könnten deren Alterseigenschaften 53, 'twenty-five' und unknown sein.
Index
Eine Art von Objektspeicher zum Organisieren von Daten in einem anderen Objektspeicher (Referenzobjektspeicher genannt) nach einem einzelnen Attribut der Daten. Der Index wird zum Abrufen von Datensätzen im Objektspeicher durch dieses Attribut verwendet. Wenn Sie beispielsweise Personen speichern, können Sie diese später anhand ihres Namens, ihres Alters oder ihres Lieblingstiers abrufen.
Vorgang
Eine Interaktion mit der Datenbank.
Transaktion
Ein Wrapper um einen Vorgang oder eine Gruppe von Vorgängen, der die Datenbankintegrität gewährleistet. Wenn eine der Aktionen in einer Transaktion fehlschlägt, wird keine davon angewendet und die Datenbank kehrt in den Zustand vor Beginn der Transaktion zurück. Alle Lese- oder Schreibvorgänge in IndexedDB müssen Teil einer Transaktion sein. Dadurch sind atomare Read-Modify-Write-Vorgänge ohne das Risiko von Konflikten mit anderen Threads möglich, die gleichzeitig für die Datenbank arbeiten.
Cursor
Ein Mechanismus zur Iteration über mehrere Datensätze in einer Datenbank.

Unterstützung von IndexedDB prüfen

IndexedDB wird fast universal unterstützt. Falls Sie jedoch mit älteren Browsern arbeiten, ist es sicherheitshalber keine schlechte Idee, die Funktionserkennung bereitzustellen. Am einfachsten ist es, das window-Objekt zu prüfen:

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();

Datenbank öffnen

Mit IndexedDB können Sie mehrere Datenbanken mit beliebigen Namen erstellen. Wenn eine Datenbank beim Öffnen nicht vorhanden ist, wird sie automatisch erstellt. Verwenden Sie zum Öffnen einer Datenbank die Methode openDB() aus der Bibliothek 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();

Diese Methode gibt ein Promise zurück, das in ein Datenbankobjekt aufgelöst wird. Wenn Sie die Methode openDB() verwenden, geben Sie zum Einrichten der Datenbank einen Namen, eine Versionsnummer und ein Ereignisobjekt an.

Hier ein Beispiel für die openDB()-Methode im Kontext:

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();

Platzieren Sie die Prüfung auf IndexedDB-Unterstützung oben in der anonymen Funktion. Wenn der Browser IndexedDB nicht unterstützt, wird die Funktion beendet. Wenn die Funktion fortgesetzt werden kann, ruft sie die Methode openDB() auf, um eine Datenbank mit dem Namen 'test-db1' zu öffnen. In diesem Beispiel wurde das optionale Ereignisobjekt zur Vereinfachung ausgelassen. Es muss jedoch angegeben werden, um sinnvolle Arbeiten mit IndexedDB auszuführen.

Mit Objektspeichern arbeiten

Eine IndexedDB-Datenbank enthält einen oder mehrere Objektspeicher, die jeweils eine Spalte für einen Schlüssel und eine weitere Spalte für die mit diesem Schlüssel verknüpften Daten enthalten.

Objektspeicher erstellen

Eine gut strukturierte IndexedDB-Datenbank sollte für jeden Datentyp, der beibehalten werden muss, einen Objektspeicher haben. Eine Website, auf der Nutzerprofile und Notizen beibehalten werden, kann beispielsweise einen people-Objektspeicher mit person-Objekten und einen notes-Objektspeicher mit note-Objekten haben.

Zur Gewährleistung der Datenbankintegrität können Sie Objektspeicher nur in einem openDB()-Aufruf im Ereignisobjekt erstellen oder entfernen. Das Ereignisobjekt stellt eine upgrade()-Methode bereit, mit der Sie Objektspeicher erstellen können. Rufen Sie die Methode createObjectStore() in der Methode upgrade() auf, um den Objektspeicher zu erstellen:

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();

Diese Methode verwendet den Namen des Objektspeichers und ein optionales Konfigurationsobjekt, mit dem Sie verschiedene Attribute für den Objektspeicher definieren können.

Hier ein Beispiel für die Verwendung von 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();

In diesem Beispiel wird ein Ereignisobjekt an die Methode openDB() übergeben, um den Objektspeicher zu erstellen. Wie zuvor wird die Erstellung des Objektspeichers über die Methode upgrade() des Ereignisobjekts ausgeführt. Da der Browser jedoch einen Fehler ausgibt, wenn Sie versuchen, einen bereits vorhandenen Objektspeicher zu erstellen, empfehlen wir, die Methode createObjectStore() in eine if-Anweisung zu kapseln, die prüft, ob der Objektspeicher vorhanden ist. Rufen Sie im if-Block createObjectStore() auf, um einen Objektspeicher mit dem Namen 'firstOS' zu erstellen.

Primärschlüssel definieren

Wenn Sie Objektspeicher definieren, können Sie mithilfe eines Primärschlüssels definieren, wie Daten im Speicher eindeutig identifiziert werden. Sie können einen Primärschlüssel definieren, indem Sie entweder einen Schlüsselpfad definieren oder einen Schlüsselgenerator verwenden.

Ein Schlüsselpfad ist eine Eigenschaft, die immer vorhanden ist und einen eindeutigen Wert enthält. Im Fall eines people-Objektspeichers können Sie beispielsweise die E-Mail-Adresse als Schlüsselpfad auswählen:

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();

In diesem Beispiel wird ein Objektspeicher namens 'people' erstellt und das Attribut email als Primärschlüssel in der Option keyPath zugewiesen.

Sie können auch einen Schlüsselgenerator wie autoIncrement verwenden. Der Schlüsselgenerator erstellt einen eindeutigen Wert für jedes dem Objektspeicher hinzugefügte Objekt. Wenn Sie keinen Schlüssel angeben, erstellt IndexedDB einen Schlüssel und speichert ihn getrennt von den Daten.

Im folgenden Beispiel wird ein Objektspeicher mit dem Namen 'notes' erstellt und der Primärschlüssel wird automatisch als automatisch erhöhende Nummer zugewiesen:

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();

Das folgende Beispiel ähnelt dem vorherigen Beispiel, doch dieses Mal wird der Wert, der automatisch erhöht, explizit einem Attribut mit dem Namen 'id' zugewiesen.

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();

Welche Methode zum Definieren des Schlüssels verwendet werden soll, hängt von Ihren Daten ab. Wenn Ihre Daten ein Attribut haben, das immer eindeutig ist, können Sie es als keyPath festlegen, um diese Eindeutigkeit zu erzwingen. Andernfalls verwenden Sie einen Wert, der automatisch erhöht wird.

Mit dem folgenden Code werden drei Objektspeicher erstellt, die die verschiedenen Möglichkeiten zum Definieren von Primärschlüsseln in Objektspeichern zeigen:

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();

Indexe definieren

Indexe sind eine Art von Objektspeicher, mit dem Daten anhand eines bestimmten Attributs aus dem Referenzobjektspeicher abgerufen werden. Ein Index befindet sich innerhalb des Referenzobjektspeichers und enthält dieselben Daten, verwendet jedoch das angegebene Attribut als Schlüsselpfad anstelle des Primärschlüssels des Referenzspeichers. Indexe müssen beim Erstellen der Objektspeicher erstellt werden. Sie können verwendet werden, um eine eindeutige Einschränkung für Ihre Daten zu definieren.

Rufen Sie zum Erstellen eines Index die Methode createIndex() für eine Objektspeicherinstanz auf:

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();

Diese Methode erstellt ein Indexobjekt und gibt es zurück. Die Methode createIndex() auf der Instanz des Objektspeichers verwendet den Namen des neuen Index als erstes Argument und das zweite Argument bezieht sich auf das Attribut der Daten, die indexiert werden sollen. Mit dem letzten Argument können Sie zwei Optionen definieren, die bestimmen, wie der Index funktioniert: unique und multiEntry. Wenn unique auf true gesetzt ist, erlaubt der Index keine doppelten Werte für einen einzelnen Schlüssel. Als Nächstes bestimmt multiEntry, wie sich createIndex() verhält, wenn das indexierte Attribut ein Array ist. Wenn es auf true gesetzt ist, fügt createIndex() dem Index für jedes Arrayelement einen Eintrag hinzu. Andernfalls wird ein einzelner Eintrag hinzugefügt, der das Array enthält.

Beispiel:

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();

In diesem Beispiel haben die Objektspeicher 'people' und 'notes' Indexe. Zum Erstellen der Indexe weisen Sie zuerst das Ergebnis von createObjectStore() (einem Objektspeicherobjekt) einer Variablen zu, damit Sie dafür createIndex() aufrufen können.

Mit Daten arbeiten

In diesem Abschnitt wird beschrieben, wie Sie Daten erstellen, lesen, aktualisieren und löschen. Diese Vorgänge sind alle asynchron und verwenden Promis, wenn die IndexedDB API Anfragen verwendet. Dies vereinfacht die API. Anstatt auf Ereignisse zu warten, die durch die Anfrage ausgelöst werden, können Sie .then() für das Datenbankobjekt aufrufen, das von der Methode openDB() zurückgegeben wird, um Interaktionen mit der Datenbank zu starten, oder await für deren Erstellung.

Alle Datenvorgänge in IndexedDB werden innerhalb einer Transaktion ausgeführt. Jeder Vorgang hat die folgende Form:

  1. Datenbankobjekt abrufen.
  2. Transaktion in Datenbank öffnen.
  3. Objektspeicher bei Transaktion öffnen.
  4. Operation für Objektspeicher ausführen.

Eine Transaktion kann als sicherer Wrapper für einen Vorgang oder eine Gruppe von Vorgängen betrachtet werden. Wenn eine der Aktionen in einer Transaktion fehlschlägt, werden alle Aktionen zurückgesetzt. Transaktionen sind spezifisch für einen oder mehrere Objektspeicher, die Sie beim Öffnen der Transaktion definieren. Sie können schreibgeschützt oder schreibgeschützt sein. Dies gibt an, ob die Vorgänge innerhalb der Transaktion die Daten lesen oder eine Änderung an der Datenbank vornehmen.

Daten erstellen

Rufen Sie zum Erstellen von Daten die Methode add() auf der Datenbankinstanz auf und übergeben Sie die Daten, die Sie hinzufügen möchten. Das erste Argument der Methode add() ist der Objektspeicher, dem Sie die Daten hinzufügen möchten, und das zweite Argument ein Objekt, das die Felder und zugehörigen Daten enthält, die Sie hinzufügen möchten. Hier ist das einfachste Beispiel, in dem eine einzelne Datenzeile hinzugefügt wird:

import {openDB} from 'idb';

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

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

addItemToStore();

Jeder add()-Aufruf erfolgt innerhalb einer Transaktion. Selbst wenn das Promise erfolgreich aufgelöst wird, bedeutet dies nicht unbedingt, dass der Vorgang funktioniert hat. Sie müssen mit der Methode transaction.done() prüfen, ob die gesamte Transaktion abgeschlossen wurde. Dies ist ein Versprechen, das aufgelöst wird, wenn die Transaktion selbst abgeschlossen wird, und abgelehnt wird, wenn die Transaktion fehlerhaft ist. Sie müssen diese Prüfung für alle Schreibvorgänge ausführen, da Sie nur so feststellen können, welche Änderungen an der Datenbank tatsächlich vorgenommen wurden.

Der folgende Code zeigt die Verwendung der Methode add() innerhalb einer Transaktion:

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();

Nachdem Sie die Datenbank geöffnet und bei Bedarf einen Objektspeicher erstellt haben, müssen Sie eine Transaktion öffnen, indem Sie die Methode transaction() dafür aufrufen. Diese Methode verwendet ein Argument für den Speicher, für den Sie eine Transaktion ausführen möchten, sowie den Modus. In diesem Fall möchten wir in den Speicher schreiben. Daher gibt dieses Beispiel 'readwrite' an.

Im nächsten Schritt können Sie dem Geschäft im Rahmen der Transaktion Artikel hinzufügen. Im vorherigen Beispiel haben wir drei Vorgänge im 'foods'-Speicher, die jeweils ein Promise zurückgeben:

  1. Es wird ein Datensatz für ein leckeres Sandwich hinzugefügt.
  2. Ein Datensatz für ein paar Eier wird hinzugefügt.
  3. Sie signalisieren, dass die Transaktion abgeschlossen ist (tx.done).

Da alle diese Aktionen alle versprechenbasiert sind, müssen wir warten, bis alle sie abgeschlossen sind. Die Übergabe dieser Versprechen an Promise.all ist eine gute, ergonomische Methode. Promise.all akzeptiert ein Array von Promise-Werten und wird beendet, wenn alle an sie übergebenen Promise behoben wurden.

Zum Hinzufügen der beiden Datensätze ruft die store-Schnittstelle der Transaktionsinstanz add() auf und übergibt die Daten an sie. Sie können den Promise.all-Aufruf await, sodass er nach Abschluss der Transaktion abgeschlossen wird.

Daten lesen

Rufen Sie zum Lesen von Daten die Methode get() auf der Datenbankinstanz auf, die Sie mit der Methode openDB() abrufen. get() verwendet den Namen des Speichers und den Primärschlüssel des Objekts, das Sie abrufen möchten. Hier ein einfaches Beispiel:

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();

Wie bei add() gibt die get()-Methode ein Promise zurück. Du kannst sie also mit await versehen oder den .then()-Callback des Promise verwenden.

Im folgenden Beispiel wird mit der Methode get() im Objektspeicher 'foods' der 'test-db4'-Datenbank eine einzelne Zeile mit dem Primärschlüssel 'name' abgerufen:

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();

Das Abrufen einer einzelnen Zeile aus der Datenbank ist ziemlich einfach: Öffnen Sie die Datenbank und geben Sie den Objektspeicher und den Primärschlüsselwert der Zeile an, aus der Sie Daten abrufen möchten. Da die Methode get() ein Promise zurückgibt, können Sie sie await.

Daten aktualisieren

Rufen Sie zum Aktualisieren von Daten die Methode put() im Objektspeicher auf. Die Methode put() ähnelt der Methode add() und kann auch anstelle von add() verwendet werden, um Daten zu erstellen. Hier sehen Sie ein einfaches Beispiel für die Verwendung von put(), um eine Zeile in einem Objektspeicher anhand ihres Primärschlüssels zu aktualisieren:

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();

Wie andere Methoden gibt diese Methode ein Promise zurück. Sie können auch put() als Teil einer Transaktion verwenden. Hier ein Beispiel für den 'foods'-Speicher von vorhin, bei dem der Preis für das Sandwich und die Eier aktualisiert wird:

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();

Wie Elemente aktualisiert werden, hängt davon ab, wie Sie den Schlüssel festlegen. Wenn Sie einen keyPath festlegen, ist jede Zeile im Objektspeicher mit einem Inline-Schlüssel verknüpft. Im vorherigen Beispiel werden Zeilen anhand dieses Schlüssels aktualisiert. Wenn Sie in dieser Situation Zeilen aktualisieren, müssen Sie diesen Schlüssel angeben, um das entsprechende Element im Objektspeicher zu aktualisieren. Sie können auch einen Out-of-Line-Schlüssel erstellen, indem Sie autoIncrement als Primärschlüssel festlegen.

Daten löschen

Rufen Sie zum Löschen von Daten die Methode delete() im Objektspeicher auf:

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();

Ähnlich wie add() und put() können Sie dies im Rahmen einer Transaktion verwenden:

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();

Die Struktur der Datenbankinteraktion ist mit der der anderen Vorgänge identisch. Sie müssen prüfen, ob die gesamte Transaktion abgeschlossen wurde. Dazu nehmen Sie die Methode tx.done in das Array auf, das Sie an Promise.all übergeben.

Alle Daten werden abgerufen

Bisher haben Sie jeweils nur Objekte aus dem Speicher abgerufen. Sie können auch alle Daten oder einen Teil der Daten aus einem Objektspeicher oder Index abrufen. Verwenden Sie dazu entweder die Methode getAll() oder Cursor.

Die Methode getAll()

Die einfachste Möglichkeit, alle Daten eines Objektspeichers abzurufen, besteht darin, getAll() für den Objektspeicher oder Index aufzurufen:

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();

Bei dieser Methode werden alle Objekte im Objektspeicher ohne Einschränkungen zurückgegeben. Dies ist die direkteste Methode, um alle Werte aus einem Objektspeicher abzurufen, aber auch die am wenigsten flexibel.

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();

In diesem Beispiel wird getAll() im 'foods'-Objektspeicher aufgerufen. Dadurch werden alle Objekte aus 'foods' nach Primärschlüssel geordnet zurückgegeben.

Cursor verwenden

Cursor bieten eine flexiblere Möglichkeit, mehrere Objekte abzurufen. Ein Cursor wählt jedes Objekt in einem Objektspeicher oder Index einzeln aus, sodass Sie etwas mit den Daten tun können, wenn sie ausgewählt sind. Cursor arbeiten wie die anderen Datenbankvorgänge in Transaktionen.

Rufen Sie zum Erstellen eines Cursors im Rahmen einer Transaktion openCursor() für den Objektspeicher auf. Mit dem Speicher 'foods' aus den vorherigen Beispielen springen Sie wie folgt vor, um einen Cursor durch alle Datenzeilen in einem Objektspeicher zu bewegen:

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();

Die Transaktion wird in diesem Fall im 'readonly'-Modus geöffnet und die zugehörige openCursor-Methode wird aufgerufen. In einer nachfolgenden while-Schleife können die Attribute key und value der Zeile an der aktuellen Position des Cursors gelesen werden. Sie können mit diesen Werten so arbeiten, wie es für Ihre Anwendung am sinnvollsten ist. Wenn Sie bereit sind, können Sie die Methode continue() des cursor-Objekts aufrufen, um zur nächsten Zeile zu gelangen. Die while-Schleife wird beendet, wenn der Cursor das Ende des Datasets erreicht.

Cursor mit Bereichen und Indexen verwenden

Mit Indexen können Sie die Daten in einem Objektspeicher nicht mit dem Primärschlüssel, sondern mit einem anderen Attribut abrufen. Sie können einen Index für jedes Attribut erstellen, der zum keyPath für den Index wird, einen Bereich für dieses Attribut angeben und die Daten innerhalb des Bereichs mit getAll() oder einem Cursor abrufen.

Definieren Sie den Bereich mit dem Objekt IDBKeyRange und einer der folgenden Methoden:

Die Methoden upperBound() und lowerBound() geben die Ober- und Untergrenzen des Bereichs an.

IDBKeyRange.lowerBound(indexKey);

Oder:

IDBKeyRange.upperBound(indexKey);

Sie benötigen jeweils ein Argument: den keyPath-Wert des Index für das Element, das Sie als Ober- oder Untergrenze angeben möchten.

Bei der Methode bound() werden ein Ober- und Untergrenze angegeben:

IDBKeyRange.bound(lowerIndexKey, upperIndexKey);

Der Bereich für diese Funktionen ist standardmäßig inklusiv, d. h., er umfasst die Daten, die als Limits des Bereichs angegeben wurden. Wenn Sie diese Werte weglassen möchten, geben Sie den Bereich als exklusiv an. Übergeben Sie dazu true als zweites Argument für lowerBound() oder upperBound() bzw. als drittes und viertes Argument von bound() für die Unter- bzw. Obergrenze.

Im nächsten Beispiel wird ein Index für das Attribut 'price' im 'foods'-Objektspeicher verwendet. An den Speicher ist nun auch ein Formular mit zwei Eingaben für die Ober- und Untergrenzen des Bereichs angehängt. Mit dem folgenden Code kannst du Lebensmittel finden, deren Preise zwischen diesen Grenzwerten liegen:

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);

Der Beispielcode ruft zuerst die Werte für die Limits ab und prüft, ob die Limits vorhanden sind. Der nächste Codeblock entscheidet, mit welcher Methode der Bereich basierend auf den Werten begrenzt wird. Öffnen Sie in der Datenbankinteraktion wie gewohnt den Objektspeicher der Transaktion und dann den Index 'price' dafür. Mit dem Index 'price' können Sie Artikel nach Preis suchen.

Der Code öffnet dann einen Cursor auf dem Index und übergibt den Bereich. Der Cursor gibt ein Versprechen zurück, das das erste Objekt im Bereich darstellt, oder undefined, wenn keine Daten im Bereich vorhanden sind. Die Methode cursor.continue() gibt einen Cursor zurück, der das nächste Objekt darstellt, und durchläuft die Schleife, bis das Ende des Bereichs erreicht ist.

Datenbankversionsverwaltung

Wenn Sie die Methode openDB() aufrufen, können Sie die Versionsnummer der Datenbank im zweiten Parameter angeben. In allen Beispielen in diesem Leitfaden wurde die Version auf 1 gesetzt. Eine Datenbank kann jedoch auf eine neue Version aktualisiert werden, wenn Sie Änderungen vornehmen müssen. Ist die angegebene Version höher als die Version der vorhandenen Datenbank, wird der upgrade-Callback im Ereignisobjekt ausgeführt, sodass Sie der Datenbank neue Objektspeicher und Indexe hinzufügen können.

Das db-Objekt im upgrade-Callback hat ein spezielles oldVersion-Attribut, das die Versionsnummer der Datenbank angibt, auf die der Browser Zugriff hat. Sie können diese Versionsnummer in eine switch-Anweisung übergeben, um Codeblöcke innerhalb des upgrade-Callbacks basierend auf der vorhandenen Datenbankversionsnummer auszuführen. Beispiel:

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');
    }
  }
});

In diesem Beispiel wird die neueste Version der Datenbank auf 2 gesetzt. Wenn dieser Code zum ersten Mal ausgeführt wird, ist die Datenbank noch nicht im Browser vorhanden. Daher lautet oldVersion 0 und die switch-Anweisung beginnt bei case 0. Im Beispiel wird der Datenbank dadurch ein 'store'-Objektspeicher hinzugefügt.

Wichtig: In switch-Anweisungen befindet sich normalerweise ein break nach jedem case-Block. Dies wird hier jedoch absichtlich nicht verwendet. Wenn die vorhandene Datenbank etwas älter ist oder nicht vorhanden ist, durchläuft der Code so lange die restlichen case-Blöcke, bis er auf dem neuesten Stand ist. In diesem Beispiel fährt der Browser also über case 1 weiter und erstellt einen name-Index im Objektspeicher store.

Aktualisieren Sie die Versionsnummer und fügen Sie wie folgt einen neuen case-Block hinzu, um einen 'description'-Index im 'store'-Objektspeicher zu erstellen:

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');
    }
  }
});

Wenn die im vorherigen Beispiel erstellte Datenbank noch im Browser vorhanden ist, hat oldVersion bei der Ausführung den Wert 2. Der Browser überspringt case 0 und case 1 und führt den Code in case 2 aus, wodurch ein description-Index erstellt wird. Danach hat der Browser eine Datenbank der Version 3, die einen store-Objektspeicher mit den Indexen name und description enthält.

Weitere Informationen

Die folgenden Ressourcen enthalten weitere Informationen und Kontext zur Verwendung von IndexedDB.

IndexedDB-Dokumentation

Beschränkungen für die Datenspeicherung