読者です 読者をやめる 読者になる 読者になる

日曜プログラミング

休日趣味でやってるプログラミング関連記事をダラダラと

datomic を試す(5) - 時と共に

datomic

前回はパラメータクエリ、クエリ内関数、全文検索、ルールの使用などを見てきた。
datomic チュートリアルを試す(4) - アドバンスド・クエリー - 日曜プログラミング

今回の記事で公式チュートリアルで該当する原文項目は以下。

  • Working with time
    • Time is built in
    • Revisiting the past
    • Imagining the future

datomic の紹介関係で良く見る

  • 追加のみのデータベースで時間は組み込みである

と言うのを実際に扱うにはどうするのかチュートリアルでもようやく出てきた。

サブタイトル適当訳してたら妙にキザっぽくなってしまったのはご愛嬌。

時間は組み込み(Time is built in)

これは良く宣伝される所でここでの細かい紹介は外部サイトに譲るが、自分の理解ではDB 全体で変化があったら自動的にマーキングして時系列的に持っているイメージ。多分内部的にはまるごとコピーではなく変化した差分だけ持っているような感じなんだろう。更新・消去の管理は別プロセスで動いている transactor で一手に引き受けてる。

で、今接続している db の更新日時一覧を取得するパラメータクエリ実行サンプル。

> (d/q '[:find ?when
         :where
         [?tx :db/txInstant ?when]]
       (d/db cn))
#{[#inst "2013-10-25T05:34:20.048-00:00"]
  [#inst "2013-10-25T05:47:10.216-00:00"]
  [#inst "1970-01-01T00:00:00.000-00:00"]}

DB 自身が持つ :db/txInstant 属性を見ればいいようだ。最初の 1970 年のはスタートポイントとして既に入ってるのかな?また、Clojurian だともう常識かもしれないが、:db/txInstant で返ってきた #inst に続く値は java.util.Date 型の Clojure 表記。

チュートリアルの最初の方でやった一番新しい時間が実データの読込を、その一つ前の時間がスキーマ読込を実施した時間になる。

過去を訪ねる(Revisiting the past)

一つ前の項目で DB の更新日時一覧を取得する方法は分かったので、今度は特定の日時でのデータが本当に取得できるのかを確認してみる。

現段階でのチュートリアルにおけるデータ更新タイミングは先にも書いた通りスキーマ定義した時とその後にデータをまるっと追加した時の 2 つ。スキーマ定義した段階に遡って単純なクエリを実行すれば何もデータが返って来ない事が予想される。

確かめてみる。

> (let [tx-dates (->> (d/q '[:find ?when :where [?tx :db/txInstant ?when]] (d/db cn))
                      (map #(% 0))
                      sort)
        query '[:find ?c :where [?c :community/name]]]
    (map #(count (d/q query (d/as-of (d/db cn) %))) tx-dates))
(0 0 150)

クエリで返ってくるのは Java の HashSet で順序の保証はないため、sort をかます。

d/q で通常渡されている二つ目の引数が (d/db cn) でなく (d/as-of (d/db cn) %) なのに注目。
as-of の1つ目に DB を指定し、2 つめに transaction の特定のポイントとなるものを指定。ここでは最初に取得した日時を指定しているが、API リファレンス によるとトランザクション ID やトランザクション No を指定できるらしい。それをどうやって取るのかはまだ良く分からん。

返って来た結果は各更新日時(原文では transaction point とか何かそんな表現してる)段階でのクエリ結果件数のシーケンス。先頭は 1970 年とかなってて多分無視して良いもので、2 つ目がスキーマ定義時のデータ件数、最後がデータ登録後のデータ件数となり、この結果からスキーマ定義時にはデータが一切入ってない事が分かる。

さて、DB 全体で履歴を持つとなると差分だけとは言えデータの増加量が心配になる。デフォルトではどうやら履歴を取るようにしているらしいが、属性単位で定義時に :db/noHistory true を追加してやればその属性については履歴を取らないようにする事も可能。

未来を思い描く(Imagining the future)

leiningen プロジェクト作成時に移動した 3 ファイルの内、まだ使っていなかった seattle-data1.dtm がある。ここでそいつを読み込んでやる。

; read-dtm は自分が独自に定義したヘルパー関数。前の記事参照。
(read-dtm "resources/seattle-data1.dtm")

ここで自分が失敗したんだが、read-dtm では内部的に transact を呼んでいて、これはどうも SQL で言う COMMIT に相当するものみたい。仮に登録してどうなるのか確認する為には transact の代わりに with を使ってみるといいよ、と言う趣旨の説明をしてるっぽい。

ちょっとチュートリアルの流れと異なるが、自分が確認した方法をここに載せる。

> (let [tx-dates (->> (d/q '[:find ?when :where [?tx :db/txInstant ?when]] (d/db cn))
                      (map #(% 0))
                      sort)
        query '[:find ?c :where [?c :community/name]]]
    (map #(vector % (count (d/q query (d/since (d/db cn) %)))) tx-dates))
([#inst "1970-01-01T00:00:00.000-00:00" 258]
 [#inst "2013-10-25T05:34:20.048-00:00" 258]
 [#inst "2013-10-25T05:47:10.216-00:00" 108]
 [#inst "2013-11-06T05:26:55.521-00:00" 0])

さっきも見たようなコードだが、as-of の代わりに since によるクエリ結果の件数を返している。これは結果を見るに、指定したポイントからの差分データのみを取るようなイメージみたい。なので、最新の日時では当然 0 件で、その一つ前は seattle-data1.dtm を読み込む前となるからその増えたデータ数である 108 が返ってきている。

ここでこの項目終わり。

ん~、どちらかと言うと時系列間を横断してデータを取る例を見たかったんだが何となく想像はつくので次へ進む事にする。