IndexedDB を操作する

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

IndexedDB とは

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

各 IndexedDB データベースはオリジン(通常はサイトのドメインまたはサブドメイン)に固有です。つまり、他のオリジンにアクセスしたり、他のオリジンからアクセスされたりすることはありません。データ ストレージの上限は通常、存在する場合でも大きいですが、ブラウザによって上限とデータの強制排除の処理方法が異なります。詳細については、参考情報のセクションをご覧ください。

IndexedDB の用語

データベース
IndexedDB の最上位レベル。これには、永続化するデータを含むオブジェクト ストアが含まれます。任意の名前で複数のデータベースを作成できます。
オブジェクト ストア
データを保存する個々のバケット。リレーショナル データベースのテーブルに似ています。通常、保存するデータの種類(JavaScript のデータ型ではない)ごとに 1 つのオブジェクト ストアがあります。データベース テーブルとは異なり、ストア内のデータの JavaScript データ型は一貫している必要はありません。たとえば、アプリに 3 人の情報を含む people オブジェクト ストアがある場合、それらのユーザーの年齢プロパティは 53、'twenty-five'、unknown になります。
Index
別のオブジェクト ストア(参照オブジェクト ストアと呼ばれる)のデータを、データの個々のプロパティによって整理するためのオブジェクト ストアの一種。インデックスは、このプロパティによってオブジェクト ストア内のレコードを取得するために使用されます。たとえば、人物を保存する場合、後で名前、年齢、好きな動物で取得することがあります。
オペレーション
データベースとのやり取り。
Transaction
データベースの整合性を確保するオペレーションまたはオペレーション グループをラップします。トランザクション内のアクションのいずれかが失敗すると、どのアクションも適用されず、データベースはトランザクション開始前の状態に戻ります。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' という名前のデータベースを開きます。この例では、わかりやすくするためにオプションの events オブジェクトを省略していますが、IndexedDB で意味のある処理を行うには、このオブジェクトを指定する必要があります。

オブジェクト ストアの操作方法

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

オブジェクト ストアを作成する

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

データベースの整合性を確保するため、openDB() 呼び出しの events オブジェクトでのみ、オブジェクト ストアを作成または削除できます。events オブジェクトは、オブジェクト ストアを作成できる 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();

この例では、events オブジェクトが openDB() メソッドに渡されてオブジェクト ストアが作成されます。前述のように、オブジェクト ストアの作成は、イベント オブジェクトの upgrade() メソッドで行われます。ただし、すでに存在するオブジェクト ストアを作成しようとすると、ブラウザでエラーがスローされるため、createObjectStore() メソッドを if ステートメントでラップして、オブジェクト ストアが存在するかどうかを確認することをおすすめします。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 つのオプション(unique と multiEntry)を定義できます。unique が true に設定されている場合、インデックスでは 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 つ以上のオブジェクト ストアに固有です。読み取り専用または読み取り / 書き込みにできます。これは、トランザクション内のオペレーションがデータを読み取るか、データベースに変更を加えるかを示します。

データを作成する

データを作成するには、データベース インスタンスで 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' ストアで 3 つのオペレーションを処理しています。各オペレーションは Promise を返します。

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

データベースから 1 つの行を取得するのは、非常に簡単です。データベースを開き、オブジェクト ストアと、データを取得する行の主キー値を指定します。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 番目の引数として、または bound() の 3 番目と 4 番目の引数として true を渡して、範囲を排他的に指定します。

次の例では、'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 に設定します。このコードが最初に実行されるとき、データベースはブラウザにまだ存在しないため、oldVersion は 0 であり、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');
    }
  }
});

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

関連情報

IndexedDB の使用に関する詳細とコンテキストについては、次のリソースをご覧ください。

IndexedDB のドキュメント

データ ストレージの上限