Ki6cooでファイルの最終更新日時をIndexedDBに保存することにしました。そのためにIndexedDBにDateを保存するときの動作について確認しました。Dexie.jsを使っています。

試す前からこういう振る舞いだろうなとは思っていましたが、試したことで自信が持てました。JavaScriptのDateは「そうくるか」と思うことが多いので想定がありませんでしたが、試して良かったと思います。タイムゾーンやその有無にまったく不安を感じない、Rustのchronoに触れたすぐ後だからかもしれませんが、JavaScriptのDateがとても不安に思えていました。

振る舞いの確認をFirefoxとEdgeでして、両者に差異は見つかりませんでした。

この記事の中で使用したTypeScript

import Dexie, { Table } from 'dexie'

interface Item {
  key: string
  date: Date
}

class DateDexie extends Dexie {
  items!: Table<Item>

  constructor() {
    super('date-database')
    this.version(1)
      .stores({
        items: '&key, date'
      })
  }
}

Dateの保存時と読み出し時でタイムゾーンが違う場合

読み出したときのタイムゾーンのDateオブジェクトが手に入ります。

以下確認です。

まずIndexedDBにDateを保存します。これはパソコンのタイムゾーンを日本標準時にして実行しました。

const database = new DateDexie()
const date = new Date()
console.log('保存するDateの値', date)
await database.items.put({ key, date })

これが実行結果です。

保存するDateの値 Tue Mar 01 2022 19:26:01 GMT+0900 (日本標準時)

次に日本標準時のまま読み出します。

const database = new DateDexie()
const date = (await database.items.get(key))?.date!

console.log('取得', date)
console.log('カウントA', await database.items.where('date').equals(date).count())

date.setHours(date.getHours() - 1)

console.log('カウントB', await database.items.where('date').above(date).count())
console.log('カウントC', await database.items.where('date').below(date).count())

これが実行結果です。

取得 Tue Mar 01 2022 19:26:01 GMT+0900 (日本標準時)
カウントA 1
カウントB 1
カウントC 0

次に協定世界時に変更して上記と同様のプログラムで読み出します。

取得 Tue Mar 01 2022 10:26:01 GMT+0000 (協定世界時)
カウントA 1
カウントB 1
カウントC 0

保存時のタイムゾーンに関わらず、読み出したときのタイムゾーンの値が返されます。当たり前と言える結果だと思いますが、確認したことで自信が持てます。

カウントの結果からもタイムゾーンが変化したことで結果がおかしなことにならないことが確認できています。

Dexie.jsのwhere

Dexie.jsのWhereClauseにstartsWithがあるのを知ったときは、Dateに対してどう振る舞うのかと思いましたが、引数にstringしか取りません。

equalsやaboveがDateに対して使用できるWhereClauseです。

string型とDate型

IndexedDBに保存するオブジェクトにDate型の代わりにstring型を使ってもいいんじゃないかと迷ったので、次に同じような課題に直面した時に以前の判断理由が分かるように文章を残します。今にして思えば、なんで迷ったんだろうと思う内容です。

Date型の代わりにstring型でIndexedDBに保存してもいいんじゃないかと考えた理由は以下の4つです。

  • タイムゾーンの扱いが不明
  • Date型を直接使うことはなく、常にDay.js経由で日時を扱っている
  • SQLと違い、IndexedDBではDate(に関わらずすべての型)に対してクエリ中で計算できない
  • この日時のデータを取得やこの日時の範囲の日付の取得など、Dateに対して使えるwhereの条件はstringで同様のことができる

「タイムゾーンの扱いが不明」については、の記事中にあるように、確認したため解決しました。

次にDay.jsを使っていることについてです。Day.jsを使っているため、IndexedDBに保存するときにはDay.jsからDateに変換するし、IndexedDBから読み出すときにはDateからDay.jsに変換します。Day.jsは文字列にも変換できますから、Dateの代わりにstringを使っても手間は変わらないだろうと考えました。

次にSQLとの違いについてです。SQLならクエリ中で計算ができるためdateやtimestamp型を使えば、例えばある日付と別のある日付の間に何日あるかという計算ができます。IndexedDBは取得したいオブジェクトの値に対して比較ができるだけで、そういった計算はできません。計算はオブジェクトの取得後にしかできません。そうは言ってもIndexedDBの用途を考えれば、仮にSQLの様に計算できたとしても計算することはないでしょう。

最後にwhereの条件はstringであっても再現できることについてです。stringにISO 8601フォーマットを採用したとします。2022年を条件にした場合は2022-01-01から2023-01-01をbetweenに渡せば達成できます。余談ですが、Dexie.jsではbetweenに境界値を含めるかどうかを指定できます。

stringではなく、Dateを使うデメリットは特に思い当たりませんでした。冒頭に書いたようにJavaScriptのDateに対する不安が一番大きかったと思います。numberであるものをわざわざstringに変換して保存しないのと同様、日時という情報はDateで扱うべきだと考え、Dateで保存することにします。