Indexed Database APIの基礎 

# 記事の趣旨
ローカル環境での永続化手法の1つである「Indexed Database API」について，ザックリと勉強したので，そのメモ書き．

## 1. Indexed Database APIとは

Webブラウザに標準搭載されている，Key-Value型のAPIとのこと．API仕様は[W3C](https://www.w3.org/TR/IndexedDB-2/)で規定されている．主な登場人物としては「Database」「Object Store」「Record」で，関係性としては以下のとおり．

- Databaseは，0以上のObject Storeを有している
- Object Storeは，Key-Value型のRecordのリストを有している
- Object Storeはnameを持ち，nameはDatabase内で一意であることが保証されている
- Recordのリスト内でkeyは一意であることが保証されている

## 2. IDBRequest

先述の登場人物に対するCRUDは「Request」を通じて実行される．その仕様は`IDBRequest`というインタフェースで定義されている．大雑把に触れると以下のとおり．

- `IDBRequest`の実装オブジェストは，生成された瞬間にリクエストを発出する．
- このリクエストは**非同期**に処理される．
- 結果はオブジェクト内の`result`変数に格納される．
- リクエストが成功／失敗したときのイベントハンドラとして`onsucess`および`onerror`が用意されている．
- 拡張インタフェースである`IDBOpenDBRequest`のみ，追加で`onblock`および`onupgradeneeded`というイベントハンドラが用意されている．後者は初回起動時やDBのバージョン更新時のみ実行される．

たとえばこんな感じ．

```javascript
var connection;

window.onload = function () {
    var openRequest = indexedDB.open('testDB');

    openRequest.onsuccess = function () {
        connection = openRequest.result;
    }
}
```

`indexedDB`はブラウザ側で用意されているオブジェクトなので特に宣言なしに使用できる．こいつの`open('testDB')`メソッドで返却しているのが，まさに先述の`IDBOpenRequest`の実装オブジェクト．このメソッド実行後，「testDBという名称のDatabaseに接続する，なければ作る」というリクエストが非同期に実行される．処理が完了すると`result`に結果が格納されているので，それを`connection`変数に渡すよう，`onsuccess`のイベントハンドラに設定している．

## 3. connectionとtransaction

先述のconnectionを起点としてRequestを発出する．connectionは，以下の2つのメソッドを有している．

- `createObjectStore(name)`：新たなObject Storeを作成する．
- `transaction(name, mode).objectStore(name)`：既存のObject Storeに対するTransactionを開始する．modeは`'readonly'`か`readwrite`．

いずれも`IDBObjectStore`の実装オブジェクトを返却してくれる．そのオブジェクトは，例えば以下のようなRequest発出用のメソッドを有している．

- `put(value, key)`：Recordをputする．既存のkeyを使用した場合は上書き．
- `add(value, key)`：Recordをaddする．既存のRecordを使用した場合はエラー．
- `get(key)`：指定したkeyのRecordをgetする．
- `getAll()`：すべてのRecordを配列型でgetする．
- `delete(key)`：指定したkeyのRecordをdeleteする．
- `clear()`：すべてのRecordをdeleteする．

## 4. サンプル

ここまでに得た情報を踏まえて，とりあえずザックリとサンプルを書いてみる．Chromeの「79.0.3945.88」で動くことを確認．

```html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <button class="showButton">Object Storeの表示</button>
  </body>
  <script src="scripts/main.js"></script>
</html>
```

```javascript
var connection;

window.onload = function () {
    var openRequest = indexedDB.open('testDB');

    openRequest.onupgradeneeded = function () {
        connection = openRequest.result;
        var objectStore = connection.createObjectStore('testObjectStore', {keyPath:'id'});
        objectStore.put({id:1, name:'taro'});
        objectStore.put({id:2, name:'jiro'});
        objectStore.put({id:3, name:'saburo'});
    }

    openRequest.onsuccess = function () {
        connection = openRequest.result;
    }
}

function showObjectStore() {

    var humans = getAllSync().then(
            function(humans) {
                for(var i = 0; i<humans.length; i++) {
                    var human = humans[i];
                    alert(human.name);
                }
            }
        );
}

function getAllSync() {
    return new Promise(
        function (resolve, reject) {
            var transaction = connection.transaction('testObjectStore', 'readonly');
            var request = transaction.objectStore('testObjectStore').getAll();
            request.onsuccess = function (e) {
                resolve(request.result);
            }
            request.onerror = reject;
        }
    )
}

document.querySelector('.showButton').addEventListener('click', showObjectStore);
```

`testDB`内に`testObjectStore`を作り，初期設定として3種類のRecordを格納している．ボタンを押下すると，そのすべてを取得し，alertで表示する，という挙動になる．

細部については，以下のとおり．

- `createObjectStore`メソッドの第二引数`{keyPath:'id'}`は，「idをキーと認識します」という宣言のようなもの．
- 先ほども述べた通り，リクエスト処理は非同期なので，Promiseによる「待ち」を施している．
  - 今回で言うと`transaction.objectStore('testObjectStore').getAll()`がいつ終わるか不明．
  - なお，`onload`内の各種`put`にも，たぶん本当は対策が必要．
- 無事データが取得できたら，resultに配列としてデータが格納されているので，`alert`で全員分の名前を表示．

## 5. Key Generator

上記の例だと，keyPathで設定したid値を手入力で指定しているが，どうせならそこは自動で設定して欲しいと誰もが思うはず．幸い，「Key Generator」という仕組みが存在する．

用法は簡単で，上記の一部を以下のように書き換えるだけ．

```javascript
        var objectStore = connection.createObjectStore('testObjectStore', {keyPath:'id', autoIncrement:true});
        objectStore.put({name:'taro'});
        objectStore.put({name:'jiro'});
        objectStore.put({name:'saburo'});
```

`put`時にidというkeyがなくなっているが，勝手に生成してくれる．

## 6. 終わりに

非同期処理の対策をミスると面倒くさいという点を除けば割と便利だと思う．ちょっとした自作ブラウザアプリを作るだけなら，これだけで十分だと思う．