IndexedDBについて
IndexedDB
学習リソース
IndexedDBが利用される場面
- 大量のデータを保存するアプリケーション
- 動作するのに持続的なインターネット接続が不要なアプリケーション
TODOアプリや学習記録アプリなどは個人のブラウザ環境にデータがあれば良いのでIndexedDBを用いれば良さそう。ログイン処理が必要なくなる。
IndexedDBの概要
- IndexedDBでは"キー"でインデックスされたオブジェクトを保存したり取り出したりすることができる。
- IndexedDBに保存されたデータには同一ドメイン内からアクセス可能であるが、異なるドメインにまたがってデータへアクセスすることはできない
- IndexedDB APIは非同期APIである
- IndexedDBと競合する仕様であるWebSQLデータベースはW3Cが非推奨とした。
- IndexedDBはリレーショナルデータベースではない
IndexedDBの重要な概念
- キーと値のペアを保存する。値はオブジェクトを取ることができる。
- IndexedDBはトランザクショナルデータベースモデルに基づいて構築されている。
- IndexedDB APIは概ね非同期である。よって、APIはデータを戻り値として返却しない。同期的にデータベースに値を保存したり、値を取り出したりしない。その代わりに、データベースで実行する操作を要求する。
- IndexedDBは結果が使用可能であることを通知するためにDOMイベントを使用する。
IndexedDBの用語
- データベース。データベースは任意文字列の名称を持つ。名前は存続期間を通じて不変。
- オブジェクトストア。オブジェクトストアはレコードを持続的に保持している。オブジェクトストア内のレコードは、キーによって昇順に保存されている。
- トランザクション。書き込みのトランザクションのスコープが重ならない限り、一つのデータベース接続で同時に複数のアクティブなトランザクションが存在できる。トランザクションのスコープは生成時に定義される。トランザクションには
readwrite
、readonly
、versionchange
の3つのモードがある。 - IndexedDBの全ての操作はトランザクション内で発生するのでトランザクションはIndexedDBにとって重要な概念である。
キー
- キーレンジ。キーとして使用する何らかのデータ型の連続的な区間。キーやキーレンジを使用して、オブジェクトストアやインデックスからレコードを取り出すことができる。
IndexedDBを使用する
学習リソース
- IndexedDBはユーザーのブラウザー内にデータを永続的に保存する手段である。ネットワークの状態に関わらず高度な関わらず高度な問合せ機能を持つウェブアプリケーションを作成できる。
IndexedDB実装の基本パターン
- データベースを開く
- データベース内にオブジェクトストアを作成する
- データの追加や取り出しといった、データベース操作のトランザクションを開始し、リクエストを行う。
- DOMイベントを受け取ることにより操作が完了するのを待つ
- 結果に応じた処理を行う
データベースを開く
const request = window.indexedDB.open("MyTestDatabase", 3);
データベースを開く操作も他の操作と同様にリクエストが必要になる。
openメソッドはIDBOpenDBRequest
オブジェクトを返す。
リクエストを生成するコードの後にはリクエストした操作が成功したときとエラーとなったときのハンドラを登録するコードを書くと良い。
リクエストが成功するとtypeプロパティが"success"であるDOMイベントが、requestをターゲットとして発生する。イベントが発生するとrequest
のonsuccess()
関数がsuccessイベントを引数として呼び出される。
errorの場合はエラーイベントがrequestに発生し、onerror()
関数が呼び出される。
TypeScriptでIndexedDBを開いてエラーと成功時のハンドラーを書く
let db;
const request = window.indexedDB.open("MyTestDatabase", 3);
request.onerror = (e) => {
alert("IndexedDB can't be used.")
};
request.onsuccess = (e: ) => {
db = e.target.result;
};
上のコードはエラーが出る。なぜかよくわからない。
参考
解決策
-
e.target
に対して型アサーションを使う(よくなさそう)
let db;
const request = window.indexedDB.open("MyTestDatabase", 3);
request.onerror = (e) => {
alert("IndexedDB can't be used.")
};
request.onsuccess = (e) => {
db = (e.target as IDBOpenDBRequest).result;
};
改善・変更
-
db
変数のスコープをグローバルから移動 - データベースのインスタンス(
IDBDatabase
オブジェクト)を得る際にrequest.resultを使用
const request = window.indexedDB.open("MyTestDatabase", 3);
request.onerror = (e) => {
alert("IndexedDB can't be used.")
};
request.onsuccess = (e) => {
const db = request.result;
};
この型エラーですが、下の書き方で防げることが確認できました
request.onsuccess = function() {
db = this.result;
}
アロー関数を使いたい・thisは極力控えたいということであればtry~catchで囲んで処理したほうがスマートな気がします
let db:IDBDatabase
try{
const request = window.indexedDB.open("MyTestDatabase", 3)
db = request.result
}catch(err){
console.error(err)
}
データベース の作成の仕方
ポイント
- データベースが存在しないときにopenのリクエストを行うと直ちに
onupgradeneeded
イベントがrequestに発生する。 -
onupgradeneeded
イベントのイベントハンドラーにおいてのみ、データベースの構造を変更することができる。つまり、オブジェクトストアの作成や削除、インデックスの構築や削除が可能である。 - オブジェクトストアは
createObjectStore()
を一回呼び出して作成する。
const objectStore = db.createObjectStore("customers", { keyPath: "ssn" });
- オブジェクトストアはRDBSにおけるテーブルみたいなもの?
-
createObjectStore
でオブジェクトストアの名前と、ストア内で個々のオブジェクトを一意にするプロパティを定義している。
補足
IndexedDB はテーブルではなくオブジェクトストアを使用しており、ひとつのデータベースに複数のオブジェクトストアを含めることができます。
- オブジェクトストアはテーブルに相当する
- オブジェクトストアは1つのデータベースの中で複数作成できる
データの追加、削除、更新
- データベースに対する操作を行う前にトランザクションを開始する必要がある
- トランザクションの中ではオブジェクトストアへのアクセスやリクエストが可能になる
データベース操作の手順
- トランザクションの開始
- 操作対象にするオブジェクトストアの指定
- モードの選択(
readwrite
、readonly
、versionchange
)
-
データベースの構造を変化させるには
versionchange
モードでトランザクションを開く必要がある。つまり、オブジェクトストアやインデックスを削除するときはversionchange
モードでトランザクションを開く必要がある。 -
versionchange
モードのトランザクションはIDBFactory.open
メソッドによって開く -
オブジェクトストアからデータを読み出すときは
readwrite
かreadonly
でトランザクションを開く -
これらのモードは
IDBDabe.transactioin
メソッドで開く。操作対象には複数のオブジェクトストアを選択できる(オブジェクトストアを選択するというのはトランザクションのスコープを決めるということ)
トランザクションとイベントループ
- イベントループが何かよくわからない
- トランザクションが生成されてから、何のリクエストもしないでいればそのトランザクションは非アクティブ状態になる。
- アクティブ状態を保ち続けるためにはそのトランザクションでリクエストを行えば良い。
// customersオブジェクトストアを対象とするトランザクションを開始する
// トランザクションの対象とするオブジェクトストアは複数指定できるが、ここでは一つだけを指定している
const transaction = db.transaction(["customers"]);
// トランザクションの対象として選択したオブジェクトストアの中からリクエストを行いたいオブジェクトストアを取り出す
const objectStore = transaction.objectStore("customers");
// "444-44-4444"は`keypath`として指定したオブジェクトストアで一意なキー
// 以下のコードはオブジェクトストアから、キー"444-44-4444"に対応するレコード(?)を取り出すリクエストを行っている
const request = objectStore.get("444-44-4444");
request.onerror = function(event) {
// エラー処理!
};
request.onsuccess = function(event) {
// request.result に対して行う処理!
alert("Name for SSN 444-44-4444 is " + request.result.name);
};
カーソル
- カーソルを使用するとオブジェクトストアないのレコードに対して反復処理ができる
- 取り出した値を配列に格納するのに使われることが多い
インデックスの使用
-
keypath
として指定したキーは一意であるのですぐに対象のレコードを取り出すことができる - 一方で
name
で顧客を検索しなければならない場合は対象のレコードが見つかるまで全てのレコードを反復しなければならない。 - 上のような問題はインデックスを用いると解消される。
//インデックスを`name`で作成する
const index = objectStore.index("name");
// "Donna"に一致するnameの値をもつ最もキーの値が小さいレコードが取り出される
// "Donna"に一致する全てのレコードを取り出すにはカーソルを用いた反復を行わなければならない
index.get("Donna").onsuccess = function(event) {
alert("Donna's SSN is " + event.target.result.ssn);
};
- インデックスでは二つの種類のカーソルを開くことができる。
- ノーマルカーソルの
value
(cursor.value
)はレコード(つまりオブジェクト)全体が格納されている - キーカーソルの
value
(cursor.value
)はオブジェクトストアのキー(記事の例ではssnの値)が格納されている
確認問題
- IndexedDBはどのようなときに利用するか?
- データベースはどのように開くのか?
- データベースの構造を決めるタイミングはどこに限定されているか?
-
onupgrageneeded
イベントはいつ発生するか?
- 継続的なインターネット接続が不要な大量のデータを扱うwebアプリを作ることに向いている。
const request = window.indexedDB.open("MyTestDatabase", 3);
-
IDBOpenDBRequest
が持つonupgradeneeded
に登録するイベントハンドラーの中の処理でデータベースの構造を決める。(オブジェクトストアを作成する) -
onupgrageneeded
イベントは新しいデータベースを作成したり、データベースのバージョンが更新されたときに発生する。
DBの初期化処理
エラーハンドリングは未実装
const data = [
{ id: 3, title: "食パン買ってくる" },
{ id: 4, title: "牛乳買ってくる" },
];
let db: IDBDatabase;
const initDB = () => {
const openReq = indexedDB.open("todos", 1);
openReq.onerror = () => {
};
openReq.onsuccess = () => {
};
openReq.onupgradeneeded = (e) => {
db = (e.target as IDBOpenDBRequest).result;
// オブジェクトストアの作成
const objStore = db.createObjectStore("todo", {
keyPath: "id",
autoIncrement: true,
});
// オブジェクトストアへのデータ書き込み操作
// オブジェクトストアの作成は非同期なので作成の完了を待たなければならない
objStore.transaction.oncomplete = () => {
const trans = db.transaction("todo", "readwrite");
const todoObjStore = trans.objectStore("todo");
data.forEach((item) => {
todoObjStore.add(item);
});
};
};
};
initDB();
DBの初期化、データの追加
- エラーハンドリングを書いていないのでよくないと思う
- insert関数実行時"todo"のオブジェクトストアが作成されている保証はどこにあるのかがよくわからない。
- 型アノテーションを用いておりよくない
type TodoTitle = {
title: string;
}
const data: TodoTitle[] = [
{ title: "食パン買ってくる" },
{ title: "牛乳買ってくる" },
];
const init = () => {
const openReq = indexedDB.open("todos", 1);
openReq.onerror = () => {
// エラー処理
};
openReq.onsuccess = () => {
};
// データベースが存在しないときにしか走らない処理
openReq.onupgradeneeded = (e) => {
const db = (e.target as IDBOpenDBRequest).result;
// オブジェクトストアの作成
const objStore = db.createObjectStore("todo", {
keyPath: "id",
autoIncrement: true,
});
};
};
const insert = (data: TodoTitle[]) => {
// データベースを開く
const openReq = indexedDB.open("todos", 1);
openReq.onsuccess = (e) => {
const db = (e.target as IDBOpenDBRequest).result;
const trans = db.transaction("todo", "readwrite");
const objStore = trans.objectStore("todo");
data.forEach((item) => {
objStore.add(item);
});
};
};
init();
// insert関数実行時"todo"のオブジェクトストアが作成されている保証はどこにあるのか?
insert(data);
更新処理
const update = () => {
const openReq = indexedDB.open("todos");
openReq.onsuccess = () => {
// openReqのリザルトにデータベースが格納されているのでこれを使っても良い(あとで仕様書を確認すべき)
const db = openReq.result;
const trans = db.transaction("todo", "readwrite");
const objStore = trans.objectStore("todo");
//idが3のレコードを更新する
objStore.put({ id: 3, title: "更新されました"});
console.log("更新処理");
};
};
課題
- データベースの各操作(open、get、add、put、deleteなど)をPromiseでラップせよ。
- TODOアプリに検索機能をつけよ。
- TODOアプリにソート機能をつけよ。
TODOアプリ
createIndex
-
objectStore.createIndex()
はupgradeneeded
で行わなければならない
こちらの研究大変参考になりました!
共有ありがとうございます!!