【イラスト付き】IndexedDB【利用方法】

ブラウザーAPI

皆さんこんにちは、ChanCodeです。
今回はIndexedDBをご紹介します。

IndedxedDBはブラウザに大量データを保存し、検索することができる仕組みです。今回はIndexedDBの用語や基本的な使い方をについて扱います。

こんな人にオススメ
  • IndexedDBの概要が知りたい
  • IndexedDBの書き方が知りたい

初めて学習する方にも分かるように、要点を絞って丁寧に解説していきます。

IndexedDBの全体像をご紹介します♪

IndexedDBとは

まずポイントをチェック
  • ブラウザで動作するオブジェクト指向データベース
  • 大量データの保存に対応
  • インデックスでの効率的な検索が可能
  • 非同期APIなのでメインスレッドをブロックしない
  • トランザクションをサポート

IndexedDBとはブラウザで動作するオブジェクト指向データベースです。大量データの保存が可能であり、本格的なオフラインアプリケーションやPWAなどに向いています。

保存可能なデータは構造化複製アルゴリズムに対応したデータです。DOMノードや関数以外はほとんど格納可能です。

リレーショナルデータベース(RDB)ではないことに注意してください。IndexedDBには独自の仕組みがあり、RDBと同一視すると理解しにくいことがあります。

IndexedDB APIは非同期で動作するためメインスレッドをブロック操作をしません。また、全ての操作はトランザクション内で実行されデータの整合性が保証されます。

IndexedDB APIはPromise対応していないため、そのままでは利用しづらいです。使いづらい場合はライブラリも利用可能です。

データの検索はキーだけでなくインデックスという仕組みにより、単純なキーバリュー形式よりも、柔軟な検索が可能です。インデックスはキー以外の項目で検索する機能です。

保存可能なデータ量はデバイスのディスクの空き容量によって決定されます。ディスクの空き容量の50%まで利用可能など、詳細な判断方法はブラウザごとに様々です。

空き容量のチェック方法

Storage for the web
There are many different options for storing data in the browser. Which one is best for your needs?
if (navigator.storage && navigator.storage.estimate) {
  const quota = await navigator.storage.estimate();
  // quota.usage -> Number of bytes used.
  // quota.quota -> Maximum number of bytes available.
  const percentageUsed = (quota.usage / quota.quota) * 100;
  console.log(`You've used ${percentageUsed}% of the available storage.`);
  const remaining = quota.quota - quota.usage;
  console.log(`You can write up to ${remaining} more bytes.`);
}

データの保存は同一ドメインポリシーが適用されます。そのため、異なるドメインのデータにはアクセスできません。

ブラウザで大量データの保存と効率的な検索ができます♪

用語と構造

まずポイントをチェック
  • データベース:最上位の構成要素
  • オブジェクトストア:データを格納する場所
  • インデックス:データ検索に使用する索引
  • キー:データを一意に識別するための値
    • インラインキー:格納するデータ自身に含まれる項目をキーとする
    • アウトオブラインキー:格納するデータとは別に与えるキー

IndexedDBの構成と各用語をご紹介します。RDBとは異なる独特な構成をしています。

データベース:最上位の構成要素

データベースとはIndexedDBのトップレベルに位置する要素です。この中にはオブジェクトストアが含まれています。データベースは名前で管理されており、複数のデータベースを用意することも可能です。

オブジェクトストア:データを格納する場所

オブジェクトストアとは実際にデータを格納する場所です。個別のデータを保存し、各データはキーによって一意に識別されます。

オブジェクトストアはRDBにおけるテーブルのような存在です。テーブルと同じように格納するデータの種類に応じて複数のオブジェクトストアを作成します。

例えば、ユーザー情報とタスク情報を管理したいアプリの場合、ユーザー用のオブジェクトストアとタスク用のオブジェクトストアを用意します。

インデックス:データ検索に使用する索引

インデックスはデータ検索時に利用されます。インデックスは項目をもとに作成し、その項目に基づいた検索を行うことができます。なおインデックスの作成は必須ではありません

例えば、ユーザー情報としてユーザー名やメールアドレスを保存している場合、ユーザー名インデックスやメールアドレスインデックスのように、各項目のインデックスを作成することで各項目の値で検索することができます

キー:データを一意に識別するための値

キーとはオブジェクトストア内のデータを一意に識別するための値です。キーは2種類ありインラインキーとアウトオブラインキーと言います。

インラインキーとは、格納するデータ自身に含まれる項目を利用したキーです。どの項目をキーとするかオブジェクトストア作成時に指定します。アウトオブラインキーとは、格納するデータとは別に与えるキーです。

キーはどちらの方式を選んでも機能としての違いはありません。管理しやすい方式を選んでOKです。

IndexedDBはキーとインデックスで検索可能です♪

利用ステップの全体像

まずポイントをチェック
  • 定義
    1. データベースを開く
    2. オブジェクトストアを作成
    3. インデックスを作成
  • 操作
    1. トランザクションの開始
    2. 操作対象のオブジェクトストアの取得
    3. データの操作
    4. トランザクションの終了

IndexedDBを利用する際の流れをご紹介します。ステップは大きく「定義」と「操作」に分かれます。

定義ステップ1:データベースを開く

まずはデータベースを開きます。データベースを開く際は名前とバージョン番号を指定します。

データベースの名前を途中で変更することはできません

バージョン番号は正の整数のみ指定することができます。特に指定がない場合、データベースが存在しないなら1、データベースが存在するなら現在のバージョンが指定されます。

定義ステップ2:オブジェクトストアを作成

データベースにオブジェクトストアを作成します。作成時に名前とキーの設定を指定します。オブジェクトストアはデータの種類に応じて複数用意可能です。

キーはインラインキーとアウトオブラインキーのどちらかの方式を選びます。インラインキーの場合どの項目をキーとするかを指定します。この時使う値をキーパスと言います。インラインキーとアウトオブラインキーのどちらもキージェネレーターの利用を選ぶことができます。キージェネレーターはキーの値を自動採番する仕組みです。

キーとして利用可能な値は文字列、Date、浮動小数点数、配列のいずれかの型です。

定義ステップ3:インデックスを作成

オブジェクトストアに紐づくインデックスを作成します。インデックスはオブジェクトストアごとに複数作成することができます。インデックスで利用する項目はキーパスで指定します。項目が配列の場合、オプションで配列の各要素に対して独立した検索をするかを指定します。なおインデックスは必須ではありません

ここまでが構造の定義です♪

操作ステップ1:トランザクションの開始

データ操作はトランザクションの中で行うので、まずはトランザクションを開始します。トランザクションの開始時にこれから利用するオブジェクトストアとトランザクションのモードを指定します。トランザクションモードは「参照のみ」と「読み書き」の2種類があります。

操作ステップ2:操作対象のオブジェクトストアの取得

トランザクションを開始したらオブジェクトストアを取得します。利用可能なオブジェクトストアはトランザクション開始時に宣言したもののみです。

操作ステップ3:データの操作

オブジェクトストアを使った処理を行います。操作内容はトランザクション開始時に指定したモードに従います。「参照のみ」の場合はキーやインデックスでの検索を行います。「読み書き」の場合はオブジェクトストアの更新ができます。

操作ステップ4:トランザクションの終了

トランザクションは自動的にコミットされます。全ての操作が完了するとコミットされ、いずれかの操作が失敗した場合、ロールバックされます。

定義は最初のみで、それ以降は操作を行います♪

検索方法

まずポイントをチェック
  • 主キー検索
  • 全件検索
  • インデックスでの検索
  • 範囲検索

IndexedDBで利用可能な検索方法をご紹介します。IndexedDBの検索は大きく分けて4種類あります。

主キー検索

主キー検索はキーを指定した1件検索です。インラインキーとアウトオブラインキーのどちらにしてもキーは一意であるため、検索結果は1件になります。

検索のキーワードは常に全文検索です。SQLのLIKE 演算子のような部分一致検索はできません。

全件検索

キーを指定しない場合、オブジェクトストア内の全てのデータを取得します。

インデックスでの検索

インデックスでの検索は検索結果が複数になる可能性があります。インデックス項目にユニーク制約がついている場合は検索結果は1件になります。

インデックスが配列の場合は、配列内の要素に基づいて検索することができます。設定を行うことで各要素を個別の検索対象にすることができます。

例えば、「 [‘red’, ‘blue’]という配列」と「 [‘red’, ‘green’]という配列」がある場合、redで検索すると両方の配列が検索結果になります。

範囲検索

検索する値の範囲を指定することもできます。主キーやインデックスが数値の場合に利用します。値が指定の範囲内か、指定の値以上か、などの範囲指定ができます。

条件指定は主キーかインデックスを使って行います♪

idbライブラリ

まずポイントをチェック
  • IndexedDB APIはPromise対応していない
  • idbライブラリはPromiseでラップしている

生のIndexedDB APIはコールバックとイベントを使用して実装します。そのままでは少し扱いづらいのでPromise対応したライブラリを使います。今回はidbライブラリを利用します。IndexedDB API の非同期処理をPromiseでラップしているためasync/awaitを使うことができます。

導入:ビルドツールを利用している場合

ビルドツールを利用している場合、npm install で導入できます。

npm install idb

インストール後はimport文で機能を取り出して使います。

import { xxx, xxx } from 'idb';

導入:CDN

CDNで簡易的に導入することもできます。HTMLファイルにscript要素を追加します。この場合、JavaScriptファイルから利用する際はimport文は不要です。

<script src="https://cdn.jsdelivr.net/npm/idb@7/build/umd.js"></script>

サンプルコード

サンプルHTML(indexeddb-sample/01.raw-vs-promise/index.html)

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <script src="https://cdn.jsdelivr.net/npm/idb@7/build/umd.js"></script>
    <!-- <script src="raw.js" type="module"></script> -->
    <script src="promise.js" type="module"></script>
    <title>Document</title>
</head>
<body>
    
</body>
</html>

生のIndexedDB APIを利用している例(indexeddb-sample/01.raw-vs-promise/raw.js)

// DBのオープン
const request = indexedDB.open('sample-db', 1);

// 構造の定義
request.addEventListener('upgradeneeded', (event) => {
    const db = event.target.result;
    const objectStore = db.createObjectStore('usersStore', { keyPath: 'id' });
    objectStore.createIndex('nameIndex', 'name', { unique: false });
});

// データの投入
request.addEventListener('success', (event) => {
    const db = event.target.result;
    const tx = db.transaction(['usersStore'], 'readwrite');
    const usersStore = tx.objectStore('usersStore');
    usersStore.add({ id: 1, name: 'ユーザー1' });
});

idbライブラリを利用している例(indexeddb-sample/01.raw-vs-promise/promise.js)

// DBのオープン
let db = await idb.openDB('sample-db', 1, {
    // 構造の定義
    upgrade(db) {
        db.createObjectStore('usersStore', { keyPath: 'id' });
    },
});

// データの投入
let tx = db.transaction('usersStore', 'readwrite');
let usersStore = tx.objectStore('usersStore');
await usersStore.add({ id: 1, name: 'ユーザー1' });

ビルドツールを導入している場合、メソッドはインポートして利用するため、idb.メソッド名()の様には書きません。次のように直接メソッド利用します。

import { openDB } from 'idb';
const db = await openDB('sample-db', 1, {
    upgrade(db) {
        db.createObjectStore('sampleStore', { keyPath: 'id', autoIncrement: true });
    },
});

なお、script要素をtype=”module”に設定するだけでは使えませんでした。Webpackでビルドすることでimportでの利用ができます。

IndexedDB APIはPromise対応していないので、ライブラリを使います♪

構造定義のコード

まずポイントをチェック
  • ステップ1:データベースを開く
  • ステップ2:オブジェクトストアを定義
  • ステップ3:インデックスを定義

構造定義のステップとコード例をご紹介します。ここではidbライブラリを利用します。

ステップ1:データベースを開く

まずはデータベースを開きます。idbライブラリのopenDBメソッドを使います。

openDBメソッド
  • 第一引数:データベース名
  • 第二引数:バージョン番号
  • 第三引数:定義処理
  • 戻り値:データベースを表すオブジェクトに解決されるPromiseオブジェクト

第三引数の定義処理について、データベースを開いた際の定義処理はオブジェクトのupgragdeメソッドとして渡します。データベースを開いた際の処理とはオブジェクトストアやインデックスの定義処理です。upgragdeメソッドは指定したバージョンのDBが存在しない場合のみ動作します。

// DBのオープン
let db = await idb.openDB('データベース名', バージョン番号, {
    // 構造の定義
    upgrade(db) {
        // オブジェクトストアの作成など
    },
});

ステップ2:オブジェクトストアを定義

データベースを開いた際にオブジェクトストアを定義します。オブジェクトストアの定義はupgradeメソッド内でcreateObjectStoreメソッドを使います。upgradeメソッドは引数にデータベースを表すオブジェクトを受け取ります。このオブジェクトからcreateObjectStoreメソッドを呼び出します。

createObjectStoreメソッド
  • 第一引数:オブジェクトストア名
  • 第二引数:キーの設定オブジェクト(任意)
    • keyPathプロパティ:どの項目をキーとするか
    • autoIncrementプロパティ:キーを自動採番するか
  • 戻り値:オブジェクトストアを表すオブジェクト

第二引数のキーの設定について、キー項目を指定する文字列はキーパスと言います。キーパスを指定しない場合はアウトオブラインキーが利用されます。自動採番をする場合はautoIncrementプロパティをtrueにします。なお自動採番を利用しない場合はこのプロパティは不要です。

// DBのオープン
let db = await idb.openDB('データベース名', バージョン番号, {
    // 構造の定義
    upgrade(db) {
        // オブジェクトストアの定義
        cosnt sampleStore = db.createObjectStore('オブジェクトストア名', { keyPath: 'キーパス', autoIncrement:true });
    },
});

ステップ3:インデックスを定義

オブジェクトストアに対してインデックスを定義することができます。インデックスの定義はupgradeメソッド内でcreateIndexメソッドを使います

createIndexメソッド
  • 第一引数:インデックス名
  • 第二引数:どの項目のインデックスにするか(キーパス)
  • 第三引数:インデックスの設定
  • 戻り値:インデックスを表すオブジェクト

第三引数のインデックスの設定について、インデックスの設定とは、重複を禁止するか、配列の場合に要素に基づいて検索するかの設定です。重複を禁止する場合はuniqueプロパティにtrueを指定します。配列の個別の要素で検索する場合はmultiEntryにtrueを設定します。これらの設定はデフォルトで全てfalseなので、インデックスの設定が不要な場合は第三引数は不要です

// DBのオープン
let db = await idb.openDB('データベース名', バージョン番号, {
    // 構造の定義
    upgrade(db) {
        // オブジェクトストアの定義
        cosnt sampleStore = db.createObjectStore('オブジェクトストア名', { keyPath: 'キーパス', autoIncrement:true });
        // インデックスの定義
        sampleStore.createIndex('インデックス名', 'キーパス', { unique: true, multiEntry: true});
    },
});

サンプルコード全体像(indexeddb-sample/02.create-db/app.js)

// DBのオープン
let db = await idb.openDB('sample-db', 1, {
    // 構造の定義
    upgrade(db) {
        // オブジェクトストアの定義
        const sampleStore = db.createObjectStore('sampleStore', { keyPath: 'id' });
        // インデックスの定義
        sampleStore.createIndex('sampleIndex', 'name', { unique: true, multiEntry: true});
    },
});

DB→オブジェクトストア→インデックスの順で定義します♪

データ操作のコード

まずポイントをチェック
  • ステップ1:データベースを開く
  • ステップ2:トランザクションを開始
  • ステップ3:オブジェクトストアを取得
  • ステップ4:データ操作(新規追加、更新、削除)
  • ステップ5:トランザクションの終了

データ操作のステップとコード例をご紹介します。データの新規作成、更新、削除についてご紹介します。ここではidbライブラリを利用します。

ステップ1:データベースを開く

まずはデータベースを開きます。openDBメソッドの引数にデータベース名のみ渡すと現在のバージョンのデータベースを開くことができます。戻り値のPromiseが解決されるとデータベースを表すオブジェクトを取得できます。

// DBのオープン
const db = await idb.openDB('データベース名');

ステップ2:トランザクションを開始

データ操作の前にトランザクションを開始します。トランザクションの開始はtransactionメソッドを使います。データ操作の際はトランザクションモードにreadwriteを指定します。

transactionメソッド
  • 第一引数:対象のオブジェクトストア名の配列
  • 第二引数:トランザクションモード
    • readonly:参照のみ
    • readwrite:読み書き
  • 戻り値:トランザクションを表すオブジェクト
// DBのオープン
const db = await idb.openDB('データベース名');
// トランザクション開始
const tx = db.transaction(['オブジェクトストア名'], 'トランザクションモード');

ステップ3:オブジェクトストアを取得

トランザクションを開始したらオブジェクトストアを取得します。利用可能なオブジェクトストアはトランザクション開始時に宣言したものに限られます。オブジェクトストアの取得はobjectStoreメソッドを使います。

objectStoreメソッド
  • 第一引数:オブジェクトストア名
// DBのオープン
const db = await idb.openDB('データベース名');
// トランザクション開始
const tx = db.transaction(['オブジェクトストア名'], 'トランザクションモード');
// オブジェクトストアの取得
const sampleStore = tx.objectStore('オブジェクトストア名');

ステップ4:データ操作(新規追加、更新、削除)

オブジェクトストアを取得したらいよいよデータ操作をします。新規追加はaddメソッド、更新はputメソッド、削除はdeleteメソッドを使います。

addメソッド
  • 第一引数:投入データ
  • 第二引数:主キーの値(アウトオブラインキーの場合)
  • 戻り値:主キー値に解決されるPromiseオブジェクト
// データの追加
await sampleStore.add(投入データ);
putメソッド
  • 第一引数:更新後のデータ
  • 第二引数:主キーの値(アウトオブラインキーの場合)
  • 戻り値:主キー値に解決されるPromiseオブジェクト

第二引数の主キーに該当するデータが存在する場合は更新し、存在しない場合は新規登録します。

// データの更新
await sampleStore.put(更新後のデータ);
deleteメソッド
  • 第一引数:削除対象の主キーの値
  • 戻り値:undefinedに解決されるPromiseオブジェクト
// データの削除
await sampleStore.delete(削除対象の主キーの値);

ステップ5:トランザクションの終了

トランザクションは自動的にコミットされます。いずれかの処理でエラーが発生した場合はロールバックされます。

サンプルコード全体像(indexeddb-sample/03.operate-data/app.js)

// オブジェクトストアの定義
await idb.openDB('sample-db', 1, {
    upgrade(db) {
        const sampleStore = db.createObjectStore('sampleStore', { keyPath: 'id' });
        sampleStore.createIndex('sampleIndex', 'name');
    },
});

// DBのオープン
const db = await idb.openDB('sample-db');
// トランザクション開始
const tx = db.transaction(['sampleStore'], 'readwrite');
// オブジェクトストアの取得
const sampleStore = tx.objectStore('sampleStore');
// データの追加
await sampleStore.add({ id: 1, name: 'ユーザー1' });
// データの更新
await sampleStore.put({ id: 1, name: 'ユーザー9' });
// データの削除
await sampleStore.delete(1);

データ操作はreadwriteモードで行います♪

データ検索のコード

まずポイントをチェック
  • ステップ1:データベースを開く
  • ステップ2:トランザクションを開始
  • ステップ3:オブジェクトストアを取得
  • ステップ4:インデックスを取得
  • ステップ5:データ検索
  • ステップ6:トランザクションの終了

データ検索のステップとコード例をご紹介します。ステップ3まではデータ操作とほとんど同じです。検索の場合はトランザクションモードにreadonlyを指定します。ここではidbライブラリを利用します。

ステップ1〜3:データ操作と同じ

トランザクションモードはreadonlyを指定します。

// DBのオープン
const db = await idb.openDB('データベース名');
// トランザクション開始
const tx = db.transaction(['オブジェクトストア名'], 'readonly');
// オブジェクトストアの取得
const sampleStore = tx.objectStore('オブジェクトストア名');

ステップ4:インデックスを取得

インデックスで検索をする場合は、オブジェクトストアからインデックスを取得します。インデックスの取得はindexメソッドを使います。

indexメソッド
  • 第一引数:インデックス名
  • 戻り値:インデックスを表すオブジェクト
// DBのオープン
const db = await idb.openDB('データベース名');
// トランザクション開始
const tx = db.transaction(['オブジェクトストア名'], 'readonly');
// オブジェクトストアの取得
const sampleStore = tx.objectStore('オブジェクトストア名');
// インデックスの取得
const nameIndex = sampleStore.index('インデックス名');

ステップ5:データ検索

データ検索はgetメソッドとgetAllメソッドを使います。getメソッドは最初に見つかった1件、getAllメソッドは該当するデータ全件を配列として取得します。オブジェクトストアとインデックスどちらからでも同じメソッドを使います。

getメソッド
  • 第一引数:検索条件
  • 戻り値:最初に見つかったデータ1件に解決されるPromiseオブジェクト
getAllメソッド
  • 第一引数:検索条件
  • 戻り値:該当するデータ全件の配列に解決されるPromiseオブジェクト
  • オブジェクトストアでgetメソッドを使う
  • 引数に主キーの値を指定
// 主キー検索
const pkResult = await sampleStore.get(1);
  • オブジェクトストアでgetAllメソッドを使う
  • 引数に何も指定しない
// 全件検索
const allReslut = await sampleStore.getAll();

オブジェクトストア内のデータは主キーの昇順で自動的に並び替えられています。

  • インデックスでgetメソッドを使う
  • 引数にインデックスの値を指定
  • 最初に見つかった1件のみ取得
  • インデックスがユニークな場合に便利
// インデックス検索(1件のみ)
const idxResult = await nameIndex.get('ユーザー1');
  • インデックスでgetAllメソッドを使う
  • 引数にインデックスの値を指定
// インデックス検索(複数件)
const idxMulti = await ageIndex.getAll(20);
  • インデックス宣言時にmultiEntryをtrueした場合、配列内の要素で検索可能
  • インデックスでgetAllメソッドを使う
  • 引数に配列の要素の値を指定
// インデックス検索(配列)
const arrResult = await groupIndex.getAll('A');
  • インデックスでgetAllメソッドを使う
    • オブジェクトストアの場合は主キーで範囲検索
  • 引数にIDBKeyRangeオブジェクトを指定
    • 下限、上限、範囲のいずれかの指定が可能
    • デフォルトでは以上、以下の指定
    • 追加でtrue, falseを指定することで未満、より大きいの指定が可能
// 下限指定
const rangeResult = await ageIndex.getAll(IDBKeyRange.lowerBound(25)); // 25以上
const rangeResult = await ageIndex.getAll(IDBKeyRange.lowerBound(25, true)); // 25より大きい
// 上限指定
const rangeResult = await ageIndex.getAll(IDBKeyRange.upperBound(25)); // 25以下
const rangeResult = await ageIndex.getAll(IDBKeyRange.upperBound(25, true)); // 25未満
// 範囲指定
const rangeResult = await ageIndex.getAll(IDBKeyRange.bound (30, 31)); // 30以上、31以下
const rangeResult = await ageIndex.getAll(IDBKeyRange.bound (30, 31, false, true)); // 30以上、31未満

カーソルでの全件検索

getAllではなくカーソルでも全件検索が可能です。カーソルはオブジェクトストア内の全てのデータを渡り歩き、該当するデータを逐次取得します。getAllメソッドは検索結果を配列で一括取得します。getAllメソッドの場合は一括で取得するためデータ量によってメモリ使用量が大きくなってしまいます。メモリ使用量を抑えたい場合や逐次的に処理をしたい場合は、カーソルを使います

openCursorメソッド
  • 第一引数:検索条件
    • 指定しない場合、全てのデータが対象になる
  • 戻り値:カーソルを表すオブジェクトに解決されるPromiseオブジェクト

continueメソッドを実行することで次のデータを処理することができます。

// カーソルでの全件検索
let cursor = await sampleStore.openCursor();
while (cursor) {
  console.log(cursor.key, cursor.value);
  cursor = await cursor.continue();
}

ステップ6:トランザクションの終了

トランザクションは自動的に終了します。

サンプルコード全体(indexeddb-sample/04.search-data/app.js)

// データベースの定義
await idb.openDB('sample-db', 1, {
    async upgrade(db) {
        const sampleStore = db.createObjectStore('sampleStore', { keyPath: 'id' });
        sampleStore.createIndex('nameIndex', 'name');
        sampleStore.createIndex('ageIndex', 'age');
        sampleStore.createIndex('groupIndex', 'group', { multiEntry: true });
        // 確認用データの追加
        const datas = [
            { id: 1, name: 'ユーザー1', age: 20, group: ['A', 'B'] },
            { id: 2, name: 'ユーザー2', age: 30, group: ['A', 'C'] },
            { id: 3, name: 'ユーザー3', age: 20, group: ['D', 'E'] },
        ];
        for (const data of datas) {
            await sampleStore.put(data);
        }
    },
});

// DBのオープン
const db = await idb.openDB('sample-db');
// トランザクション開始
const tx = db.transaction(['sampleStore'], 'readonly');
// オブジェクトストアの取得
const sampleStore = tx.objectStore('sampleStore');
// インデックスの取得
const nameIndex = sampleStore.index('nameIndex');
const ageIndex = sampleStore.index('ageIndex');
const groupIndex = sampleStore.index('groupIndex');

// 主キー検索
const pkResult = await sampleStore.get(1);
console.log(pkResult);

// 全件検索
const allReslut = await sampleStore.getAll();
console.log(allReslut);

// インデックス検索(1件のみ)
const idxResult = await nameIndex.get('ユーザー1');
console.log(idxResult);

// インデックス検索(複数件)
const idxMulti = await ageIndex.getAll(20);
console.log(idxMulti);

// インデックス検索(配列)
const arrResult = await groupIndex.getAll('A');
console.log(arrResult);

// 範囲検索
// 下限指定
const rangeResult = await ageIndex.getAll(IDBKeyRange.lowerBound(25));
// const rangeResult = await ageIndex.getAll(IDBKeyRange.lowerBound(25, true));
// 上限指定
// const rangeResult = await ageIndex.getAll(IDBKeyRange.upperBound(25));
// 範囲指定
// const rangeResult = await ageIndex.getAll(IDBKeyRange.bound (30, 31));
console.log(rangeResult);

// カーソルでの全件検索
let cursor = await sampleStore.openCursor();
while (cursor) {
  console.log(cursor.key, cursor.value);
  cursor = await cursor.continue();
}

オブジェクトストアとインデックスどちらでも同じメソッドで検索します♪

構造の更新

まずポイントをチェック
  • データベースの構造は変更可能
  • 旧バージョンのデータは引き継がれる
  • データベースのバージョン番号を上げて再定義
    • オブジェクトストアの追加、削除
    • インデックスの追加、削除

データベースの構造は変更することができます。アプリのバージョンアップに伴い、必要であればIndexedDBもバージョンアップします。なお、旧バージョン時に格納していたデータは引き継がれます

データベースの構造を変更するにはバージョン番号を現在よりも大きい数字で指定します。データベース新規作成時と同じopenDBメソッドを使います。引数で新しいバージョン番号を指定します。バージョンアップの具体的な処理をオブジェクトのupgragdeメソッドとして渡します。upgragdeメソッドは新しいバージョンを指定した際にも動作します

オブジェクトストアの削除はdeleteObjectStoreメソッドインデックスの削除はdeleteIndexメソッドを使います。既存のオブジェクトの取得はupgradeメソッドの引数のtransactionオブジェクトからobjectStoreメソッドを利用します。

サンプルコード抜粋(indexeddb-sample/05.update-db/app.js)

// データベース定義の更新
await idb.openDB('sample-db', 2, {
    async upgrade(db, oldVersion, newVersion, transaction) {
        // オブジェクトストアの追加
        db.createObjectStore('newStore', { keyPath: 'id' });
        // オブジェクトストアの削除
        db.deleteObjectStore('deleteStore');
        
        // 既存のオブジェクトストアの取得
        const sampleStore = transaction.objectStore('sampleStore');
        // インデックスの追加
        sampleStore.createIndex('newIndex', 'group');
        // インデックスの削除
        sampleStore.deleteIndex('deleteIndex');
    },
});

新しいバージョン番号を指定する以外は新規作成とほぼ同じです♪

生のAPIでの実装例

まずポイントをチェック
  • 生のIndexedDB APIはPromise対応していない
  • 検索結果などはイベントオブジェクトから受け取る

参考までにデータ検索の例を生のIndexedDB APIに置き換えた例を掲載します。生のAPIはイベント経由で各種操作を行います。そのためイベントリスナーの中にイベントリスナーを設定するといった入れ子の記述になっています。

データベースを開く
  • openメソッドを使う
  • upgradeneededイベントで構造定義を行う
    • event.target.resultからデータベースを表すオブジェクトを受け取る
// データベースの定義
const request = indexedDB.open('データベース名', バージョン番号);
request.onupgradeneeded = function (event) {
    const db = event.target.result;
    // オブジェクトストアの定義など
};
オブジェクトストアを定義する
  • upgradeneededイベント内で定義する
  • createObjectStoreメソッドを使う
// データベースの定義
const request = indexedDB.open('データベース名', バージョン番号);
request.onupgradeneeded = function (event) {
    const db = event.target.result;
    // オブジェクトストアの定義
    const sampleStore = db.createObjectStore('オブジェクトストア名', キーの設定);
};
インデックスを定義する
  • upgradeneededイベント内で定義する
  • createIndexメソッドを使う
// データベースの定義
const request = indexedDB.open('データベース名', バージョン番号);
request.onupgradeneeded = function (event) {
    const db = event.target.result;
    // オブジェクトストアの定義
    const sampleStore = db.createObjectStore('オブジェクトストア名', キーの設定);
    // インデックスの定義
    sampleStore.createIndex('インデックス名', 'キーパス');
};
データを追加する
  • addメソッドを使う
  • 実行完了時はsuccessイベントが発生する
// データベースの定義
const request = indexedDB.open('データベース名', バージョン番号);
request.onupgradeneeded = function (event) {
    const db = event.target.result;
    // オブジェクトストアの定義
    const sampleStore = db.createObjectStore('オブジェクトストア名', キーの設定);
    // インデックスの定義
    sampleStore.createIndex('インデックス名', 'キーパス');
    // データの追加
    const addRequest = sampleStore.add(data);
    addRequest.onsuccess = function (event) {
        console.log('データが追加されました');
    };
};
データを検索する
  • openメソッドのsuccessイベントで処理する
  • transactionメソッドでトランザクションを開始
  • objectStoreメソッドでオブジェクトストアを取得
  • indexメソッドでインデックスを取得
  • getメソッドの戻り値のオブジェクトでsuccessイベントが発生
    • event.target.resultから検索結果を受け取る
// データベースを開く
const request = indexedDB.open('データベース名', バージョン番号);
request.onsuccess = function (event) {
    const db = event.target.result;
    // トランザクション開始
    const tx = db.transaction(['オブジェクトストア名'], 'readonly');
    const sampleStore = tx.objectStore('オブジェクトストア名');
    const nameIndex = sampleStore.index('インデックス名');

    // 主キー検索
    const getRequest = sampleStore.get(主キーの値);
    getRequest.onsuccess = function (event) {
        const result = event.target.result;
        console.log(result);
    };
    getRequest.onerror = function (event) {
        console.error('データの取得中にエラーが発生しました', event.target.error);
    };

    
    // インデックス検索
    const idxGetRequest = nameIndex.get('インデックスの値');
    idxGetRequest.onsuccess = function (event) {
        const result = event.target.result;
        console.log(result);
    };
    idxGetRequest.onerror = function (event) {
        console.error('インデックス検索中にエラーが発生しました', event.target.error);
    };
};
カーソルでの検索
  • openCursorメソッドでカーソルでの検索を開始する
    • 戻り値のオブジェクトでsuccessイベントが発生する
    • event.target.resultから現在のカーソルを取得する
    • cursor.valueから検索結果を受け取る
    • cursor.continue();で次のデータを取得する
      • 次のデータがない場合、event.target.resulはnullになるので、if文で判断する
// カーソルでの全件検索
const cursorRequest = sampleStore.openCursor();
cursorRequest.onsuccess = function (event) {
    const cursor = event.target.result;
    if (cursor) {
        console.log(cursor.value);
        cursor.continue();
    }
};
cursorRequest.onerror = function (event) {
    console.error('カーソルでの全件検索中にエラーが発生しました', event.target.error);
};

サンプルコード全体像(indexeddb-sample/06.raw-api/app.js)

// データベースの定義
const request = indexedDB.open('sample-db', 1);
request.onupgradeneeded = function (event) {
    const db = event.target.result;
    const sampleStore = db.createObjectStore('sampleStore', { keyPath: 'id' });
    sampleStore.createIndex('nameIndex', 'name');
    sampleStore.createIndex('ageIndex', 'age');
    sampleStore.createIndex('groupIndex', 'group', { multiEntry: true });
    // 確認用データの追加
    const datas = [
        { id: 1, name: 'ユーザー1', age: 20, group: ['A', 'B'] },
        { id: 2, name: 'ユーザー2', age: 30, group: ['A', 'C'] },
        { id: 3, name: 'ユーザー3', age: 20, group: ['D', 'E'] },
    ];
    for (const data of datas) {
        const addRequest = sampleStore.add(data);
        addRequest.onsuccess = function (event) {
            console.log('データが追加されました');
        };
        addRequest.onerror = function (event) {
            console.error('データの追加中にエラーが発生しました', event.target.error);
        };
    }
};

request.onsuccess = function (event) {
    const db = event.target.result;
    // トランザクション開始
    const tx = db.transaction(['sampleStore'], 'readonly');
    const sampleStore = tx.objectStore('sampleStore');
    const nameIndex = sampleStore.index('nameIndex');
    const ageIndex = sampleStore.index('ageIndex');
    const groupIndex = sampleStore.index('groupIndex');

    // 主キー検索
    const getRequest = sampleStore.get(1);
    getRequest.onsuccess = function (event) {
        const result = event.target.result;
        console.log(result);
    };
    getRequest.onerror = function (event) {
        console.error('データの取得中にエラーが発生しました', event.target.error);
    };

    // 全件検索
    const getAllRequest = sampleStore.getAll();
    getAllRequest.onsuccess = function (event) {
        const result = event.target.result;
        console.log(result);
    };
    getAllRequest.onerror = function (event) {
        console.error('全件検索中にエラーが発生しました', event.target.error);
    };

    // インデックス検索(1件のみ)
    const idxGetRequest = nameIndex.get('ユーザー1');
    idxGetRequest.onsuccess = function (event) {
        const result = event.target.result;
        console.log(result);
    };
    idxGetRequest.onerror = function (event) {
        console.error('インデックス検索中にエラーが発生しました', event.target.error);
    };

    // インデックス検索(複数件)
    const idxGetAllRequest = ageIndex.getAll(20);
    idxGetAllRequest.onsuccess = function (event) {
        const result = event.target.result;
        console.log(result);
    };
    idxGetAllRequest.onerror = function (event) {
        console.error('インデックス検索中にエラーが発生しました', event.target.error);
    };

    // インデックス検索(配列)
    const arrayGetAllRequest = groupIndex.getAll('A');
    arrayGetAllRequest.onsuccess = function (event) {
        const result = event.target.result;
        console.log(result);
    };
    arrayGetAllRequest.onerror = function (event) {
        console.error('インデックス検索中にエラーが発生しました', event.target.error);
    };

    // 範囲検索
    const range = IDBKeyRange.lowerBound(25);
    const rangeGetAllRequest = ageIndex.getAll(range);
    rangeGetAllRequest.onsuccess = function (event) {
        const result = event.target.result;
        console.log(result);
    };
    rangeGetAllRequest.onerror = function (event) {
        console.error('範囲検索中にエラーが発生しました', event.target.error);
    };

    // カーソルでの全件検索
    const cursorRequest = sampleStore.openCursor();
    cursorRequest.onsuccess = function (event) {
        const cursor = event.target.result;
        if (cursor) {
            console.log(cursor.value);
            cursor.continue();
        }
    };
    cursorRequest.onerror = function (event) {
        console.error('カーソルでの全件検索中にエラーが発生しました', event.target.error);
    };
};
request.onerror = function (event) {
    console.error('データベースのオープン中にエラーが発生しました', event.target.error);
};

生のAPIはイベント経由で様々な処理を行います♪

IndexedDBのデバッグ

まずポイントをチェック
  • 今回はChromeのデベロッパーツールを紹介
  • 構造やデータの確認・削除が可能

IndexedDBのデバッグはブラウザのデベロッパーツールを利用します。いくつかの機能をご紹介します。今回はChromeを前提とします。

デベロッパーツールの開き方
  1. F12でデベロッパーツールを開く
    • その他のツール > 開発者ツール をクリックしてもOK
  2. Applicationタブを開く
  3. IndexedDB項目を開く
構造やデータの確認
  • 各要素を展開すると次の順番で表示される
    • データベース、オブジェクトストア、インデックス
  • 各要素を選択し中身を確認可能

オブジェクトストア内のデータやデータベースのバージョンなどを確認できます。

構造やデータの確認
  • データベースを選択し「データベースを削除」で削除可能
  • オブジェクトストアを右クリックで削除可能
  • データを右クリックで削除可能

開発時にデータベースを削除したいときに便利です。

データの中身や構造の確認ができます♪

おわりに

皆さん、お疲れ様でした。
ここまでご覧いただき、ありがとうございました。

IndexedDBについて確認をしていただきました。
独特な用語やデータ構造など特徴的な仕様が多数あります。
IndexedDBは目に見えにくくRDBとは違うので、少しイメージしづらいですが徐々に理解を深めていきましょう。

これからもプログラミング学習頑張りましょう♪

web.dev のリンク
サンプルコード

コメント

タイトルとURLをコピーしました