Closed20

IndexedDBについて

akiaki

IndexedDB

学習リソース
https://developer.mozilla.org/ja/docs/Web/API/IndexedDB_API/Basic_Concepts_Behind_IndexedDB
https://javascript.info/indexeddb
https://www.w3.org/TR/IndexedDB/

IndexedDBが利用される場面

  • 大量のデータを保存するアプリケーション
  • 動作するのに持続的なインターネット接続が不要なアプリケーション

TODOアプリや学習記録アプリなどは個人のブラウザ環境にデータがあれば良いのでIndexedDBを用いれば良さそう。ログイン処理が必要なくなる。

IndexedDBの概要

  • IndexedDBでは"キー"でインデックスされたオブジェクトを保存したり取り出したりすることができる。
  • IndexedDBに保存されたデータには同一ドメイン内からアクセス可能であるが、異なるドメインにまたがってデータへアクセスすることはできない
  • IndexedDB APIは非同期APIである
  • IndexedDBと競合する仕様であるWebSQLデータベースはW3Cが非推奨とした。
  • IndexedDBはリレーショナルデータベースではない

IndexedDBの重要な概念

  • キーと値のペアを保存する。値はオブジェクトを取ることができる。
  • IndexedDBはトランザクショナルデータベースモデルに基づいて構築されている。
  • IndexedDB APIは概ね非同期である。よって、APIはデータを戻り値として返却しない。同期的にデータベースに値を保存したり、値を取り出したりしない。その代わりに、データベースで実行する操作を要求する。
  • IndexedDBは結果が使用可能であることを通知するためにDOMイベントを使用する。

IndexedDBの用語

  • データベース。データベースは任意文字列の名称を持つ。名前は存続期間を通じて不変。
  • オブジェクトストア。オブジェクトストアはレコードを持続的に保持している。オブジェクトストア内のレコードは、キーによって昇順に保存されている。
  • トランザクション。書き込みのトランザクションのスコープが重ならない限り、一つのデータベース接続で同時に複数のアクティブなトランザクションが存在できる。トランザクションのスコープは生成時に定義される。トランザクションにはreadwritereadonlyversionchangeの3つのモードがある。
  • IndexedDBの全ての操作はトランザクション内で発生するのでトランザクションはIndexedDBにとって重要な概念である。

キー

  • キーレンジ。キーとして使用する何らかのデータ型の連続的な区間。キーやキーレンジを使用して、オブジェクトストアやインデックスからレコードを取り出すことができる。
akiaki

IndexedDBを使用する

学習リソース
https://developer.mozilla.org/ja/docs/Web/API/IndexedDB_API/Using_IndexedDB

  • IndexedDBはユーザーのブラウザー内にデータを永続的に保存する手段である。ネットワークの状態に関わらず高度な関わらず高度な問合せ機能を持つウェブアプリケーションを作成できる。

IndexedDB実装の基本パターン

  1. データベースを開く
  2. データベース内にオブジェクトストアを作成する
  3. データの追加や取り出しといった、データベース操作のトランザクションを開始し、リクエストを行う。
  4. DOMイベントを受け取ることにより操作が完了するのを待つ
  5. 結果に応じた処理を行う

データベースを開く

const request = window.indexedDB.open("MyTestDatabase", 3);

データベースを開く操作も他の操作と同様にリクエストが必要になる。
openメソッドはIDBOpenDBRequestオブジェクトを返す。

リクエストを生成するコードの後にはリクエストした操作が成功したときとエラーとなったときのハンドラを登録するコードを書くと良い。

リクエストが成功するとtypeプロパティが"success"であるDOMイベントが、requestをターゲットとして発生する。イベントが発生するとrequestonsuccess()関数がsuccessイベントを引数として呼び出される。

errorの場合はエラーイベントがrequestに発生し、onerror()関数が呼び出される。

akiaki

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;
};

上のコードはエラーが出る。なぜかよくわからない。

参考

https://github.com/microsoft/TypeScript/issues/28293

akiaki

解決策

  • 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;
};
akiaki

改善・変更

  1. db変数のスコープをグローバルから移動
  2. データベースのインスタンス(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;
};
SNKWSNKW

この型エラーですが、下の書き方で防げることが確認できました

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)
}
akiaki

データベース の作成の仕方

ポイント

  • データベースが存在しないときにopenのリクエストを行うと直ちにonupgradeneededイベントがrequestに発生する。
  • onupgradeneededイベントのイベントハンドラーにおいてのみ、データベースの構造を変更することができる。つまり、オブジェクトストアの作成や削除、インデックスの構築や削除が可能である。
  • オブジェクトストアはcreateObjectStore()を一回呼び出して作成する。
  const objectStore = db.createObjectStore("customers", { keyPath: "ssn" });
  • オブジェクトストアはRDBSにおけるテーブルみたいなもの?
  • createObjectStoreでオブジェクトストアの名前と、ストア内で個々のオブジェクトを一意にするプロパティを定義している。
akiaki

補足

IndexedDB はテーブルではなくオブジェクトストアを使用しており、ひとつのデータベースに複数のオブジェクトストアを含めることができます。

  • オブジェクトストアはテーブルに相当する
  • オブジェクトストアは1つのデータベースの中で複数作成できる
akiaki

データの追加、削除、更新

  • データベースに対する操作を行う前にトランザクションを開始する必要がある
  • トランザクションの中ではオブジェクトストアへのアクセスやリクエストが可能になる

データベース操作の手順

  1. トランザクションの開始
  2. 操作対象にするオブジェクトストアの指定
  3. モードの選択(readwritereadonlyversionchange
  • データベースの構造を変化させるにはversionchangeモードでトランザクションを開く必要がある。つまり、オブジェクトストアやインデックスを削除するときはversionchangeモードでトランザクションを開く必要がある。

  • versionchangeモードのトランザクションはIDBFactory.openメソッドによって開く

  • オブジェクトストアからデータを読み出すときはreadwritereadonlyでトランザクションを開く

  • これらのモードはIDBDabe.transactioinメソッドで開く。操作対象には複数のオブジェクトストアを選択できる(オブジェクトストアを選択するというのはトランザクションのスコープを決めるということ)

akiaki

トランザクションとイベントループ

  • イベントループが何かよくわからない
  • トランザクションが生成されてから、何のリクエストもしないでいればそのトランザクションは非アクティブ状態になる。
  • アクティブ状態を保ち続けるためにはそのトランザクションでリクエストを行えば良い。
// 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);
};
akiaki

カーソル

  • カーソルを使用するとオブジェクトストアないのレコードに対して反復処理ができる
  • 取り出した値を配列に格納するのに使われることが多い
akiaki

インデックスの使用

  • 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);
};
  • インデックスでは二つの種類のカーソルを開くことができる。
  • ノーマルカーソルのvaluecursor.value)はレコード(つまりオブジェクト)全体が格納されている
  • キーカーソルのvaluecursor.value)はオブジェクトストアのキー(記事の例ではssnの値)が格納されている
akiaki

確認問題

  1. IndexedDBはどのようなときに利用するか?
  2. データベースはどのように開くのか?
  3. データベースの構造を決めるタイミングはどこに限定されているか?
  4. onupgrageneededイベントはいつ発生するか?
akiaki
  1. 継続的なインターネット接続が不要な大量のデータを扱うwebアプリを作ることに向いている。
  2. const request = window.indexedDB.open("MyTestDatabase", 3);
  3. IDBOpenDBRequestが持つonupgradeneededに登録するイベントハンドラーの中の処理でデータベースの構造を決める。(オブジェクトストアを作成する)
  4. onupgrageneededイベントは新しいデータベースを作成したり、データベースのバージョンが更新されたときに発生する。
akiaki

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

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

更新処理

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("更新処理");
  };
};
akiaki

createIndex

  • objectStore.createIndex()upgradeneededで行わなければならない
このスクラップは2022/08/08にクローズされました