IndexedDB の操作

IndexedDB の基本に関するガイド。

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

IndexedDB とは

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

IndexedDB は、ファイル/blob など、大量の構造化データをクライアント側で保存するための低レベル API です。この API はインデックスを使用して、このデータを効率的に検索できるようにします。DOM Storage は少量のデータ保存には有用ですが、大量の構造化データの保存にはあまり有用ではありません。IndexedDB が解決策を提供します。

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

IndexedDB の用語

データベース
最上位レベルの IndexedDB。これにはオブジェクト ストアが含まれており、このオブジェクト ストアには、永続化するデータが含まれています。複数のデータベースを任意の名前で作成できます。
オブジェクト ストア
データを保存する個別のバケット。オブジェクト ストアは、従来のリレーショナル データベースのテーブルに似ています。通常、保存するデータのタイプごとに 1 つのオブジェクト ストアがあります(JavaScript データ型ではありません)。たとえば、ブログ投稿とユーザー プロフィールを永続化するアプリについて、2 つのオブジェクト ストアがあるとします。従来のデータベースのテーブルとは異なり、ストア内の実際の JavaScript データ型は一貫している必要はありません(たとえば、people オブジェクト ストアに 3 人の人物がいる場合、年齢プロパティは 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 つ以上のオブジェクト ストアが含まれています。オブジェクト ストアのコンセプトは、SQL データベース内のテーブルのコンセプトに似ています。SQL テーブルと同様に、オブジェクト ストアには行と列が含まれますが、IndexedDB では、IndexedDB オブジェクト ストアにキーの列と、そのキーに関連付けられたデータの列が含まれるため、列数の柔軟性が低くなります。

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

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

データベースの整合性を確保するため、オブジェクト ストアの作成と削除は、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 はキーを作成し、データとは別に保存します。

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

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

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

この例は前の例と似ていますが、自動増分値が 'id' というプロパティに明示的に割り当てられています。

キーの定義に使用する方法は、データによって異なります。データに常に一意のプロパティがある場合は、それを 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 が正常に解決されたとしても、オペレーションが実行されたとは限りません。トランザクション内のアクションの 1 つが失敗すると、トランザクション内のすべてのオペレーションがロールバックされることに注意してください。

追加オペレーションが実行されたことを確認するには、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 in-line 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 を返します。また、add() メソッドと同様に、トランザクションの一部として 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();

ここで、getAll()'foods' オブジェクト ストアで呼び出されます。ストアが主キーで並べ替え、'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 オブジェクトを使用して定義します。このオブジェクトには、範囲の上限を定義するために使用される 5 つのメソッドがあります。

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

IDBKeyRange.lowerBound(indexKey);

または

IDBKeyRange.upperBound(indexKey);

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

bound() メソッドは、上限と下限の両方を指定するために使用され、最初の引数として下限を受け取ります。

IDBKeyRange.bound(lowerIndexKey, upperIndexKey);

これらの関数の範囲はデフォルトで包含的ですが、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' オブジェクト ストアがデータベースに追加されます。

'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 のドキュメント

データ ストレージの上限