kostumブログ

勉強したことやノート代わりのアウトプットに使っています。

IndexedDB APIを知る② ~ IndexedDBを使う ~

前の記事

前回記事はこちら

kostum.hatenablog.jp

基本パターン

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

ストアを作成および構築する

  • 接頭辞を使用している実装は不具合がある、未完成、古い版の仕様に従っている可能性があるため、製品版で使用することは推奨されない。対応しているものとするより、未対応とする方が良い。
if (!window.indexedDB) {
    console.log("このブラウザは安定版のIndexedDB を対応していません。")
}

データベースを開く

// データベースは開く
var request = window.indexedDB.open("MyTestDatabase", 3);
  • データベースを開くリクエストは、すぐにはデータベースを開いたりトランザクションを開始したりはしない。
  • open()関数を呼び出すと、結果(成功)またはイベントとして扱うエラー値を伴うIDBDatabaseオブジェクトを返す。

IDBFactory.open - Web API | MDN

ハンドラーの生成

  • 全てが成功すると、typeプロパティが”success"であるDOMイベントが、requesttargetとして発生する。イベントが発生するとrequestonsuccess()関数が、successイベントを引数として呼び出される。
  • 何らかの問題がある場合、typeプロパティが”error"であるDOMイベントがrequestで発生する。これは、エラーイベントを引数としてonerror()関数を呼び出す。
  • IndexedDB APIはエラー処理を最小限にするよう設計されているが、データベースを開く場合、データベースを作成する許可をユーザーがウェブアプリに与えていない場合が多い。
  • ブラウザはwebアプリが初めてストレージ用にIndexedDBを開こうとした時に、ユーザーへプロンプトを表示する。ユーザーはアクセスを許可または否定できる。

データベースを作成またはデータベースのバージョンを更新する

  • 新しいデータベースを作成したり既存のデータベースのバージョンを更新したりすると、onupgradeneededイベントが発生して、request.resultに設定したonversionchangeイベントハンドラーにIDBVersionChangeEventオブジェクトが渡される。
  • upgradeneededイベントのハンドラーでは、このバージョンのデータベースで必要なオブジェクトストアを作成する。
  • onupgradeneededイベントから正常に抜けた場合は、データベースを開くリクエストのonsuccessハンドラーが実行される。

データベースを構築する

  • 値をオブジェクトストアへ保存するたびに、値がキーと関連づけられる。オブジェクトストアでキーパス(オブジェクトストアやインデックスのどこからブラウザがキーを取り出すべきかを定義する)を使用するかキージェネレータ(指定した順序で新たなキーを背制する仕組み)を使用するかに応じて、キーを供給する方法がいくつか存在する。
  • オブジェクトストアがプリミティブではなくオブジェクトを保持していれば、オブジェクトストアでインデックスを作成することもできる。
  • インデックスは、オブジェクトのキーではなく保存されたオブジェクトのプロパティの値を使用して、オブジェクトストア内に保存された値を検索することを可能にする。
  • さらにインデックスには、保存されたデータに単純な制限を強制する機能がある。
// データを保存例

const dbName = "the_name";

var request = indexedDB.open(dbName, 2);

request.onerror = function(event) {
  // エラー処理
};
request.onupgradeneeded = function(event) {
  var db = event.target.result;

  // 顧客の情報を保存する objectStore を作成する。
  // "ssn" は一意であることが保証されているので、キーパスとして使用する。
    // 第一引数:ストアの名前
    // 第二引数:引数オブジェクト(省略可能)。
    //   keyPath:ストア内で個々のオブジェクトを一意にするプロパティ
  var objectStore = db.createObjectStore("customers", { keyPath: "ssn" });

  // 顧客を名前で検索するためのインデックスを作成。
  // 重複する可能性があるので、一意のインデックスとしては使用できない。
  objectStore.createIndex("name", "name", { unique: false });

  // 顧客をメールアドレスで検索するためのインデックスを作成します。2 人の顧客が同じメールアドレスを
  // 使用しないようにしたいので、一意のインデックスを使用します。
  objectStore.createIndex("email", "email", { unique: true });

  // データを追加する前に objectStore の作成を完了させるため、
  // transaction oncomplete を使用します。
  objectStore.transaction.oncomplete = function(event) {
    // 新たに作成した objectStore に値を保存します。
    var customerObjectStore = db.transaction("customers", "readwrite").objectStore("customers");
    customerData.forEach(function(customer) {
      customerObjectStore.add(customer);
    });
  };
};
  • onupgradeneededはデータベースの構造を変えることができる唯一の場所で、オブジェクトストアの作成や削除、インデックスの構築および削除が可能。

キージェネレータを使用する

  • オブジェクトストアを作成するときにautoIncrementフラグを設定すると、そのオブジェクトストアでキージェネレータを使用できる。
  • キージェネレータを使用すると、オブジェクトストアに値を追加するのに応じて自動的にキーが生成される。
  • データベースの操作が取り消された場合を除いて、キージェネレーターの現在の値が減少することはない。

データの追加、読み取り、削除

  • データベースで何かを行えるようにする前に、トランザクションを開始しなければならない。
  • トランザクションはデータベースオブジェクトから生じており、トランザクションの対象にしたいオブジェクトストアを指定しなければならない。
  • トランザクションは、readonly, readwrite, versionchangeの3つのモードを使用できる。
    • データベースの「スキーマ」や構造を変更するには、versionchangeモードにしなければならない。このトランザクションは、versionを指定してIDBFactory.openメソッドを呼び出すことによって開く。
    • 既存のオブジェクトストアからレコードを読み出すには、トランザクションreadolyモードまたはreadwriteモードを使用できる。
    • 変更処理を行うには、readwriteモードにしなければならない。
    • このようなトランザクションは、IDBDatabase.transactionで開き、第一引数はstoreNames(アクセスしたいオブジェクトストアの配列で定義されるスコープ)で第二引数はトランザクションmodereadonlyまたはreadwrite)。
    • 適切なトランザクションのスコープとモードによって、データアクセスを高速化できる。
      • スコープを定義するときは、必要なオブジェクトストアのみ指定する。これにより、同時にスコープが重なり合うことなく複数のトランザクションを実行できる。
      • readwriteモードは、必要な場合に限り限定する。readwriteトランザクションはオブジェクトストアに対して1個しか実行できない。

データベースにデータを追加する

カーソルの使用

  • オブジェクトストア内の全ての値を取得したい場合は、カーソルを使用できる。
  • opneCursor()関数は、引数がいくつかある。
    • 第一引数:すぐに取得するキーレンジオブジェクトを使用して、読み出すアイテムの範囲を制限できる。
    • 第二引数:反復処理を行いたい方向を指定できる。
  • カーソルの成功イベントのコールバックは少し特殊で、カーソルオブジェクト自体は、リクエストのresultだが、実際のキーと値は、カーソルオブジェクトのkeyプロパティとvalueプロパティで見つかる。
  • 進み続けたい場合は、カーソルでcontinue()を呼び出す必要がある。
  • データの終端に達した(または、openCursor()リクエストに一致する項目が存在しない)場合は成功のコールバックを受け取るが、resultプロパティがundefinedになる。
    • それ以外に、このような処理を行うためにgetAll()(およびgetAllKeys())を使用することができるが、カーソルのvalueプロパティに関してパフォーマンスコストが発生する。例えば、それぞれのキーを検索することにのみ関心がある場合は、getAl()よりもカーソルを使用する方が効率的。オブジェクトストア内の全データの配列を得ようとする場合は、getAll()を使用すべき。

インデックスの使用

  • 検索するとき、正しいものが見つかるまでデータベース内の全ての値に対して反復処理を行わなければならない。この方法だと遅いため、代わりにインデックスを使用する。
  • 指定した値に該当する全ての項目にアクセスしなければならない場合は、カーソルを使用する。インデックス上で、2種類のカーソルを開くことができる。ノーマルカーソルは、インデックスのプロパティと、オブジェクトストア内のオブジェクトを紐付ける。キーカーソルはインデックスのプロパティと、オブジェクトストア内にオブジェクトを保存するために使用するキーを紐づける。
    var index = objectStore.index("name");
    
    // 顧客レコードのオブジェクト全体を得るために、ノーマルカーソルを使用する。
    index.openCursor().onsuccess = function(event) {
      var cursor = event.target.result;
      if (cursor) {
        // cursor.key は "Bill" のような名前、cursor.value はオブジェクト全体。
        console.log("Name: " + cursor.key + ", SSN: " + cursor.value.ssn + ", email: " + cursor.value.email);
        cursor.continue();
      }
    };
    
    // 顧客レコードのオブジェクトのキーを得るために、キーカーソルを使用。
    index.openKeyCursor().onsuccess = function(event) {
      var cursor = event.target.result;
      if (cursor) {
        // cursor.key は "Bill" のような名前、cursor.value は SSN 。
        // 保存されたオブジェクトの他の部分を直接取得する方法はない。
        console.log("Name: " + cursor.key + ", SSN: " + cursor.value);
        cursor.continue();
      }
    };

カーソルの範囲や方向を指定する

  • カーソルで参照する値の範囲を制限したい場合は、IDBKeyRangeオブジェクトを使用して、openCursor()またはopenKeyCursor()の第1引数として渡す
  • 一つのキーのみ許可するキーレンジ、下限または上限の片方を持つキーレンジ、あるいは下限と上限の両方を持つキーレンジを作成できる。
  • 境界はclosed(キーレンジは指定した値を含む)またはopen(キーレンジは指定した値を含まない)
  • 方向の切り替えは、openCursor()の第2引数にprevを渡す
    // "Donna" にのみ一致
    var singleKeyRange = IDBKeyRange.only("Donna");
    
    // "Bill" より先のすべてに一致。"Bill" を含みます。
    var lowerBoundKeyRange = IDBKeyRange.lowerBound("Bill");
    
    // "Bill" より先のすべてに一致。ただし "Bill" は含まない。
    var lowerBoundOpenKeyRange = IDBKeyRange.lowerBound("Bill", true);
    
    // "Donna" までのすべてに一致。ただし "Donna" は含まない。
    var upperBoundOpenKeyRange = IDBKeyRange.upperBound("Donna", true);
    
    // "Bill" から "Donna" までに一致。ただし "Donna" は含まない。
    var boundKeyRange = IDBKeyRange.bound("Bill", "Donna", false, true);
    
    // いずれかのキーレンジを使用するには、openCursor()/openKeyCursor() の第 1 引数として渡す。
    index.openCursor(boundKeyRange).onsuccess = function(event) {
      var cursor = event.target.result;
      if (cursor) {
        // 一致した場合の処理。
        cursor.continue();
      }
    };
    
    objectStore.openCursor(boundKeyRange, "prev").onsuccess = function(event) {
      var cursor = event.target.result;
      if (cursor) {
        // 項目に対して行う処理
        cursor.continue();
      }
    };
    // データを保存例
    
    const dbName = "the_name";
    
    var request = indexedDB.open(dbName, 2);
    
    request.onerror = function(event) {
      // エラー処理
    };
    request.onupgradeneeded = function(event) {
      var db = event.target.result;
    
      // 顧客の情報を保存する objectStore を作成する。
      // "ssn" は一意であることが保証されているので、キーパスとして使用する。
        // 第一引数:ストアの名前
        // 第二引数:引数オブジェクト(省略可能)。
        //   keyPath:ストア内で個々のオブジェクトを一意にするプロパティ
      var objectStore = db.createObjectStore("customers", { keyPath: "ssn" });
    
      // 顧客を名前で検索するためのインデックスを作成。
      // 重複する可能性があるので、一意のインデックスとしては使用できない。
      objectStore.createIndex("name", "name", { unique: false });
    
      // 顧客をメールアドレスで検索するためのインデックスを作成。2 人の顧客が同じメールアドレスを
      // 使用しないようにしたいので、一意のインデックスを使用する。
      objectStore.createIndex("email", "email", { unique: true });
    
      // データを追加する前に objectStore の作成を完了させるため、
      // transaction oncomplete を使用。
      objectStore.transaction.oncomplete = function(event) {
        // 新たに作成した objectStore に値を保存。
        var customerObjectStore = db.transaction("customers", "readwrite").objectStore("customers");
        customerData.forEach(function(customer) {
          customerObjectStore.add(customer);
        });
      };
    
        transaction.onerror = function(event) {
          // エラー制御
        };
    };
    
    // 「スキーマ」や構造の変更
    var transaction = db.transaction(["customers"], "readwrite");
    
    // レコードの読み出し
    var transaction = db.transaction(["customers"], "readonly");
    
    // 変更処理
    var transaction = db.transaction(["customers"], "readwrite");
    
    // データの削除
    var request = db.transaction(["customers"], "readwrite")
                    .objectStore("customers")
                    .delete("444-44-4444");
    request.onsuccess = function(event) {
      // 削除完了!
    };
    
    // データの取得
    var request = db.transaction(["customers"], "readwrite")
                    .objectStore("customers")
                    .get("444-44-4444");
    request.onerror = function(event) {
      // エラー処理!
    };
    request.onsuccess = function(event) {
      // request.result に対して行う処理
      console.log("Name for SSN 444-44-4444 is " + request.result.name);
    };
    
    // データの更新
    request.onsuccess = function(event) {
      // 更新したい、古い値を取得
      var data = request.result;
    
      // オブジェクト内の値を、希望する値に更新
      data.age = 42;
    
      // 更新したオブジェクトを、データベースに書き戻す。
      var requestUpdate = objectStore.put(data);
      requestUpdate.onerror = function(event) {
        // エラーが発生した場合の処理
      };
      requestUpdate.onsuccess = function(event) {
        // 成功 - データを更新
      };
    };
    
    // カーソルを使用したデータの取得
    objectStore.openCursor().onsuccess = function(event) {
        var cursor = event.target.result;
        if (cursor) {
            // 処理
            cursor.continue();
        }
    }
    
    // インデックス
    // 最初に、 request.onupgradeneeded の中にインデックスを生成したか確認する。
    // objectStore.createIndex("name", "name");
    // まだであれば、 DOMException が発生する。
    
    var index = objectStore.index("name");
    
    index.get("Donna").onsuccess = function(event) {
      console.log("Donna's SSN is " + event.target.result.ssn);
    };
    ```
    
    # ウェブアプリが別のタブで開かれているときにバージョンを変更する
    
    ```javascript
    var openReq = mozIndexedDB.open("MyTestDatabase", 2);
    
    openReq.onblocked = function(event) {
      // 他のタブがデータベースを読み込んでいる場合は、処理を進める前にそれらを閉じる。
      console.log("このサイトを開いている他のタブをすべて閉じてください!");
    };
    
    openReq.onupgradeneeded = function(event) {
      // 他のデータベースはすべて閉じられたため、すべての処理を行う。
      db.createObjectStore(/* ... */);
      useDatabase(db);
    };
    
    openReq.onsuccess = function(event) {
      var db = event.target.result;
      useDatabase(db);
      return;
    };
    
    function useDatabase(db) {
      // 別のページがバージョン変更を求めた場合に、通知されるようにするためのハンドラーを追加。
      // データベースを閉じなければならない。データベースを閉じると、別のページがデータベースをアップグレードできる。
      // これを行わなければ、ユーザーがタブを閉じるまでデータベースはアップグレードされない。
      db.onversionchange = function(event) {
        db.close();
        console.log("新しいバージョンのページが使用可能になりました。再読み込みしてください!");
      };
    
      // データベースを使用する処理
    }

セキュリティ

  • 同一生成元の原則。ストアとサイトの生成元を紐づけるため、他の生成元からアクセスできない

ブラウザの終了に関する警告

ロケールを意識した並べ替え