ブラウザでデータを保管する場合、Web Storage(sessionStorageやlocalStorage )を使うことが多かったと思いますが、IndexedDBが登場したことであえてWeb Storageを使う理由がなくなりました。
IndexedDBを使う場合、Dexie.jsを使用することでだいぶ実装がスッキリするので、使い方を紹介したいと思います。
Web StorageとIndexedDBの違い
そもそも本当にIndexedDBを使うのが良いのか、という疑問があるので念のため、Web Storageと IndexedDBの違いを調べてみました。調べた全ての点でIndexedDBの方が良いので、やはりIndexedDB一択ですね。
項目 | Web Storage | IndexedDB |
---|---|---|
型 | 文字列のみ | boolean、数値、文字列、dateなど色々 |
RDBのテーブル | なし。key-valueで全てフラットに管理 | RDBのテーブルに相当するオブジェクトストアがあり、その下にレコードを格納する |
容量制限 | Originあたり10MB | ディスクサイズに依存(普通は10MBよりは大きい) |
devtoolでの変更 | 追加・変更・削除などなんでもできる | クリアや削除はできるが追加変更はできない |
アクセス範囲 | 同一オリジン内。JavaScriptから全てのキーを取得できる | 同一オリジン内。ただし、データベース名とバージョンを知らないとアクセスできない |
※chromeのケースで調べてます。間違ってたらすいません。
Dexieの使い方
DexieはIndexedDB利用を非常に楽にしてくれるライブラリです。自分としては、IndexedDBのレコードをTypescriptのクラスにO/R Mappingしてくれる点が非常にありがたいと思いました。
インストール
npmでパッケージをインストールします。
npm install dexie
DB定義およびスタート
dexie.ts
を作って以下のような感じでDB定義およびスタートを行います。
import Dexie from 'dexie'
// スキーマの修正がある場合、この値を変更する必要あり
const SCHEMA_VERSION = 1
// オブジェクトストアに対応するクラスを作成。interfaceでもOK。実際にはmodelは別ファイルに定義してあります
export class AccessCountModel {
key: number
readCount: number
updateCount: number
deleteCount: number
constructor(key: number, readCount = 0, updateCount = 0, deleteCount = 0) {
this.key = key
this.readCount = readCount
this.updateCount = updateCount
this.deleteCount = deleteCount
}
}
interface HogeDatabase extends Dexie {
accessCounts: Dexie.Table<AccessCountModel, number> // ここでオブジェクトストアとモデルクラスを対応づけている。numberはキーの型
}
const dexieDb = new Dexie('hoge-database') as HogeDatabase
dexieDb.version(SCHEMA_VERSION).stores({
accessCounts: 'key,readCount,updateCount,deleteCount', // オブジェクトストアの定義
})
export default dexieDb
repositoryクラスの作成
直接、dexieDbを使ってもいいと思いますが、毎回then
やcatch
を書くのも大変なので、repositoryクラスを作ります。
import dexieDb, { AccessCountModel } from './dexie'
export class AccessCountRepository {
// データ取得
async get(key: number): Promise<AccessCountModel | undefined> {
return dexieDb.accessCounts
.get(key)
.then(async (count: AccessCountModel | undefined) => {
return count
})
.catch((error) => {
throw new Error(`Error getting access count:, ${error}`)
})
}
// データ更新
async save(accessCount: AccessCountModel): Promise<AccessCountModel> {
await dexieDb.accessCounts.update(accessCount.key, accessCount)
return await this.get(accessCount.key)
}
}
利用
実際の利用箇所では、repositoryクラスをimportして使います。
import { AccessCountRepository } './repositories'
import { AccessCountModel } from './dexie'
class Hoge {
accessCountRepository = new AccessCountRepository()
async fuga(): Promise<void> {
const key = 3600
const accessCount: AccessCountModel = await this.accessCountRepository.get(key)
console.log(accessCount)
}
}
CDN利用
dexieはCDNも提供されているので、そちらを使うこともできます。
htmlでdexie.min.js
を読み込んで、
<body>
<script src="https://unpkg.com/dexie@latest/dist/dexie.min.js"></script>
</body>
webpack.config.jsのexternalsにdexie
を追加します。global変数名はDexie
なのでそれを指定してます。
externals: {
dexie: 'Dexie',
}
これで、バンドルファイルからは除外され、CDNから読み込むようになります。
トラブルシューティング
オブジェクトストアが存在しない
存在しないオブジェクトストアにアクセスしようとすると以下のエラーが発生します。
NotFoundError: Failed to execute 'transaction' on 'IDBDatabase': One of the specified object stores was not found.
このような場合、自分の経験的には原因は以下の二つのどちらかでした。
- DB定義が間違っている(typoや定義し忘れ)
- DB定義を変更したのに、スキーマバージョンを変更しわすれている
最後に
サーバサイドかと思うぐらいかっちりした感じで実装することができました。
ただ、今回紹介した内容は、個々のクエリが個別のtransactionで実行される状態なので、一連の処理を一つのtransaction内で実行する場合はもう少し工夫が必要そうです。