ブラウザでデータを保管する場合、Web Storage(sessionStorageやlocalStorage )を使うことが多かったと思いますが、IndexedDBが登場したことであえてWeb Storageを使う理由がなくなりました。

IndexedDBを使う場合、Dexie.jsを使用することでだいぶ実装がスッキリするので、使い方を紹介したいと思います。

Web StorageとIndexedDBの違い

そもそも本当にIndexedDBを使うのが良いのか、という疑問があるので念のため、Web StorageIndexedDBの違いを調べてみました。調べた全ての点で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を使ってもいいと思いますが、毎回thencatchを書くのも大変なので、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内で実行する場合はもう少し工夫が必要そうです。