trezor APIで得たデータをdexie.js経由でIndexedDBに保存し、それをsql.js経由でSQLite3形式に変換してダウンロード&アップロードできるようにした。
ブツ
今回の目的
データを保存したり集計したりする方法や形式の模索。
アップロードできるが無意味
ダウンロードしたZIPを展開してアドレス.db
ファイルをドラッグ&ドロップすれば、IndexedDBに上書きできる。が、レコード数が少ないなどの理由で古いデータだと判断したらアップロードしないようにしている。そしてDEMOページはアクセスしたら自動的に最新データを取得してしまう。よってアップロードできるのは最新データ取得後にダウンロードしたファイルを、そのまますぐにドラッグ&ドロップしたときだけ。当然、同じデータなので上書きされているかどうかは見た目上わからない。なので実質、アップロードしても何も嬉しいことはない。
今回はSQLite3ファイルからIndexedDBにアップロードするときの実装方法を確認したかったので、無意味だと知りつつ機能をつけた。
保存方法
保存方法 | 問題点 |
---|---|
IndexedDBのみ | ローカルにそれをダウンロードする方法がない |
SQLite3のみ | ブラウザでそれを保存する方法がない |
ブラウザでもローカルでも取引データを取得・編集・保存できるようにしたかったのでIndexedDBとSQLite3の両方を使ってみた。
取得方法
trezor APIで取引データを取得し、それをdexie.js経由でIndexedDBに保存する。
DB, テーブル作成
this.dexie = new Dexie(`${address}`); // 自分のアドレスひとつごとにDBを作成する。テーブル結合不要になる。
this.dexie.version(1).stores({
last: `++id`,
sendPartners: `address`,
receivePartners: `address`,
transactions: `txid`,
})
じつは今回transactions
テーブルの値のうちisPay
プロパティをisSend
に改名した。以下のようなコードで。
const txs = await this.dexie.transactions.toArray().catch(e=>console.error(e))
for (const t of txs) {
if (t.hasOwnProperty('isPay')) {
await this.dexie.transactions.put({
txid: t.txid,
isSend: t.isPay, // このプロパティ名を変更する
addresses: t.addresses,
value: t.value,
fee: t.fee,
confirmations: t.confirmations,
blockTime: t.blockTime,
blockHeight: t.blockHeight,
})
}
}
このように列名を変えたり追加したりしてテーブルのスキーマを変更する対処をマイグレーションという。その手順は、あたらしいテーブルを作ったあと、古いテーブルからそのデータを挿入して、古いテーブルを削除すれば完了。
マイグレーションをdexie.jsで行うときはversion()
の値をインクリメントし、さらにupgrade
メソッドで古いテーブルを修正するコードを書くのが正しい作法らしい。以下のように。
this.dexie.version(2).stores({
last: `++id`,
sendPartners: `address`,
receivePartners: `address`,
transactions: `txid`,
}).upgrade(async(tx)=>{
const txs = await this.dexie.transactions.toArray().catch(e=>console.error(e))
for (const t of txs) {
if (t.hasOwnProperty('isPay')) {
await this.dexie.transactions.put({
txid: t.txid,
isSend: t.isPay, // このプロパティ名を変更する
addresses: t.addresses,
value: t.value,
fee: t.fee,
confirmations: t.confirmations,
blockTime: t.blockTime,
blockHeight: t.blockHeight,
})
}
}
})
でも今回はそうしなかった。その理由は一度バージョン値をインクリメントするとデクリメントできないから。正しくマイグレーション用コードを書けなかったのにエラーがなければ成功と判断されてしまいバージョンがインクリメントされてしまう。ムダにバージョンがあがる。
デバックの時点でそんな仕様にハマり、デベロッパツールで何度もDB削除とデータ取得を繰り返す羽目になって大変だった。
よって今後dexie.jsでマイグレーションするときはバージョン値を1
に固定し、力技で修正するようにすることに決めた。
レコード挿入
trezor APIで取引したデータをdexie.js経由でIndexedDBに保存する。
dbs.get(address).dexie.transactions.put({
txid: tx.txid,
isSend: isSend,
addresses: Array.from(addrs).join(','),
value: value,
fee: fee,
confirmations: tx.confirmations,
blockTime: tx.blockTime,
blockHeight: tx.blockHeight,
})
ダウンロード
IndexedDB→SQLite3
まずはSQLite3でテーブルを作成する。
db.exec(this.#createSqlTransactions())
#createSqlTransactions() { return `
create table if not exists transactions (
txid text primary key not null,
is_send integer,
addresses text,
value integer,
fee integer,
confirmations integer,
block_time integer,
block_height integer
) without rowid;`
}
次にIndexedDBファイルからデータを取得し、それをSQLite3のテーブルへ挿入する。
const txs = await this.dbs.get(this.my).dexie.transactions.toArray()
for (const tx of txs) {
db.exec(`insert into transactions values (
'${tx.txid}',
${tx.isSend},
'${tx.addresses}',
${tx.value},
${tx.fee},
${tx.confirmations},
${tx.blockTime},
${tx.blockHeight}
);`)
}
ダウンロードしたらZIPを展開し、アドレス.db
ファイルをsqlite3
コマンドで開いてSQL文を発行して中身を確かめることができる。以下参考。
アップロード
SQLite3→IndexedDB
sql.jsのライブラリファイルを読み込む。
this.SQL = await initSqlJs({locateFile: file => `${this.PATH_WASM}/${file}`})
SQLite3ファイルをドラッグ&ドロップで受け取ったら、そのバイナリデータをSQL.Database()
の引数に渡す。その戻り値db
のメソッドexec()
でSQL文を発行する。
return new this.SQL.Database(dbAsUint8Array)
SQLite3ファイルからIndexedDBへレコードを挿入・上書きするコードの抜粋が以下。
rows = db.exec(`select * from transactions;`)
for (const row of rows[0].values) {
const v = row
await this.dbs.get(this.my).dexie.transactions.put({
txid: v[0],
isSend: v[1],
addresses: v[2],
value: v[3],
fee: v[4],
confirmations: v[5],
blockTime: v[6],
blockHeight: v[7],
})
}
SQLite3のデータはselect * from transactions;
というSQL文で取得している。このとき戻り値rows
は配列である。さらに以下のようになっている。
rows[0]
columns[N]:[列1名,列2名,列3名,...]
values[N]
[0]:[列1値,列2値,列3値,...]
[1]:[列1値,列2値,列3値,...]
...
columns
はテーブルの列名が入っている。values
にはレコードの値が入っている。オブジェクトにするなら{column1:value1, column2:value2, ...}
のようになる。私としてはそうしてくれるとコードの可読性が上がって嬉しかったのだが。なぜ配列なのか。その理由を予想するなら、キー名が全レコードで同じであることがわかりきっており、できるだけムダなデータを削減したかったからかもしれない。
この配列の順序はSQLでcreate table
文を発行するときに定義した列名の順である。
考察
今はIndexedDBにJavaScriptオブジェクトとして値を保存している。そのため集計はJavaScriptのmap
,filter
,reduce
といったAPIを使って行っている。
だが、IndexedDBにSQLite3ファイルデータdb.export()
の結果をそのまま丸ごと保存する方法も考えられる。そうすればDBまわりの取得や集計をすべてSQL文で統一できる。以下のように。
this.dexie = new Dexie(`mona-transaction-sqlite3`);
this.dexie.version(1).stores({
dbs: `address`,
})
this.dexie.dbs.put({
address: address,
sqlite3: db.export(),
})
const db = SQL.Database(this.dexie.dbs.get(address))
db.exec(`select * from transactions;`)
もし取得や集計をSQL文で統一できれば、ローカルとブラウザ上で同じコードだけを使って同じことができる。そのほうが覚えることも少なくて済みそうで楽なような気もする。
けれどブラウザではJavaScriptのmap
,filter
,reduce
といったAPIを使ったほうが楽な場合もある。どっちもどっち。
もしSQLで統一してしまえばブラウザのほうはsql.jsがないと解析できなくなってしまう。依存性が高まることには不安がある。たとえばバージョン差異により互換性がなくなるなど。
SQLite3のdb.export()
は全データ丸ごとなので扱いづらいのも気になる。一件でも新しいデータが追加されたら全データ丸ごと交換になる。ムダに大きな読書が発生しそうな気がする。
はたしてどう実装するのが最善なのか。悩みどころだったが、総合的に考えて今回のコードのとおりIndexedDBにJavaScriptオブジェクトとして値を保存するのがよさそうな気がする。