Praca z IndexedDB

W tym przewodniku znajdziesz podstawowe informacje o interfejsie IndexedDB API. Używamy biblioteki IndexedDB Promised Jake'a Archibalda, która jest bardzo podobna do interfejsu IndexedDB API, ale używa obietnic, które można await, aby uzyskać bardziej zwięzłą składnię. Upraszcza to interfejs API, zachowując jego strukturę.

Co to jest IndexedDB?

IndexedDB to wielkoskalowy system pamięci NoSQL, który umożliwia przechowywanie niemal wszystkiego w przeglądarce użytkownika. Oprócz zwykłych działań wyszukiwania, pobierania i umieszczania IndexedDB obsługuje też transakcje i dobrze nadaje się do przechowywania dużych ilości uporządkowanych danych.

Każda baza danych IndexedDB jest unikalna dla danego źródła (zwykle domeny lub subdomeny witryny), co oznacza, że nie może uzyskać dostępu do żadnego innego źródła ani nie może być przez nie dostępna. Limity miejsca w magazynie danych są zwykle duże, jeśli w ogóle istnieją, ale różne przeglądarki inaczej obsługują limity i usuwanie danych. Więcej informacji znajdziesz w sekcji Dalsze materiały.

Terminy IndexedDB

Baza danych
Najwyższy poziom IndexedDB. Zawiera magazyny obiektów, które z kolei zawierają dane, które chcesz zachować. Możesz utworzyć wiele baz danych o dowolnych nazwach.
Magazyn obiektów
Pojedynczy zasobnik do przechowywania danych, podobny do tabel w relacyjnych bazach danych. Zwykle dla każdego typu (nie typu danych JavaScriptu) przechowywanych danych istnieje jeden magazyn obiektów. W przeciwieństwie do tabel bazy danych typy danych JavaScriptu w magazynie nie muszą być spójne. Jeśli na przykład aplikacja ma magazyn obiektów people zawierający informacje o 3 osobach, właściwości wieku tych osób mogą mieć wartości 53, 'twenty-five' i unknown.
Indeks
Rodzaj magazynu obiektów służący do porządkowania danych w innym magazynie obiektów (nazywanym referencyjnym magazynem obiektów) według poszczególnych właściwości danych. Indeks służy do pobierania rekordów z magazynu obiektów według tej właściwości. Jeśli na przykład przechowujesz dane o osobach, możesz później chcieć pobrać je według imienia i nazwiska, wieku lub ulubionego zwierzęcia.
Operacja
Interakcja z bazą danych.
Transakcja
Opakowanie operacji lub grupy operacji, które zapewnia integralność bazy danych. Jeśli jedno z działań w transakcji się nie powiedzie, żadne z nich nie zostanie zastosowane, a baza danych powróci do stanu sprzed rozpoczęcia transakcji. Wszystkie operacje odczytu i zapisu w IndexedDB muszą być częścią transakcji. Umożliwia to wykonywanie niepodzielnych operacji odczytu, modyfikacji i zapisu bez ryzyka konfliktów z innymi wątkami, które działają w bazie danych w tym samym czasie.
Kursor
Mechanizm iteracji po wielu rekordach w bazie danych.

Jak sprawdzić, czy IndexedDB jest obsługiwana

IndexedDB jest obsługiwany prawie wszędzie. Jeśli jednak pracujesz ze starszymi przeglądarkami, warto na wszelki wypadek sprawdzić, czy obsługują tę funkcję. Najprostszym sposobem jest sprawdzenie obiektu 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();

Jak otworzyć bazę danych

Za pomocą IndexedDB możesz utworzyć wiele baz danych o dowolnych nazwach. Jeśli baza danych nie istnieje, gdy próbujesz ją otworzyć, zostanie utworzona automatycznie. Aby otworzyć bazę danych, użyj metody openDB() z biblioteki 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();

Ta metoda zwraca obietnicę, która jest rozwiązywana do obiektu bazy danych. Podczas korzystania z metody openDB() podaj nazwę, numer wersji i obiekt zdarzeń, aby skonfigurować bazę danych.

Oto przykład metody openDB() w kontekście:

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

Umieść sprawdzanie obsługi IndexedDB u góry funkcji anonimowej. Jeśli przeglądarka nie obsługuje IndexedDB, ta funkcja zostanie zamknięta. Jeśli funkcja może kontynuować, wywołuje metodę openDB(), aby otworzyć bazę danych o nazwie 'test-db1'. W tym przykładzie opcjonalny obiekt zdarzeń został pominięty, aby uprościć kod, ale musisz go określić, aby wykonać jakiekolwiek istotne działania z IndexedDB.

Praca z magazynami obiektów

Baza danych IndexedDB zawiera co najmniej 1 magazyn obiektów, z których każdy ma kolumnę na klucz i inną kolumnę na dane powiązane z tym kluczem.

Tworzenie magazynów obiektów

Dobrze ustrukturyzowana baza danych IndexedDB powinna mieć 1 magazyn obiektów dla każdego typu danych, które mają być przechowywane. Na przykład witryna, która przechowuje profile użytkowników i notatki, może mieć magazyn obiektów people zawierający obiekty person i magazyn obiektów notes zawierający obiekty note.

Aby zapewnić integralność bazy danych, magazyny obiektów w obiekcie zdarzeń możesz tworzyć i usuwać tylko w wywołaniu openDB(). Obiekt events udostępnia metodę upgrade(), która umożliwia tworzenie magazynów obiektów. Aby utworzyć magazyn obiektów, wywołaj metodę createObjectStore() wewnątrz metody upgrade():

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

Ta metoda przyjmuje nazwę magazynu obiektów i opcjonalny obiekt konfiguracji, który umożliwia zdefiniowanie różnych właściwości magazynu obiektów.

Oto przykład użycia 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();

W tym przykładzie obiekt zdarzeń jest przekazywany do metody openDB(), aby utworzyć magazyn obiektów. Podobnie jak wcześniej, magazyn obiektów jest tworzony w metodzie upgrade() obiektu zdarzenia. Jeśli jednak spróbujesz utworzyć magazyn obiektów, który już istnieje, przeglądarka zgłosi błąd. Dlatego zalecamy umieszczenie metody createObjectStore() w instrukcji if, która sprawdza, czy magazyn obiektów istnieje. W bloku if wywołaj createObjectStore(), aby utworzyć magazyn obiektów o nazwie 'firstOS'.

Jak definiować klucze podstawowe

Podczas definiowania magazynów obiektów możesz określić, jak dane są jednoznacznie identyfikowane w magazynie za pomocą klucza podstawowego. Klucz podstawowy możesz zdefiniować, określając ścieżkę klucza lub używając generatora kluczy.

Ścieżka klucza to właściwość, która zawsze istnieje i zawiera unikalną wartość. Na przykład w przypadku magazynu obiektów people możesz wybrać adres e-mail jako ścieżkę klucza:

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

W tym przykładzie tworzony jest magazyn obiektów o nazwie 'people', a właściwość email jest przypisywana jako klucz podstawowy w opcji keyPath.

Możesz też użyć generatora kluczy, takiego jak autoIncrement. Generator kluczy tworzy unikalną wartość dla każdego obiektu dodanego do magazynu obiektów. Domyślnie, jeśli nie określisz klucza, IndexedDB utworzy go i zapisze oddzielnie od danych.

W tym przykładzie tworzony jest magazyn obiektów o nazwie 'notes', a klucz podstawowy jest ustawiany tak, aby był przypisywany automatycznie jako liczba zwiększana automatycznie:

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

Poniższy przykład jest podobny do poprzedniego, ale tym razem wartość automatycznie zwiększana jest jawnie przypisana do właściwości o nazwie '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();

Wybór metody definiowania klucza zależy od danych. Jeśli Twoje dane mają właściwość, która jest zawsze unikalna, możesz ustawić ją jako keyPath, aby wymusić tę unikalność. W przeciwnym razie użyj wartości zwiększanej automatycznie.

Poniższy kod tworzy 3 magazyny obiektów, pokazując różne sposoby definiowania kluczy podstawowych w magazynach obiektów:

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

Jak definiować indeksy

Indeksy to rodzaj magazynu obiektów, który służy do pobierania danych z referencyjnego magazynu obiektów według określonej właściwości. Indeks znajduje się w magazynie obiektów referencyjnych i zawiera te same dane, ale jako ścieżki klucza używa określonej właściwości, a nie klucza podstawowego magazynu referencyjnego. Indeksy muszą być tworzone podczas tworzenia magazynów obiektów i mogą być używane do definiowania unikalnego ograniczenia danych.

Aby utworzyć indeks, wywołaj metodę createIndex() w instancji magazynu obiektów:

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

Ta metoda tworzy i zwraca obiekt indeksu. Metoda createIndex() w instancji magazynu obiektów przyjmuje nazwę nowego indeksu jako pierwszy argument, a drugi argument odnosi się do właściwości danych, które chcesz zindeksować. Ostatni argument pozwala zdefiniować 2 opcje, które określają sposób działania indeksu: unique i multiEntry. Jeśli właściwość unique ma wartość true, indeks nie zezwala na zduplikowane wartości dla jednego klucza. Następnie multiEntry określa, jak zachowuje się createIndex(), gdy indeksowana właściwość jest tablicą. Jeśli jest ustawiona na true, createIndex() dodaje wpis w indeksie dla każdego elementu tablicy. W przeciwnym razie dodaje pojedynczy wpis zawierający tablicę.

Oto przykład:

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

W tym przykładzie magazyny obiektów 'people' i 'notes' mają indeksy. Aby utworzyć indeksy, najpierw przypisz wynik createObjectStore() (obiekt magazynu obiektów) do zmiennej, aby można było wywołać na nim createIndex().

Praca z danymi

W tej sekcji opisano, jak tworzyć, odczytywać, aktualizować i usuwać dane. Wszystkie te operacje są asynchroniczne i wykorzystują obietnice, podczas gdy interfejs IndexedDB API używa żądań. Upraszcza to interfejs API. Zamiast nasłuchiwać zdarzeń wywoływanych przez żądanie, możesz wywołać metodę .then() na obiekcie bazy danych zwróconym przez metodę openDB(), aby rozpocząć interakcje z bazą danych, lub await, aby ją utworzyć.

Wszystkie operacje na danych w IndexedDB są wykonywane w ramach transakcji. Każda operacja ma następującą formę:

  1. Pobierz obiekt bazy danych.
  2. Otwórz transakcję w bazie danych.
  3. Otwórz magazyn obiektów w transakcji.
  4. Wykonaj operację na magazynie obiektów.

Transakcję można traktować jako bezpieczną otoczkę operacji lub grupy operacji. Jeśli jedno z działań w ramach transakcji się nie powiedzie, wszystkie działania zostaną wycofane. Transakcje są powiązane z co najmniej jednym magazynem obiektów, który określasz podczas otwierania transakcji. Mogą być tylko do odczytu lub do odczytu i zapisu. Wskazuje, czy operacje w transakcji odczytują dane, czy wprowadzają zmiany w bazie danych.

Tworzenie danych

Aby utworzyć dane, wywołaj metodę add() w instancji bazy danych i przekaż dane, które chcesz dodać. Pierwszym argumentem metody add() jest magazyn obiektów, do którego chcesz dodać dane, a drugim – obiekt zawierający pola i powiązane dane, które chcesz dodać. Oto najprostszy przykład, w którym dodawany jest pojedynczy wiersz danych:

import {openDB} from 'idb';

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

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

addItemToStore();

Każde wywołanie add() odbywa się w ramach transakcji, więc nawet jeśli obietnica zostanie spełniona, nie musi to oznaczać, że operacja się powiodła. Aby upewnić się, że operacja dodawania została wykonana, musisz sprawdzić, czy cała transakcja została zakończona, używając metody transaction.done(). Jest to obietnica, która jest spełniana, gdy transakcja zostanie zakończona, i odrzucana, gdy wystąpi błąd transakcji. Musisz przeprowadzić tę kontrolę w przypadku wszystkich operacji „zapisu”, ponieważ jest to jedyny sposób, aby dowiedzieć się, czy zmiany w bazie danych rzeczywiście zostały wprowadzone.

Poniższy kod pokazuje użycie metody add() w transakcji:

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

Po otwarciu bazy danych (i utworzeniu magazynu obiektów, jeśli jest to konieczne) musisz otworzyć transakcję, wywołując metodę transaction(). Ta metoda przyjmuje argument dla sklepu, w którym chcesz przeprowadzić transakcję, oraz tryb. W tym przypadku chcemy zapisywać dane w magazynie, więc w przykładzie podano 'readwrite'.

Następnym krokiem jest dodanie produktów do sklepu w ramach transakcji. W poprzednim przykładzie mamy do czynienia z 3 operacjami w magazynie 'foods', z których każda zwraca obietnicę:

  1. Dodaję rekord dotyczący pysznej kanapki.
  2. Dodaję wpis dotyczący jajek.
  3. Sygnalizowanie, że transakcja została zakończona (tx.done).

Ponieważ wszystkie te działania są oparte na obietnicach, musimy poczekać, aż wszystkie się zakończą. Przekazanie tych obietnic do Promise.all to wygodny i ergonomiczny sposób na wykonanie tego zadania. Promise.all akceptuje tablicę obietnic i kończy działanie, gdy wszystkie przekazane do niej obietnice zostaną spełnione.

W przypadku 2 dodawanych rekordów interfejs store instancji transakcji wywołuje metodę add() i przekazuje jej dane. Możesz await wywołanie Promise.all, aby zakończyło się po zakończeniu transakcji.

Odczytywanie danych

Aby odczytać dane, wywołaj metodę get() w instancji bazy danych pobranej za pomocą metody openDB(). get() przyjmuje nazwę magazynu i wartość klucza podstawowego obiektu, który chcesz pobrać. Oto podstawowy przykład:

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

Podobnie jak w przypadku add() metoda get() zwraca obietnicę, więc możesz użyć await, jeśli wolisz, lub wywołania zwrotnego obietnicy .then().

W tym przykładzie użyto metody get() w magazynie obiektów 'foods' bazy danych 'test-db4', aby pobrać pojedynczy wiersz według klucza podstawowego '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();

Pobranie pojedynczego wiersza z bazy danych jest dość proste: otwórz bazę danych i określ magazyn obiektów oraz wartość klucza podstawowego wiersza, z którego chcesz pobrać dane. Metoda get() zwraca obietnicę, więc możesz ją await.

Zaktualizuj dane

Aby zaktualizować dane, wywołaj metodę put() w magazynie obiektów. Metoda put() jest podobna do metody add() i można jej używać zamiast metody add() do tworzenia danych. Oto podstawowy przykład użycia metody put() do zaktualizowania wiersza w magazynie obiektów według wartości klucza podstawowego:

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

Podobnie jak inne metody, ta metoda zwraca obietnicę. Możesz też użyć put() w ramach transakcji. Oto przykład użycia sklepu 'foods' z poprzedniego przykładu, w którym aktualizujemy cenę kanapki i jajek:

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

Sposób aktualizowania elementów zależy od sposobu ustawienia klucza. Jeśli ustawisz keyPath, każdy wiersz w magazynie obiektów będzie powiązany z kluczem wbudowanym. W poprzednim przykładzie wiersze są aktualizowane na podstawie tego klucza. Gdy aktualizujesz wiersze w tej sytuacji, musisz określić ten klucz, aby zaktualizować odpowiedni element w magazynie obiektów. Możesz też utworzyć klucz zewnętrzny, ustawiając autoIncrement jako klucz podstawowy.

Usuń dane

Aby usunąć dane, wywołaj metodę delete() w magazynie obiektów:

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

Podobnie jak w przypadku add() i put(), możesz użyć tego elementu w ramach transakcji:

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

Struktura interakcji z bazą danych jest taka sama jak w przypadku innych operacji. Pamiętaj, aby sprawdzić, czy cała transakcja została zakończona, przez uwzględnienie metody tx.done w tablicy przekazywanej do Promise.all.

Pobieranie wszystkich danych

Do tej pory obiekty były pobierane z magazynu pojedynczo. Możesz też pobrać wszystkie dane lub ich podzbiór z magazynu obiektów lub indeksu za pomocą metody getAll() lub kursorów.

Metoda getAll()

Najprostszym sposobem na pobranie wszystkich danych z magazynu obiektów jest wywołanie getAll() w magazynie obiektów lub indeksie, na przykład:

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

Ta metoda zwraca wszystkie obiekty w magazynie obiektów bez żadnych ograniczeń. Jest to najbardziej bezpośredni sposób na uzyskanie wszystkich wartości z magazynu obiektów, ale też najmniej elastyczny.

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

Ten przykład wywołuje metodę getAll() w magazynie obiektów 'foods'. Spowoduje to zwrócenie wszystkich obiektów z 'foods' uporządkowanych według klucza podstawowego.

Jak używać kursorów

Kursory to bardziej elastyczny sposób pobierania wielu obiektów. Kursor wybiera po kolei każdy obiekt w magazynie obiektów lub indeksie, umożliwiając wykonanie działania na wybranym obiekcie. Kursory, podobnie jak inne operacje na bazie danych, działają w transakcjach.

Aby utworzyć kursor, wywołaj openCursor() w magazynie obiektów w ramach transakcji. Korzystając z magazynu 'foods' z poprzednich przykładów, możesz przesuwać kursor przez wszystkie wiersze danych w magazynie obiektów w ten sposób:

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

W tym przypadku transakcja jest otwierana w trybie 'readonly' i wywoływana jest jej metoda openCursor. W kolejnej pętli while można odczytać właściwości key i value wiersza w bieżącej pozycji kursora i wykonać na tych wartościach dowolne operacje, które mają sens w przypadku Twojej aplikacji. Gdy wszystko będzie gotowe, możesz wywołać metodę continue() obiektu cursor, aby przejść do następnego wiersza. Pętla while zakończy się, gdy kursor dotrze do końca zbioru danych.

Używanie kursorów z zakresami i indeksami

Indeksy umożliwiają pobieranie danych z magazynu obiektów według właściwości innej niż klucz podstawowy. Możesz utworzyć indeks dla dowolnej właściwości, która stanie się keyPath indeksu, określić zakres tej właściwości i pobrać dane w tym zakresie za pomocą getAll() lub kursora.

Zdefiniuj zakres za pomocą obiektu IDBKeyRange i jednej z tych metod:

Metody upperBound() i lowerBound() określają górną i dolną granicę zakresu.

IDBKeyRange.lowerBound(indexKey);

Lub:

IDBKeyRange.upperBound(indexKey);

Każda z nich przyjmuje 1 argument: wartość keyPath indeksu elementu, który chcesz określić jako górny lub dolny limit.

Metoda bound() określa zarówno górny, jak i dolny limit:

IDBKeyRange.bound(lowerIndexKey, upperIndexKey);

Zakres tych funkcji jest domyślnie inkluzywny, co oznacza, że obejmuje dane określone jako granice zakresu. Aby pominąć te wartości, określ zakres jako wyłączny, przekazując true jako drugi argument funkcji lowerBound() lub upperBound() albo jako trzeci i czwarty argument funkcji bound(), odpowiednio dla dolnego i górnego limitu.

W kolejnym przykładzie użyto indeksu we właściwości 'price' w magazynie obiektów 'foods'. Do magazynu jest teraz dołączony formularz z 2 danymi wejściowymi dla górnej i dolnej granicy zakresu. Aby znaleźć produkty spożywcze z cenami w tych granicach, użyj tego kodu:

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

Przykładowy kod najpierw pobiera wartości limitów i sprawdza, czy limity istnieją. Następny blok kodu decyduje, której metody użyć do ograniczenia zakresu na podstawie wartości. W interakcji z bazą danych otwórz magazyn obiektów w transakcji w zwykły sposób, a następnie otwórz indeks 'price' w magazynie obiektów. Indeks 'price' umożliwia wyszukiwanie produktów według ceny.

Następnie kod otwiera kursor w indeksie i przekazuje zakres. Kursor zwraca obietnicę reprezentującą pierwszy obiekt w zakresie lub undefined, jeśli w zakresie nie ma danych. Metoda cursor.continue() zwraca kursor reprezentujący następny obiekt i kontynuuje pętlę, aż do osiągnięcia końca zakresu.

Obsługa wersji bazy danych

Podczas wywoływania metody openDB() możesz określić numer wersji bazy danych w drugim parametrze. We wszystkich przykładach w tym przewodniku wersja została ustawiona na 1, ale bazę danych można uaktualnić do nowej wersji, jeśli trzeba ją w jakiś sposób zmodyfikować. Jeśli określona wersja jest nowsza niż wersja istniejącej bazy danych, wykonywane jest wywołanie zwrotne upgrade w obiekcie zdarzenia, co umożliwia dodanie do bazy danych nowych magazynów obiektów i indeksów.

Obiekt db w wywołaniu zwrotnym upgrade ma specjalną właściwość oldVersion, która wskazuje numer wersji bazy danych, do której przeglądarka ma dostęp. Możesz przekazać ten numer wersji do instrukcji switch, aby wykonywać bloki kodu w wywołaniu zwrotnym upgrade na podstawie istniejącego numeru wersji bazy danych. Oto przykład:

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

W tym przykładzie najnowsza wersja bazy danych jest ustawiona na 2. Gdy ten kod zostanie wykonany po raz pierwszy, baza danych nie będzie jeszcze istnieć w przeglądarce, więc oldVersion będzie mieć wartość 0, a instrukcja switch rozpocznie się od case 0. W tym przykładzie dodaje to do bazy danych magazyn obiektów 'store'.

Ważna uwaga: w instrukcjach switch po każdym bloku case zwykle występuje break, ale tutaj celowo nie jest używany. Dzięki temu, jeśli istniejąca baza danych jest o kilka wersji do tyłu lub nie istnieje, kod będzie kontynuował działanie przez pozostałe bloki case, dopóki nie będzie aktualny. W tym przykładzie przeglądarka kontynuuje wykonywanie kodu do case 1, tworząc indeks name w magazynie obiektów store.

Aby utworzyć indeks 'description' w magazynie obiektów 'store', zaktualizuj numer wersji i dodaj nowy blok case w następujący sposób:

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

Jeśli baza danych utworzona w poprzednim przykładzie nadal istnieje w przeglądarce, po wykonaniu tego kodu oldVersion będzie mieć wartość 2. Przeglądarka pomija case 0 i case 1, a następnie wykonuje kod w case 2, co powoduje utworzenie indeksu description. Po tym kroku przeglądarka ma bazę danych w wersji 3 zawierającą magazyn obiektów store z indeksami name i description.

Więcej informacji

Więcej informacji i kontekstu dotyczących korzystania z IndexedDB znajdziesz w tych materiałach.

Dokumentacja IndexedDB

Limity miejsca na dane