ローカルにデータを保存するために。

ブツ

developertoolapplicationindexeddb.png
developertoolconsole.png

JavaScriptにおけるデータ保存方法

方法 概要
Cookie サーバから送られた秘密データを文字列で4KBだけ保存する(ドメイン単位)
LocalStorage キーバリュー形式で文字列を5MB〜10MBだけ保存する(ドメイン単位)
IndexedDB RDBMS風でデータ型を保ったまま10MB〜2GBほど保存する(ドメイン単位)
File API ローカルにあるファイルを読み取る
FileSystemAccess API ローカルのファイルシステム上にある特定のファイルを操作する
Cache API サイトで使うファイルをキャッシュする(PWAにしダウンロードを最小化する)

かつては「Web SQL Database」なるものがあったが、今ではIndexedDBで代用することが推奨されているようだ。

色々あるが、それぞれ特徴や用途が違う。

目的

今回の目的は自分のアドレスで行ったモナコインの取引データを取得・保存・集計すること。

わざわざローカルに保存する理由は、取引回数だけtrezorのBlockBook APIを実行せねばならず、サーバ負荷が大きすぎるため。一回のリクエストごとに1秒間のウェイトをおいたとしても、取引が100件あれば取得だけで100秒間も必要になる。それが毎回だと使い物にならないので、一度入手したデータはローカルに保存することで高速化を図る。

どれを使うか

さまざまな保存方法がある。できるだけ簡単に使え、要件を満たすものはIndexedDBと思われる。

ただ、IndexedDBは利用するのが大変なのでライブラリを使う。それがdexie.js。詳しくはdexie.js - docs参照。

dexie.jsを使ってみる

class UseDexie {
    constructor() {
        this.createDb()
    }
    createDb() {
        this.dexie = new Dexie(`UsersDb`);
        this.dexie.version(1).stores({
            users: `++id, name`
        });
    }
    async insert() {
        const now = new Date()
        this.dexie.users.add({name:'ytyaru', created:now, updated:now}).catch((e)=>{console.error(e);});
    }
    async upsert() {
        const now = new Date()
        this.dexie.users.put({name:'hoge', updated:now}).catch((e)=>{console.error(e);});
    }
    async update() {
        const now = new Date()
        this.dexie.users.where(`name`).equals(`hoge`).modify({name:'hoge-update', updated:now}).catch((e)=>{console.error(e);});
    }
    async clear() { await this.dexie.users.clear() }
    async delete() {
        const ids = await this.dexie.users.where(`name`).equals(`ytyaru`).toArray()
        console.debug(ids)
        this.dexie.users.bulkDelete(ids.map(r=>r.id))
    }
    async gets() { return await this.dexie.users.toArray() }
    async get() { return await this.dexie.users.get(`1`) }
    async where() { return await this.dexie.users.where(`name`).equal(`ytyaru`).toArray() }
}

1. DBを作成する

this.dexie = new Dexie(`UsersDb`);

DBは複数のテーブルをもつ単位。

2. テーブルを作成する

this.dexie.version(1).stores({
    users: `++id, name`,
});

テーブルは複数のレコードをもつ単位。DBの中に所属する。

usersがテーブル名。++id, nameはインデックスに使う列名とそのインデックス方法の指定。

SQLならcreate文ですべての列を指定するところだが、これはwhere句で使う列のみ指定する。

記号 意味
++ 自動インクリメント主キー(整数型の列で一意特定)
& ユニークキー(値が重複しないことを保証する列)
* マルチエントリーインデックス(配列型主キー)
[A+B] 複合主キー(複数の列で一意特定)

すべての列を指定しなくて済むのは、将来の拡張に柔軟性がもてると言える。が、where句で使うものを予め理解していなければならないなら、あまり意味は無さそう。

SQL文が使えないっぽいのは残念。

3. レコードを作成する

addはSQLでいうinsertと思われる。ただ、putは期待していた動作と違った。既存値はそのまま残し、新しく与えたものだけ更新してほしかったのに、実際は与えなかったものは消えてしまう。

おそらく、addのほうはキーが既存ならすでに存在するとしてエラーになる。でもputはエラーにせず丸ごと書き換える。そういう違いがあるのだろう。

await this.dexie.users.add({name:'ytyaru', created:now, updated:now}).catch((e)=>{console.error(e);});
await this.dexie.users.put({name:'hoge', updated:now}).catch((e)=>{console.error(e);});

4. レコードを取得する

全レコードを返す

await this.dexie.users.toArray()

指定した主キーの値を返す。

await this.dexie.users.get(`1`)

指定した条件のレコードを返す。whereの条件指定メソッドは他にもたくさんある。詳しくはdexie.js - docs参照。

await this.dexie.users.where(`name`).equals(`ytyaru`).toArray()

5. レコードを更新する

なぜかupdateだとエラーになったのでmodifyを使った。

await this.dexie.users.where(`name`).equals(`hoge`).modify({name:'hoge-update', updated:now}).catch((e)=>{console.error(e);});

私が求めるのはマージのようなAPIだが、値をインデックス列にしないとすべてvalueになり、一括で書き換えられてしまうっぽい。つまりwhere句で使う列だけでなく、個別に更新したい列もインデックスにしないとならない。だったらふつうにRDBMSのように全列を指定するほうが楽じゃないか? よくわからない。たぶん容量が増えるから、できるだけインデックス列を減らしたほうがいいという話なのだろう。

6. レコードを削除する

deleteは指定した主キーをもつレコードを削除する。

this.dexie.users.delete('ytyaru')

clearはそのテーブルの全レコードを削除する。

this.dexie.users.clear()

所感

他にもまだまだ多くのAPIがある。詳しくはdexie.js - docs参照。

とにかくdexie.jsを駆使してIndexedDBにデータをローカル保存できそうだと判明した。