IndexedDB を操作する

このガイドでは、IndexedDB API の基本について説明します。ここでは Jake Archibald の IndexedDB Promised ライブラリを使用します。このライブラリは IndexedDB API とよく似ていますが、Promise を使用します。await を使用すると、より簡潔な構文になります。これにより、API の構造を維持しながら API を簡素化できます。

IndexedDB とは

IndexedDB は大規模な NoSQL ストレージ システムであり、ユーザーのブラウザにほぼすべてのデータを保存できます。通常の検索、get、put アクションに加えて、IndexedDB はトランザクションもサポートしており、大量の構造化データの保存に適しています。

各 IndexedDB データベースはオリジン(通常はサイトドメインまたはサブドメイン)に対して固有です。つまり、他のオリジンへのアクセスや、他のオリジンからのアクセスはできません。通常、データ ストレージの上限が存在する場合は大きくなりますが、ブラウザごとに上限とデータ エビクションの処理方法が異なります。詳しくは、関連情報をご覧ください。

IndexedDB の用語

データベース
最上位レベルの IndexedDB。これにはオブジェクト ストアが含まれ、このストアには永続化するデータが含まれています。複数のデータベースを任意の名前で作成できます。
オブジェクト ストア
リレーショナル データベースのテーブルと同様に、データを保存する個別のバケット。通常、オブジェクト ストアは、格納するデータの(JavaScript データ型ではなく)ごとに 1 つあります。データベース テーブルとは異なり、店舗内の JavaScript データ型に一貫性を持たせる必要はありません。たとえば、アプリに 3 人に関する情報を含む people オブジェクト ストアがある場合、それらの人の年齢プロパティは 53'twenty-five'unknown になります。
Index
別のオブジェクト ストア(参照オブジェクト ストア)のデータをデータの個々のプロパティによって整理するためのオブジェクト ストアの一種。インデックスは、このプロパティによってオブジェクト ストア内のレコードを取得するために使用されます。たとえば、人物を保存している場合、後で名前、年齢、好きな動物などで取得することもできます。
オペレーション
データベースとのやり取り。
Transaction
データベースの整合性を保証するオペレーションまたはオペレーション グループのラッパー。トランザクション内のアクションの 1 つが失敗すると、いずれのアクションも適用されず、データベースはトランザクション開始前の状態に戻ります。IndexedDB のすべての読み取りオペレーションまたは書き込みオペレーションは、トランザクションの一部である必要があります。これにより、データベースで同時に動作する他のスレッドと競合するリスクなしに、アトミックな読み取り - 変更 - 書き込みオペレーションが可能になります。
Cursor
データベース内の複数のレコードを反復処理するメカニズム。

IndexedDB のサポートを確認する方法

IndexedDB はほぼ広くサポートされています。ただし、古いブラウザを使用している場合は、念のため機能検出のサポートを利用することは賢明ではありません。最も簡単な方法は、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();

データベースを開く方法

IndexedDB では、任意の名前で複数のデータベースを作成できます。データベースが存在しない場合は、開こうとすると自動的に作成されます。データベースを開くには、idb ライブラリの openDB() メソッドを使用します。

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

このメソッドは、データベース オブジェクトに解決される Promise を返します。openDB() メソッドを使用する場合は、名前、バージョン番号、イベント オブジェクトを指定してデータベースを設定します。

openDB() メソッドを使用する場合の例を以下に示します。

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

IndexedDB サポートのチェックを匿名関数の先頭に配置します。ブラウザが IndexedDB をサポートしていない場合、この関数は終了します。関数が続行できる場合は、openDB() メソッドを呼び出して、'test-db1' という名前のデータベースを開きます。この例では、わかりやすくするためにオプションのイベント オブジェクトを省略していますが、IndexedDB で意味のある処理を行うには、このオブジェクトを指定する必要があります。

オブジェクト ストアの使用方法

IndexedDB データベースには 1 つ以上のオブジェクト ストアが含まれ、それぞれにキーの列と、そのキーに関連付けられたデータ用の列があります。

オブジェクト ストアの作成

適切に構造化された IndexedDB データベースには、永続化が必要なデータの種類ごとに 1 つのオブジェクト ストアが必要です。たとえば、ユーザー プロファイルとメモを保持するサイトでは、person オブジェクトを含む people オブジェクト ストアと、note オブジェクトを含む notes オブジェクト ストアを使用できます。

データベースの整合性を確保するため、イベント オブジェクトでオブジェクト ストアの作成や削除は、openDB() 呼び出しでのみ行うことができます。イベント オブジェクトは、オブジェクト ストアを作成できる upgrade() メソッドを公開します。upgrade() メソッド内で createObjectStore() メソッドを呼び出して、オブジェクト ストアを作成します。

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

このメソッドは、オブジェクト ストアの名前と、オブジェクト ストアのさまざまなプロパティを定義できるオプションの構成オブジェクトを受け取ります。

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

この例では、イベント オブジェクトを openDB() メソッドに渡してオブジェクト ストアを作成します。前と同様に、オブジェクト ストアの作成作業はイベント オブジェクトの upgrade() メソッドで行われます。ただし、すでに存在するオブジェクト ストアを作成しようとすると、ブラウザはエラーをスローするため、オブジェクト ストアが存在するかどうかを確認する if ステートメントに createObjectStore() メソッドをラップすることをおすすめします。if ブロック内で、createObjectStore() を呼び出して、'firstOS' という名前のオブジェクト ストアを作成します。

主キーを定義する方法

オブジェクト ストアを定義するときに、主キーを使用してストア内でデータを一意に識別する方法を定義できます。主キーを定義するには、キーパスを定義するか、キー ジェネレータを使用します。

キーパスは常に存在するプロパティであり、一意の値を含みます。たとえば、people オブジェクト ストアの場合、鍵パスとしてメールアドレスを選択できます。

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

この例では、'people' というオブジェクト ストアを作成し、keyPath オプションの主キーとして email プロパティを割り当てます。

autoIncrement などのキー生成ツールを使用することもできます。キー生成ツールは、オブジェクト ストアに追加されたオブジェクトごとに一意の値を作成します。デフォルトでは、キーを指定しない場合、IndexedDB はキーを作成し、データとは別に保存します。

次の例では、'notes' というオブジェクト ストアを作成し、主キーが自動インクリメント数として自動的に割り当てられるように設定します。

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

次の例は前の例と似ていますが、ここでは自動インクリメント値が '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();

キーの定義に使用する方法は、データによって異なります。データに常に一意のプロパティがある場合は、それを keyPath にすることで、この一意性を適用できます。それ以外の場合は、自動インクリメント値を使用します。

次のコードは、オブジェクト ストア内で主キーを定義するさまざまな方法を示す 3 つのオブジェクト ストアを作成します。

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

インデックスの定義方法

インデックスは、オブジェクト ストアの一種で、指定されたプロパティによって参照オブジェクト ストアからデータを取得するために使用されます。インデックスは参照オブジェクト ストア内にあり、同じデータを含んでいますが、リファレンス ストアの主キーではなく、指定されたプロパティをキーパスとして使用します。インデックスはオブジェクト ストアの作成時に作成する必要があります。インデックスは、データに対する一意の制約を定義するために使用できます。

インデックスを作成するには、オブジェクト ストア インスタンスで createIndex() メソッドを呼び出します。

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

このメソッドは、インデックス オブジェクトを作成して返します。オブジェクト ストアのインスタンスの createIndex() メソッドは、最初の引数として新しいインデックスの名前を受け取り、2 番目の引数は、インデックスに登録するデータのプロパティを参照します。最後の引数では、インデックスの動作を決定する 2 つのオプション(uniquemultiEntry)を定義できます。uniquetrue に設定されている場合、インデックスは 1 つのキーに対して重複した値を許可しません。次に、multiEntry は、インデックス付きプロパティが配列の場合の createIndex() の動作を決定します。true に設定した場合、createIndex() は、各配列要素のインデックスにエントリを追加します。それ以外の場合は、配列を含む単一のエントリが追加されます。

次の例をご覧ください。

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

この例では、'people''notes' のオブジェクト ストアにインデックスがあります。インデックスを作成するには、まず createObjectStore()(オブジェクト ストア オブジェクト)の結果を変数に代入して、createIndex() を呼び出せるようにします。

データの活用方法

このセクションでは、データを作成、読み取り、更新、削除する方法について説明します。これらのオペレーションはすべて非同期で、IndexedDB API がリクエストを使用する Promise を使用します。これにより、API が簡素化されます。リクエストによってトリガーされたイベントをリッスンする代わりに、openDB() メソッドから返されたデータベース オブジェクトに対して .then() を呼び出してデータベースとのやり取りを開始するか、データベースの作成を await できます。

IndexedDB のデータ オペレーションはすべて、トランザクション内で実行されます。各オペレーションの形式は次のとおりです。

  1. データベース オブジェクトを取得します。
  2. データベースのトランザクションをオープンします。
  3. トランザクションでオブジェクト ストアを開く。
  4. オブジェクト ストアに対してオペレーションを実行します。

トランザクションは、1 つのオペレーションまたはオペレーション グループの安全なラッパーと考えることができます。トランザクション内のアクションの 1 つが失敗すると、すべてのアクションがロールバックされます。トランザクションは 1 つ以上のオブジェクト ストアに固有で、トランザクションを開くときに定義します。読み取り専用または読み取りと書き込みが可能です。これは、トランザクション内のオペレーションがデータを読み取るか、データベースに変更を加えるかを示します。

データを作成する

データを作成するには、データベース インスタンスで add() メソッドを呼び出し、追加するデータを渡します。add() メソッドの最初の引数は、データを追加するオブジェクト ストアです。2 番目の引数は、追加するフィールドと関連データを含むオブジェクトです。以下に、データの 1 行を追加する最もシンプルな例を示します。

import {openDB} from 'idb';

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

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

addItemToStore();

add() 呼び出しはトランザクション内で発生するため、Promise が正常に解決されたとしても、オペレーションが機能したことを意味するとは限りません。追加オペレーションが完了したことを確認するには、transaction.done() メソッドを使用してトランザクション全体が完了したかどうかを確認する必要があります。これは、トランザクションが自身で完了すると解決され、トランザクション エラーが発生した場合は拒否される Promise です。このチェックは、すべての「書き込み」オペレーションで行う必要があります。データベースに対する変更が実際に行われたかどうかを確認する唯一の方法であるためです。

次のコードは、トランザクション内での add() メソッドの使用を示しています。

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

データベースを開いたら(さらに、必要に応じてオブジェクト ストアを作成したら、そのデータベースで transaction() メソッドを呼び出してトランザクションを開く必要があります)このメソッドは、取引する店舗とモードの引数を受け取ります。この例ではストアに書き込む必要があるため、この例では 'readwrite' を指定しています。

次のステップとして、トランザクションの一環としてストアにアイテムを追加します。上記の例では、'foods' ストアに対して、それぞれが Promise を返す 3 つのオペレーションを処理しています。

  1. おいしいサンドイッチの記録を追加します。
  2. 卵のレコードを追加します。
  3. 取引が完了したことを通知します(tx.done)。

これらのアクションはすべて Promise ベースであるため、すべてのアクションが完了するまで待つ必要があります。そのためには、これらの Promise を Promise.all に渡すと便利です。Promise.all は Promise の配列を受け入れ、渡されたすべての Promise が解決されると終了します。

追加する 2 つのレコードについて、トランザクション インスタンスの store インターフェースが add() を呼び出し、データを渡します。Promise.all 呼び出しを await して、トランザクションが完了したときに終了するようにできます。

データを読み取る

データを読み取るには、openDB() メソッドを使用して取得したデータベース インスタンスで get() メソッドを呼び出します。get() は、店舗の名前と、取得するオブジェクトの主キーの値を受け取ります。基本的な例を次に示します。

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

add() と同様に、get() メソッドは Promise を返すため、必要に応じて await または Promise の .then() コールバックを使用できます。

次の例では、'test-db4' データベースの 'foods' オブジェクト ストアで get() メソッドを使用して、'name' 主キーで 1 つの行を取得します。

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

データベースから単一の行を取得することは非常に簡単です。データベースを開き、データを取得する行のオブジェクト ストアと主キーの値を指定します。get() メソッドは Promise を返すため、await できます。

データの更新

データを更新するには、オブジェクト ストアの put() メソッドを呼び出します。put() メソッドは add() メソッドと似ており、add() の代わりに使用してデータを作成することもできます。put() を使用し、主キーの値でオブジェクト ストア内の行を更新する基本的な例を次に示します。

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

他のメソッドと同様に、このメソッドは Promise を返します。また、トランザクションの一部として put() を使用することもできます。前述の 'foods' ストアを使用して、サンドイッチと卵の価格を更新する例を次に示します。

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

アイテムの更新方法は、キーの設定方法によって異なります。keyPath を設定すると、オブジェクト ストア内の各行がインラインキーに関連付けられます。上記の例では、このキーに基づいて行を更新します。この状況で行を更新する場合は、そのキーを指定してオブジェクト ストア内の適切なアイテムを更新する必要があります。autoIncrement を主キーとして設定して、オフライン キーを作成することもできます。

データの削除

データを削除するには、オブジェクト ストアの delete() メソッドを呼び出します。

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

add()put() と同様に、これをトランザクションの一部として使用できます。

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

データベース操作の構造は、他のオペレーションと同じです。Promise.all に渡す配列に tx.done メソッドを含めて、トランザクション全体が完了したことを確認してください。

すべてのデータの取得

これまでは、ストアから一度に 1 つずつオブジェクトしか取得していませんでした。getAll() メソッドまたはカーソルを使用して、オブジェクト ストアまたはインデックスからすべてのデータまたはサブセットを取得することもできます。

getAll() メソッド

オブジェクト ストアのすべてのデータを取得する最も簡単な方法は、次のようにオブジェクト ストアまたはインデックスに対して getAll() を呼び出すことです。

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

このメソッドは、制約なしでオブジェクト ストア内のすべてのオブジェクトを返します。これは、オブジェクト ストアからすべての値を取得する最も直接的な方法ですが、最も柔軟性が低い方法でもあります。

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

この例では、'foods' オブジェクト ストアの getAll() を呼び出します。これにより、'foods' のすべてのオブジェクトが主キーで並べ替えられて返されます。

カーソルの使用方法

カーソルは、複数のオブジェクトをより柔軟に取得できます。カーソルによって、オブジェクト ストア内の各オブジェクトが選択されるか、1 つずつインデックス付けされます。これにより、選択されたデータに対してなんらかの操作を行うことができます。カーソルは、他のデータベース オペレーションと同様に、トランザクションで機能します。

カーソルを作成するには、トランザクションの一部としてオブジェクト ストアに対して openCursor() を呼び出します。上記の例の 'foods' ストアを使用して、オブジェクト ストア内のすべてのデータの行までカーソルを進めます。

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

この場合、トランザクションは 'readonly' モードで開かれ、openCursor メソッドが呼び出されます。後続の while ループでは、カーソルの現在の位置にある行の key プロパティと value プロパティを読み取ることができ、アプリに最適な方法でそれらの値を操作できます。準備ができたら、cursor オブジェクトの continue() メソッドを呼び出して次の行に移動し、カーソルがデータセットの最後に到達すると while ループが終了します。

範囲とインデックスでカーソルを使用する

インデックスを使用すると、主キー以外のプロパティによってオブジェクト ストア内のデータをフェッチできます。任意のプロパティに対してインデックス(インデックスの keyPath になる)を作成し、そのプロパティの範囲を指定して、getAll() またはカーソルを使用して範囲内のデータを取得できます。

IDBKeyRange オブジェクトまたは次のいずれかのメソッドを使用して範囲を定義します。

upperBound() メソッドと lowerBound() メソッドは、範囲の上限と下限を指定します。

IDBKeyRange.lowerBound(indexKey);

または

IDBKeyRange.upperBound(indexKey);

それぞれ 1 つの引数(上限または下限として指定するアイテムのインデックスの keyPath 値)を取ります。

bound() メソッドは上限と下限の両方を指定します。

IDBKeyRange.bound(lowerIndexKey, upperIndexKey);

これらの関数の範囲はデフォルトで包括的です。つまり、範囲の上限として指定されたデータも含まれます。これらの値を除外するには、範囲を排他的として指定します。そのためには、lowerBound() または upperBound() の 2 番目の引数として true を、下限と上限のそれぞれについて、bound() の 3 番目と 4 番目の引数として渡します。

次の例では、'foods' オブジェクト ストア内の 'price' プロパティにインデックスを使用しています。ストアには、範囲の上限と下限の 2 つの入力を持つフォームも接続されました。以下のコードを使用して、これらの制限内の価格を含む食品を検索します。

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

このサンプルコードでは、まず上限の値を取得し、上限が存在するかどうかを確認します。次のコードブロックでは、値に基づいて範囲を制限するために使用するメソッドを決定します。データベースの操作で、通常どおりトランザクションでオブジェクト ストアを開き、次にオブジェクト ストアの 'price' インデックスを開きます。'price' インデックスを使用すると、価格でアイテムを検索できます。

その後、コードはインデックス上でカーソルを開き、その範囲を渡します。カーソルは、範囲内の最初のオブジェクトを表す Promise を返します。範囲内にデータがない場合は undefined を返します。cursor.continue() メソッドは、次のオブジェクトを表すカーソルを返し、範囲の最後に到達するまでループを繰り返します。

データベースのバージョニング

openDB() メソッドを呼び出すとき、2 番目のパラメータにデータベースのバージョン番号を指定できます。このガイドのすべての例で、バージョンは 1 に設定されていますが、データベースをなんらかの方法で変更する必要がある場合は、新しいバージョンにアップグレードできます。指定されたバージョンが既存のデータベースのバージョンより大きい場合、イベント オブジェクトの upgrade コールバックが実行され、新しいオブジェクト ストアとインデックスをデータベースに追加できます。

upgrade コールバックの db オブジェクトには、ブラウザがアクセスできるデータベースのバージョン番号を示す特別な oldVersion プロパティがあります。このバージョン番号を switch ステートメントに渡すと、既存のデータベースのバージョン番号に基づいて upgrade コールバック内のコードブロックを実行できます。次の例をご覧ください。

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

この例では、データベースの最新バージョンを 2 に設定しています。このコードが初めて実行されたときは、データベースがまだブラウザに存在しないため、oldVersion0 であり、switch ステートメントは case 0 から始まります。この例では、'store' オブジェクト ストアがデータベースに追加されます。

重要なポイント: switch ステートメントでは通常、各 case ブロックの後に break がありますが、ここでは意図的に使用されていません。このようにして、既存のデータベースのバージョン数が少ない場合や、既存のデータベースが存在しない場合は、更新されるまで残りの case ブロックを順に実行します。この例では、ブラウザは case 1 を介して実行を継続し、store オブジェクト ストアに name インデックスを作成します。

'store' オブジェクト ストアに 'description' インデックスを作成するには、バージョン番号を更新して、次のように新しい case ブロックを追加します。

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

前の例で作成したデータベースがブラウザにまだ存在する場合、これを実行すると、oldVersion2 になります。ブラウザは case 0case 1 をスキップし、case 2 のコードを実行します。これにより description インデックスが作成されます。その後、ブラウザにはバージョン 3 のデータベースがあり、name インデックスと description インデックスを持つ store オブジェクト ストアが含まれています。

関連情報

以下のリソースは、IndexedDB の使用に関する詳細情報とコンテキストを提供します。

IndexedDB のドキュメント

データ ストレージの上限