皆さんこんにちは、ChanCodeです。
今回はIndexedDBをご紹介します。
IndedxedDBはブラウザに大量データを保存し、検索することができる仕組みです。今回はIndexedDBの用語や基本的な使い方をについて扱います。
- IndexedDBの概要が知りたい
- IndexedDBの書き方が知りたい
初めて学習する方にも分かるように、要点を絞って丁寧に解説していきます。
IndexedDBの全体像をご紹介します♪
IndexedDBとは
- ブラウザで動作するオブジェクト指向データベース
- 大量データの保存に対応
- インデックスでの効率的な検索が可能
- 非同期APIなのでメインスレッドをブロックしない
- トランザクションをサポート
IndexedDBとはブラウザで動作するオブジェクト指向データベースです。大量データの保存が可能であり、本格的なオフラインアプリケーションやPWAなどに向いています。
保存可能なデータは構造化複製アルゴリズムに対応したデータです。DOMノードや関数以外はほとんど格納可能です。
IndexedDB APIは非同期で動作するためメインスレッドをブロック操作をしません。また、全ての操作はトランザクション内で実行されデータの整合性が保証されます。
データの検索はキーだけでなくインデックスという仕組みにより、単純なキーバリュー形式よりも、柔軟な検索が可能です。インデックスはキー以外の項目で検索する機能です。
ブラウザで大量データの保存と効率的な検索ができます♪
用語と構造
- データベース:最上位の構成要素
- オブジェクトストア:データを格納する場所
- インデックス:データ検索に使用する索引
- キー:データを一意に識別するための値
- インラインキー:格納するデータ自身に含まれる項目をキーとする
- アウトオブラインキー:格納するデータとは別に与えるキー
IndexedDBの構成と各用語をご紹介します。RDBとは異なる独特な構成をしています。
データベース:最上位の構成要素
データベースとはIndexedDBのトップレベルに位置する要素です。この中にはオブジェクトストアが含まれています。データベースは名前で管理されており、複数のデータベースを用意することも可能です。
オブジェクトストア:データを格納する場所
オブジェクトストアとは実際にデータを格納する場所です。個別のデータを保存し、各データはキーによって一意に識別されます。
オブジェクトストアはRDBにおけるテーブルのような存在です。テーブルと同じように格納するデータの種類に応じて複数のオブジェクトストアを作成します。
例えば、ユーザー情報とタスク情報を管理したいアプリの場合、ユーザー用のオブジェクトストアとタスク用のオブジェクトストアを用意します。
インデックス:データ検索に使用する索引
インデックスはデータ検索時に利用されます。インデックスは項目をもとに作成し、その項目に基づいた検索を行うことができます。なおインデックスの作成は必須ではありません。
例えば、ユーザー情報としてユーザー名やメールアドレスを保存している場合、ユーザー名インデックスやメールアドレスインデックスのように、各項目のインデックスを作成することで各項目の値で検索することができます。
キー:データを一意に識別するための値
キーとはオブジェクトストア内のデータを一意に識別するための値です。キーは2種類ありインラインキーとアウトオブラインキーと言います。
インラインキーとは、格納するデータ自身に含まれる項目を利用したキーです。どの項目をキーとするかオブジェクトストア作成時に指定します。アウトオブラインキーとは、格納するデータとは別に与えるキーです。
キーはどちらの方式を選んでも機能としての違いはありません。管理しやすい方式を選んでOKです。
IndexedDBはキーとインデックスで検索可能です♪
利用ステップの全体像
- 定義
- データベースを開く
- オブジェクトストアを作成
- インデックスを作成
- 操作
- トランザクションの開始
- 操作対象のオブジェクトストアの取得
- データの操作
- トランザクションの終了
IndexedDBを利用する際の流れをご紹介します。ステップは大きく「定義」と「操作」に分かれます。
定義ステップ1:データベースを開く
まずはデータベースを開きます。データベースを開く際は名前とバージョン番号を指定します。
定義ステップ2:オブジェクトストアを作成
データベースにオブジェクトストアを作成します。作成時に名前とキーの設定を指定します。オブジェクトストアはデータの種類に応じて複数用意可能です。
キーはインラインキーとアウトオブラインキーのどちらかの方式を選びます。インラインキーの場合どの項目をキーとするかを指定します。この時使う値をキーパスと言います。インラインキーとアウトオブラインキーのどちらもキージェネレーターの利用を選ぶことができます。キージェネレーターはキーの値を自動採番する仕組みです。
定義ステップ3:インデックスを作成
オブジェクトストアに紐づくインデックスを作成します。インデックスはオブジェクトストアごとに複数作成することができます。インデックスで利用する項目はキーパスで指定します。項目が配列の場合、オプションで配列の各要素に対して独立した検索をするかを指定します。なおインデックスは必須ではありません。
ここまでが構造の定義です♪
操作ステップ1:トランザクションの開始
データ操作はトランザクションの中で行うので、まずはトランザクションを開始します。トランザクションの開始時にこれから利用するオブジェクトストアとトランザクションのモードを指定します。トランザクションモードは「参照のみ」と「読み書き」の2種類があります。
操作ステップ2:操作対象のオブジェクトストアの取得
トランザクションを開始したらオブジェクトストアを取得します。利用可能なオブジェクトストアはトランザクション開始時に宣言したもののみです。
操作ステップ3:データの操作
オブジェクトストアを使った処理を行います。操作内容はトランザクション開始時に指定したモードに従います。「参照のみ」の場合はキーやインデックスでの検索を行います。「読み書き」の場合はオブジェクトストアの更新ができます。
操作ステップ4:トランザクションの終了
トランザクションは自動的にコミットされます。全ての操作が完了するとコミットされ、いずれかの操作が失敗した場合、ロールバックされます。
定義は最初のみで、それ以降は操作を行います♪
検索方法
- 主キー検索
- 全件検索
- インデックスでの検索
- 範囲検索
IndexedDBで利用可能な検索方法をご紹介します。IndexedDBの検索は大きく分けて4種類あります。
主キー検索
主キー検索はキーを指定した1件検索です。インラインキーとアウトオブラインキーのどちらにしてもキーは一意であるため、検索結果は1件になります。
全件検索
キーを指定しない場合、オブジェクトストア内の全てのデータを取得します。
インデックスでの検索
インデックスでの検索は検索結果が複数になる可能性があります。インデックス項目にユニーク制約がついている場合は検索結果は1件になります。
範囲検索
検索する値の範囲を指定することもできます。主キーやインデックスが数値の場合に利用します。値が指定の範囲内か、指定の値以上か、などの範囲指定ができます。
条件指定は主キーかインデックスを使って行います♪
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' });
IndexedDB APIはPromise対応していないので、ライブラリを使います♪
構造定義のコード
- ステップ1:データベースを開く
- ステップ2:オブジェクトストアを定義
- ステップ3:インデックスを定義
構造定義のステップとコード例をご紹介します。ここではidbライブラリを利用します。
ステップ1:データベースを開く
まずはデータベースを開きます。idbライブラリのopenDBメソッドを使います。
ステップ2:オブジェクトストアを定義
データベースを開いた際にオブジェクトストアを定義します。オブジェクトストアの定義はupgradeメソッド内でcreateObjectStoreメソッドを使います。upgradeメソッドは引数にデータベースを表すオブジェクトを受け取ります。このオブジェクトからcreateObjectStoreメソッドを呼び出します。
ステップ3:インデックスを定義
オブジェクトストアに対してインデックスを定義することができます。インデックスの定義はupgradeメソッド内でcreateIndexメソッドを使います。
サンプルコード全体像(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を指定します。
ステップ3:オブジェクトストアを取得
トランザクションを開始したらオブジェクトストアを取得します。利用可能なオブジェクトストアはトランザクション開始時に宣言したものに限られます。オブジェクトストアの取得はobjectStoreメソッドを使います。
ステップ4:データ操作(新規追加、更新、削除)
オブジェクトストアを取得したらいよいよデータ操作をします。新規追加はaddメソッド、更新はputメソッド、削除は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メソッドを使います。
ステップ5:データ検索
データ検索はgetメソッドとgetAllメソッドを使います。getメソッドは最初に見つかった1件、getAllメソッドは該当するデータ全件を配列として取得します。オブジェクトストアとインデックスどちらからでも同じメソッドを使います。
ステップ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はイベント経由で各種操作を行います。そのためイベントリスナーの中にイベントリスナーを設定するといった入れ子の記述になっています。
サンプルコード全体像(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を前提とします。
データの中身や構造の確認ができます♪
おわりに
皆さん、お疲れ様でした。
ここまでご覧いただき、ありがとうございました。
IndexedDBについて確認をしていただきました。
独特な用語やデータ構造など特徴的な仕様が多数あります。
IndexedDBは目に見えにくくRDBとは違うので、少しイメージしづらいですが徐々に理解を深めていきましょう。
これからもプログラミング学習頑張りましょう♪
- IndexedDB API:https://developer.mozilla.org/ja/docs/Web/API/IndexedDB_API
- IndexedDB の主な特徴と基本用語:https://developer.mozilla.org/ja/docs/Web/API/IndexedDB_API/Basic_Terminology
- IndexedDB の使用:https://developer.mozilla.org/ja/docs/Web/API/IndexedDB_API/Using_IndexedDB
- IDBTransaction:https://developer.mozilla.org/ja/docs/Web/API/IDBTransaction
- IDBKeyRange:https://developer.mozilla.org/ja/docs/Web/API/IDBKeyRange
- IDBCursor:https://developer.mozilla.org/ja/docs/Web/API/IDBCursor
- IDBDatabase.deleteObjectStore():https://developer.mozilla.org/ja/docs/Web/API/IDBDatabase/deleteObjectStore
- IDBObjectStore: deleteIndex():https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/deleteIndex
- Working with IndexedDB:https://web.dev/indexeddb/
- Best Practices for Using IndexedDB:https://web.dev/indexeddb-best-practices/
- IndexedDB:https://web.dev/learn/pwa/offline-data/#indexeddb
- ウェブ用ストレージ:https://web.dev/storage-for-the-web/
- IndexedDB:https://ja.javascript.info/indexeddb
- View and change IndexedDB data:https://developer.chrome.com/docs/devtools/storage/indexeddb/?utm_source=devtools
- indexeddb-sample(ChanCode):https://github.com/ChanCode-Sample/JavaScript-API/tree/main/indexeddb-sample
- Live Serverで起動してください
- idb library:https://github.com/jakearchibald/idb
コメント