Praca z IndexedDB

Ten przewodnik zawiera podstawowe informacje o interfejsie IndexedDB API. Korzystamy z biblioteki IndexedDB Promised Jake'a Archibalda, która jest bardzo podobna do interfejsu IndexedDB API, ale wykorzystuje obietnice – możesz je await, aby uzyskać bardziej zwięzłą składnię. Upraszcza to interfejs API i pozwala zachować jego strukturę.

Co to jest IndexedDB?

IndexedDB to system pamięci NoSQL na dużą skalę, który umożliwia przechowywanie niemal wszystkiego, co znajduje się w przeglądarce użytkownika. Oprócz zwykłego wyszukiwania, pobierania i wprowadzania danych 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 źródła (zwykle jest to domena lub subdomena witryny), co oznacza, że nie ma do niej dostępu z żadnego innego źródła. Limity miejsca na dane są zwykle duże (o ile w ogóle istnieją), ale różne przeglądarki inaczej obsługują limity i usuwanie danych. Więcej informacji znajdziesz w sekcji Więcej informacji.

Terminy dotyczące IndexedDB

Baza danych
Najwyższy poziom IndexedDB. Zawiera on magazyny obiektów, które z kolei zawierają dane, które chcesz zachować. Możesz utworzyć wiele baz danych z wybranymi nazwami.
Składnica obiektów
Pojedynczy zasobnik do przechowywania danych – podobnie jak w przypadku tabel w relacyjnych bazach danych. Zwykle dla każdego typu danych (a nie typu danych JavaScript) jest po 1 magazynie obiektów. W przeciwieństwie do tabel bazy danych typy danych JavaScriptu w sklepie nie muszą być spójne. Jeśli na przykład aplikacja ma magazyn obiektów people z informacjami o 3 osobach, właściwościami wieku tych osób mogą być następujące: 53, 'twenty-five' i unknown.
Indeks
Rodzaj magazynu obiektów do porządkowania danych w innym magazynie obiektów (nazywany referencyjnym magazynem obiektów) według pojedynczej właściwości danych. Indeks jest używany do pobierania rekordów z magazynu obiektów przez tę właściwość. Jeśli na przykład gromadzisz ludzi, możesz użyć ich później do pobrania według nazwy, wieku lub ulubionego zwierzęcia.
Operacja
Interakcja z bazą danych.
Transakcja
Otoka operacji lub grupy operacji, która zapewnia integralność bazy danych. Jeśli jedno z działań w transakcji zakończy się niepowodzeniem, nie zostanie zastosowane żadne z nich, a baza danych wraca do stanu sprzed rozpoczęcia transakcji. Wszystkie operacje odczytu i zapisu w IndexedDB muszą być częścią transakcji. Umożliwia to atomowe operacje odczytu, modyfikacji i zapisu bez ryzyka konfliktu z innymi wątkami działającymi w tym samym czasie na bazie danych.
Kursor
Mechanizm iteracji z użyciem wielu rekordów w bazie danych.

Jak sprawdzić obsługę IndexedDB

Protokół IndexedDB jest prawie obsługiwany Jeśli jednak korzystasz ze starszych przeglądarek, nie warto na wszelki wypadek zezwolić na wykrywanie funkcji. Najłatwiej sprawdzić obiekt 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 z dowolnymi nazwami. Jeśli w momencie otwarcia baza danych nie istnieje, jest ona tworzona 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 prowadzi do obiektu bazy danych. Jeśli używasz metody openDB(), podaj nazwę, numer wersji i obiekt zdarzeń, aby skonfigurować bazę danych.

Oto przykład użycia 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ść na górze funkcji anonimowej sprawdzenie obsługi IndexedDB. Spowoduje to zamknięcie funkcji, jeśli przeglądarka nie obsługuje IndexedDB. Jeśli można kontynuować, wywołuje metodę openDB(), by otworzyć bazę danych o nazwie 'test-db1'. W tym przykładzie pominięto opcjonalny obiekt zdarzeń, aby uprościć zadanie, ale musisz go określić, aby móc wykonywać zadania z wykorzystaniem IndexedDB.

Jak pracować z magazynami obiektów

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

Tworzenie magazynów obiektów

Dobrze uporządkowana baza danych IndexedDB powinna mieć po 1 magazynie obiektów dla każdego typu danych, który musi być utrwalony. Na przykład witryna, która zachowuje profile użytkowników i notatki, może mieć magazyn obiektów people z obiektami person oraz magazyn obiektów notes z obiektami note.

Aby zapewnić integralność bazy danych, możesz tworzyć i usuwać tylko magazyny obiektów w obiekcie zdarzeń w wywołaniu openDB(). Obiekt zdarzeń ujawnia metodę upgrade(), która umożliwia tworzenie magazynów obiektów. Wywołaj metodę createObjectStore() w metodzie upgrade(), aby utworzyć magazyn obiektów:

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

Przyjmuje ona nazwę magazynu obiektów i opcjonalny obiekt konfiguracji, który umożliwia definiowanie różnych właściwości magazynu obiektów.

Oto przykład użycia właściwości 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. Tak jak wcześniej tworzenie magazynu obiektów wykonuje się za pomocą metody upgrade() obiektu zdarzenia. Ponieważ jednak przeglądarka zgłasza błąd przy próbie utworzenia magazynu obiektów, który już istnieje, 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 zdefiniować klucze podstawowe

Definiując magazyny obiektów, możesz za pomocą klucza podstawowego określić sposób jednoznacznej identyfikacji danych w magazynie. Klucz podstawowy można zdefiniować, definiując ścieżkę klucza lub korzystając z 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 jako ścieżkę klucza możesz wybrać adres e-mail:

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 tworzymy magazyn obiektów o nazwie 'people' i przypisuje on właściwość email jako klucz podstawowy w opcji keyPath.

Możesz też skorzystać z generatora kluczy, np. autoIncrement. Generator kluczy tworzy niepowtarzalną wartość dla każdego obiektu dodanego do magazynu obiektów. Domyślnie, jeśli nie określisz klucza, IndexedDB utworzy klucz i będzie przechowywać go oddzielnie od danych.

Poniższy przykład tworzy magazyn obiektów o nazwie 'notes' i ustawia klucz podstawowy, który zostanie przypisany automatycznie jako liczba przyrostowa 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 przykładu, ale tym razem wartość automatycznego przyrostu 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 użycia do definiowania klucza zależy od Twoich danych. Jeśli Twoje dane mają właściwość, która jest zawsze unikalna, możesz ustawić w niej właściwość keyPath, aby podkreślić tę niepowtarzalność. W przeciwnym razie użyj wartości automatycznego zwiększania.

Poniższy kod tworzy 3 magazyny obiektów, co pokazuje 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 służący do pobierania danych z magazynu obiektów referencyjnych według określonej właściwości. Indeks znajduje się w referencyjnym magazynie obiektów i zawiera te same dane, ale jako jego ścieżka 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. Za ich pomocą można definiować ograniczenie dotyczące 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 w danych, które chcesz zindeksować. Ostatni argument umożliwia zdefiniowanie 2 opcji określających sposób działania indeksu: unique i multiEntry. Jeśli unique ma wartość true, indeks nie zezwala na zduplikowane wartości w jednym kluczu. Następnie multiEntry określa, jak zachowuje się createIndex(), gdy zindeksowana właściwość jest tablicą. Jeśli ustawiona jest wartość 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 do zmiennej wynik funkcji createObjectStore() (obiekt magazynu obiektów), aby móc wywołać dla niej createIndex().

Jak pracować z danymi

W tej sekcji dowiesz się, jak tworzyć, odczytywać, aktualizować i usuwać dane. Wszystkie te operacje są asynchroniczne z użyciem obietnic, w przypadku których interfejs IndexedDB API używa żądań. Upraszcza to interfejs API. Zamiast nasłuchiwać zdarzeń aktywowanych przez żądanie możesz wywołać .then() w obiekcie bazy danych zwróconym przez metodę openDB(), aby rozpocząć interakcje z bazą danych, lub await jej tworzenie.

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

  1. Pobranie obiektu bazy danych.
  2. Otwórz transakcję w bazie danych.
  3. Otwórz magazyn obiektów przy transakcji.
  4. Wykonywanie operacji na magazynie obiektów.

Transakcję można traktować jako bezpieczną otokę operacji lub grupy operacji. Jeśli jedno z działań w ramach transakcji się nie powiedzie, wszystkie zostaną wycofane. Transakcje odnoszą się do co najmniej 1 magazynu obiektów, które określasz przy otwieraniu transakcji. Mogą być tylko do odczytu lub odczytu i zapisu. Wskazuje, czy operacje wewnątrz 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ć. Pierwszy argument metody add() to magazyn obiektów, do którego chcesz dodać dane, a drugi argument to obiekt zawierający pola i powiązane dane, które chcesz dodać. Oto najprostszy przykład, w którym dodano 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 obiecywanie zadziała, niekoniecznie oznacza to, że operacja się powiodła. Aby mieć pewność, że operacja dodawania została przeprowadzona, musisz użyć metody transaction.done(), by sprawdzić, czy cała transakcja została zakończona. Jest to obietnica, która znika po zakończeniu transakcji i jest odrzucana w przypadku błędów. Musisz je przeprowadzać w przypadku wszystkich operacji zapisu, ponieważ tylko dzięki temu możesz się dowiedzieć, czy zmiany w bazie danych rzeczywiście zaszły.

Ten 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 w razie potrzeby utworzeniu magazynu obiektów) musisz otworzyć transakcję, wywołując w niej metodę transaction(). Przyjmuje ona argument dla sklepu, w którym chcesz przeprowadzić transakcję, oraz trybu. W tym przypadku chcemy napisać do sklepu, więc w tym przykładzie określamy właściwość 'readwrite'.

Następnym krokiem jest rozpoczęcie dodawania produktów do sklepu w ramach transakcji. W poprzednim przykładzie mamy tu 3 operacje w sklepie 'foods', z których każda zwraca obietnicę:

  1. Dodaję nagranie pysznej kanapki.
  2. Dodaję rekord dotyczący kilku jajek.
  3. Powiadomienie, że transakcja została ukończona (tx.done).

Wszystkie te działania opierają się na obietnicach, więc musimy poczekać, aż się zakończą. Spełnienie tych obietnic na stronie Promise.all to dobry, ergonomiczny sposób. Promise.all akceptuje tablicę obietnic i kończy się, gdy zostaną spełnione wszystkie przekazane do niej obietnice.

Interfejs store instancji transakcji wywołuje add() i przekazuje do niej dane. Możesz await wywołać 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() pobiera nazwę magazynu i podstawową wartość klucza 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();

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

W tym przykładzie użyto metody get() w magazynie obiektów 'foods' bazy danych 'test-db4' do uzyskania pojedynczego wiersza przez klucz podstawowy '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();

Pobieranie pojedynczego wiersza z bazy danych jest proste: otwórz bazę danych i podaj magazyn obiektów oraz wartość klucza podstawowego w wierszu, 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 add() i można jej używać do tworzenia danych zamiast add(). Oto podstawowy przykład użycia funkcji put() do aktualizowania 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, metoda ta zwraca obietnicę. W ramach transakcji możesz też użyć put(). Oto przykład z wcześniejszym sklepem 'foods', który aktualizuje 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 skonfigurowania klucza. Jeśli ustawisz keyPath, każdy wiersz w magazynie obiektów będzie powiązany z kluczem wbudowanym. Poprzedni przykład aktualizuje wiersze na podstawie tego klucza. Gdy aktualizujesz wiersze w tej sytuacji, musisz podać ten klucz, aby zaktualizować odpowiedni element w magazynie obiektów. Możesz też utworzyć klucz poza wierszem, 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();

Tak 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. W tym celu dodaj metodę tx.done do tablicy przekazywanej do Promise.all.

Pobieram wszystkie dane

Do tej pory obiekty były pobierane ze sklepu tylko pojedynczo. Wszystkie dane lub podzbiór możesz też pobrać z magazynu obiektów lub indeksu, korzystając z metody getAll() lub kursorów.

Metoda getAll()

Najprostszym sposobem pobrania wszystkich danych z magazynu obiektów jest wywołanie getAll() w magazynie lub indeksie obiektów. Ten sposób:

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ń. To najbardziej bezpośredni sposób na uzyskanie wszystkich wartości z magazynu obiektów, ale jednocześnie 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();

W tym przykładzie wywołujesz getAll() w magazynie obiektów 'foods'. Zwraca wszystkie obiekty z 'foods', uporządkowane według klucza podstawowego.

Jak używać kursorów

Kursory to bardziej elastyczny sposób pobierania wielu obiektów. Kursor wybiera każdy obiekt w magazynie lub indeksie jeden po drugim, dzięki czemu możesz wykonać jakąś czynność związaną z wybranymi danymi. Kursory, podobnie jak inne operacje na bazie danych, obsługują transakcje.

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 przesunąć kursor we wszystkie wiersze danych w magazynie obiektów w następujący 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();

Transakcja jest w tym przypadku otwierana w trybie 'readonly' i wywoływana jest jej metoda openCursor. W kolejnej pętli while wiersz w bieżącym położeniu kursora może odczytywać właściwości key i value. Możesz na nich wykonywać operacje w dowolny sposób, który najlepiej pasuje do Twojej aplikacji. Gdy wszystko będzie gotowe, możesz wywołać metodę continue() obiektu cursor, aby przejść do następnego wiersza, a 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 usługi, którą stanie się keyPath dla indeksu. Następnie możesz określić zakres dla tej usługi i pobrać dane z tego zakresu za pomocą funkcji getAll() lub kursora.

Zdefiniuj zakres za pomocą obiektu IDBKeyRange lub dowolnej z tych metod:

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

IDBKeyRange.lowerBound(indexKey);

lub:

IDBKeyRange.upperBound(indexKey);

Każdy z nich przyjmuje 1 argument: wartość keyPath indeksu dla elementu, który chcesz określić jako górną lub dolną granicę.

Metoda bound() określa zarówno górną, jak i dolną granicę:

IDBKeyRange.bound(lowerIndexKey, upperIndexKey);

Zakres tych funkcji jest domyślnie włączony, 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 trzeci i czwarty argument funkcji bound() odpowiednio do dolnej i górnej granicy.

W następnym przykładzie użyto indeksu we właściwości 'price' w magazynie obiektów 'foods'. Do sklepu jest teraz dołączony formularz z 2 wejściami dla górnej i dolnej granicy zakresu. Użyj tego kodu, aby znaleźć produkty, których ceny mieszczą się w tych limitach:

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 te limity istnieją. Następny blok kodu określa, której metody użyć, aby ograniczyć zakres na podstawie wartości. W interakcjach z bazą danych otwórz w zwykły sposób magazyn obiektów w transakcji, a następnie otwórz indeks 'price' w magazynie obiektów. Indeks 'price' umożliwia wyszukiwanie produktów według ceny.

Kod otwiera kursor na indeksie i przekazuje go w obrębie zakresu. Kursor zwraca obietnicę reprezentującą pierwszy obiekt w zakresie lub undefined, jeśli nie ma danych w zakresie. Metoda cursor.continue() zwraca kursor reprezentujący następny obiekt i kontynuuje pętlę, aż dotrzesz do końca zakresu.

Obsługa wersji bazy danych

Gdy wywołujesz metodę openDB(), w drugim parametrze możesz podać numer wersji bazy danych. We wszystkich przykładach w tym przewodniku wersja ma ustawienie 1, ale w razie potrzeby można uaktualnić bazę danych do nowej wersji. Jeśli podana wersja jest wyższa niż wersja istniejącej bazy danych, w obiekcie zdarzenia wykonywane jest wywołanie zwrotne upgrade, 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 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 najnowszą wersję bazy danych ustawia się na 2. Gdy ten kod jest uruchamiany po raz pierwszy, bazy danych nie ma jeszcze w przeglądarce, więc oldVersion to 0, a instrukcja switch zaczyna się od case 0. W tym przykładzie dodaje to do bazy danych magazyn obiektów 'store'.

Co ważne: w instrukcjach switch po każdej blokadzie case znajduje się zwykle element break, ale ten cel nie jest tu używany. W ten sposób, jeśli istniejąca baza danych jest opóźniona o kilka wersji lub nie istnieje, kod jest stosowany w pozostałych blokach case, dopóki nie zostanie zaktualizowany. W tym przykładzie przeglądarka kontynuuje wykonywanie kodu 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 ten 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 działania oldVersion ma wartość 2. Przeglądarka pomija elementy case 0 i case 1, a potem wykonuje kod w komponencie case 2, co powoduje utworzenie indeksu description. Później przeglądarka będzie dysponować bazą danych w wersji 3 zawierającą magazyn obiektów store z indeksami name i description.

Więcej informacji

W tych materiałach znajdziesz więcej informacji i kontekst na temat korzystania z IndexedDB.

Dokumentacja IndexedDB

Limity miejsca na dane