イントロダクション
Microservices with Clojureより。前回は、Pedestalを利用したClojureによる具体的なマイクロサービスの為のREST APIの実装について述べた。今回は次世代DB Datomicを利用したマイクロサービスの為のデータベース構築について述べる。
今回は、マイクロサービスに用いられるデータベースである次世代データベースDatomicについて述べる。Datomicはマイクロサーピスのようなデータ指向アプリケーション(データ指向アプリケーションとは、データの量や複雑さ、変化が課題となるアプリケーション;詳細は下の参考図書を参考)向けのデータを確実に保存し、取得する為の、基盤となるデータベースとなる。
マイクロサービスアプリケーションではしばしば、時間の経過とともに変化する可能性のあるユーザーの状態とともに、ユーザートランザクションを一貫して保存する必要がある。そのために、ユーザー状態を恒久的に更新して変更の履歴を失う代わりに、データの変更を維持し、時間の経過とともにクエリできるようにする必要がある。このような要件から、データベースに保存されるデータはイミュータブルであることが期待されている。Datomicはそのようなデータベースの一つで、耐久性のあるトランザクションを提供するだけでなく、ユーザーが一定期間にわたってデータベースの状態を照会できるように、不変性の概念をそのコアに組み込んでいるものとなる。DatomicはClojureで書かれたライブラリであり、AWSで提供されているクラウドサービスでもある。
Datomicの構造
DatomicはACIDトランザクションをサポートし、データをイミュータブルなファクトとして保存する分散型データベースとなる。Datomicは、基礎となるデータの一貫性を保つための堅牢なトランザクションマネージャ、不変のファクトを格納するためのデータモデル、そして時とともにファクトとしてデータを取り出すためのクエリーエンジンを提供することに重点を置いている。Datomicは独自のストレージを持たず、外部のストレージサービスに依存して、データをディスクに保存する。
Datomicと一般的なデータベースとの比較
一般的なデータベースは、ストレージエンジン、クエリエンジン、トランザクションマネージャを含むモノリシックなアプリケーションとして実装され、Clientがデータを保存したり取得したりするために接続する単一アプリケーションとしてパッケージ化されている。一方、Datomicは、トランザクションマネージャ(Transactor)を独立したプロセスとして分離し、すべてのトランザクションを処理し、Datomicが管理するすべてのデータの永続性ストアとして機能するストレージサービスにデータをコミットするという、アプローチをとっている。Datomicのハイレベルなアーキテクチャと、従来のデータベースとの比較を以下に示す。
DatomicのClientはPeerと呼ばれ、アプリケーションコードとPeerライブラリを持ち、Transactorと接続してデータを一貫して保存することができまる。Peerはまた、基礎となるストレージサービスにデータを問い合わせ、キャッシュを維持することで基礎となるストレージサービスの負荷を軽減する。また、Peerは同様にTransactorから更新を受け取り、キャッシュに追加する。DatomicのPeersは厚いClientで、データをインメモリでキャッシュしたり、Memcachedのような外部のキャッシュシステムを使ってオブジェクトを保存するよう設定することが可能となる。Peerによって維持されるキャッシュは、常に有効なイミュータブルなファクトを含んでいる。
Datomicはまた、従来のデータベースのような軽量なクライアントが直接接続してデータベースを照会できるようにするPeer Serverを備えている。このサーバーは、接続された全てのクライアントに対する中央クエリプロセッサーとして機能する。Datomicはコンソールも提供しており、スキーマの管理、トランザクションの調査、クエリの実行などをグラフィカルなユーザインターフェースで行うことができる。
Datomicはトランザクションデータ用に設計されており、ユーザープロファイル、注文、在庫詳細などを保存するために使用する必要がある。IoT に見られるような高スループットのユースケースには使用すべきではない。このユースケースでは、Redisのような時系列データベースを使用して、高速で入ってくるデータを保存する必要がある。
Development model
DatomicはPeerとClientの2つの開発モデルを提供している。どちらのモデルもトランザクションを処理し、データを一貫して保存するためにトランザクタが稼働している必要がある。Clientモデルでは、Transactorに加えてPeer Serverが、Clientからのストレージとクエリ要求を調整するために必要となる。
開発モデルおよび参加コンポーネントを下図に示す。
Clientモードの場合、トランザクタやストレージエンジンとのやり取りは全てPeer Serverが代行するため軽量だが、トランザクタやストレージエンジンと直接接続するのではなく、全てのリクエストがPeer Serverを経由するため、リクエストのホップが追加されることになる。DatomicはPeerモードとClientモードのためにそれぞれ独立したPeerライブラリとClientライブラリを提供している。
Data model
Datomicはデータをファクトとして保存し、各ファクトは5タプルとなる。これらのファクトはデータム(Datoms)と呼ばれる。各データムはエンティティID、アトリビュート、バリューからなり、5タプルの最初の3つの部分を形成している。4番目の部分は、ファクトが作成されたタイムスタンプを定義し、真を保持する。5番目の部分は、定義されたdatomがファクトの追加か削除かを決定するブーリアン値で構成されている。同じエンティティの複数のデータムは、属性と値をキーと値のペアにしたエンティ ティマップとして表現することができる。エンティティマップのエンティティIDはキー:db/idで定義される。
この例では、ID 1234 の注文エンティティに対して、4 つのorder/name, :order/status, :order/rating, および :order/contact という 属性が ID 1234 の注文エンティティに対して定義されている。
Schema
Datomicの各データベースは、エンティティに関連付けることができる全ての属性を定義するスキーマを有している。また、各属性に格納できる値の種類も定義されている。属性はそれ自体がdatomsとして取引される。つまり、属性もまたDatomicによって定義された関連属性を持つ実体とみなされる。Datomicがスキーマ定義でサポートする属性は下表のようになる。
Datomic では、オプションとして :db/doc, :db/unique, :db/index, :db/fulltext, :db/isComponent, :db/noHistory などのスキーマ属性が定義されている。db/ident, :db/valueType, および :db/cardinality 属性は必須となる。
Datomicはインデックスもサポートしており、 :db/index schema属性を使って、ある属性に対して有効化することができる。内部的には、EAVT、AEVT、AVET、VAET(Eは実体、Aは属性、Vは値、Tはトランザクション)の順番でデータムを格納する4つのインデックスを保持している。
Using Datomic
Datomicは、開発販売を行なっているCognitect社のサイトから自由にダウンロードすることができまる。利用する方法としてはAWS上のDatomic CloudとLocal-Devの2つがあるが、ダウンロードと設定に少し習熟度が必要な為、今回はMVNレポジトリに格納されているDatomic無償版を用いる。この無償版は、同時に2つのピアと組み込みストレージに制限されているが、Datomicの機能を試し、そのデータモデルで作業するには十分な性能を持っている。
Getting started with Datomic
Datomicのセットアップは、無料版のdatomic-free-x.x.xxx.zipをダウンロードし、解凍する。無料版のDatomicは、ライセンスキーは必要ない。その他のバージョンでは、Datomicが動作するためにトランザクタのプロパティに追加する必要があるライセンスキーを取得するための登録が必須となりる。Datomicの配布には、datomic-free-x.x.xxx.jarとdatomic-transactor-free-x.x.x.jarという2つのJARが含まれている。datomic-free JARファイルにはピアライブラリが、datomic- transactorにはトランザクタの実装が含まれている。また、配布物には、以下のようにDatomicコンポーネントを起動するために必要なスクリプトをすべて含むbinフォルダが含まれている。
datomic-free-0.9.5561.62 ├── bin
├── CHANGES.md
├── config
├── COPYRIGHT
├── datomic-free-0.9.5561.62.jar
├── datomic-transactor-free-0.9.5561.62.jar ├── lib
├── LICENSE
├── pom.xml
├── README
├── resources
├── samples
└── VERSION
5 directories, 8 files
無料版で始めるには、新しいClojure Leiningenプロジェクトを作成し、datomic-freeの依存関係を追加する。今回は前回述べたpedestalのプロジェクトをベースに構築することとし、以下のように、プロジェクト設定ファイルproject.cljに依存関係を追加した。
(defproject pedestal-play "0.0.1-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.11.1"]
[io.pedestal/pedestal.service "0.5.3"]
[frankiesardo/route-swagger "0.1.4"]
;; Remove this line and uncomment one of the next lines to
;; use Immutant or Tomcat instead of Jetty:
[io.pedestal/pedestal.jetty "0.5.3"]
;; [io.pedestal/pedestal.immutant "0.5.3"]
;; [io.pedestal/pedestal.tomcat "0.5.3"]
;; Datomic Free Edition
[com.datomic/datomic-free "0.9.5561.62"]
[ch.qos.logback/logback-classic "1.1.8"
:exclusions [org.slf4j/slf4j-api]]
[org.slf4j/jul-to-slf4j "1.7.22"]
[org.slf4j/jcl-over-slf4j "1.7.22"]
[org.slf4j/log4j-over-slf4j "1.7.22"]]
:min-lein-version "2.0.0"
:resource-paths ["config", "resources"]
...
:main ^{:skip-aot true} pedestal-play.server)
com.datomic/datomic-freeの依存関係は、Clojarsから必要な依存関係を取得する。Datomicディストリビューションは、bin/maven-installスクリプトも提供しており、ディストリビューションに含まれるJARをローカルのMavenリポジトリにインストールするためのbin/maven-installスクリプトも提供している。
DatomicのAPIを使い始めるために、以前”SublimeText4とVS code、LightTableでのClojureの開発環境立ち上げ“で述べたsublimetext4とClojureのプラグインであるClojure-Sublimedを利用する。それらの環境設定関しては、前述のページを参照のこと。まずメインのcore.cljの名前空間の宣言とapiライブラリの導入を行う。
(ns datomic-test.core)
(require '[datomic.api :as d])
Connecting to a database
データベースへの接続は、まずデータベースURIを定義し、datomic.api名前空間のcreate-database関数を使用してデータベースを作成する。この関数では、使用するストレージエンジン(インメモリならmem)、作成するデータベース(Helping Hands ordersならhhorder)を定義したデータベースURIを入力として受け取る。以下のように、データベースが正常に作成された場合は true を返す。
(require '[datomic.api :as d]) ;; nil
(def dburi "datomic:mem://hhorder") ;; #'pedestal-play.server/dburi
(d/create-database dburi) ;; true
(def conn (d/connect dburi)) ;; #'pedestal-play.server/conn
conn ;; #object[datomic.peer.LocalConnection 0x1946e1b5 "datomic.peer.LocalConnection@1946e1b5"]
Transacting data
Datomicでは、データベース内のエンティティに使用される属性を予め知っておく必要がある。属性(スキーマ)とファクト(データ)は共にdatomsとして、datomic.api名前空間のtransact関数を用いてdatomsとして取引される。データムは個々に、あるいはベクトルで包んで一つのトランザク ションにまとめることができる。例えば、hhorderのすべての属性を1つのトランザクションで処理するには、すべてのエンティティマップを1つのベクトル内にまとめて指定し、そのベクトルを引数としてtransaction関数の引数として渡す。
(def result
(d/transact conn [{:db/ident :order/name
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/doc "Display Name of Order"
:db/index true}
{:db/ident :order/status
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/doc "Order Status"}
{:db/ident :order/rating
:db/valueType :db.type/long
:db/cardinality :db.cardinality/one
:db/doc "Rating for the order"}
{:db/ident :order/contact
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/doc "Contact Email Address"}]))
エンティティマップの一部として :db/id キーが定義されていない場合、Datomic によって自動的に追加される。トランザクト関数は,Datomicの接続と,トランザクトする1つ以上のdatomのベクトルをパラメータとして受け取る。.この関数はプロミスを返す.プロミスを参照することで,トランザクションされたデータムや,データベースの前後の状態を確認することができる。
(pprint @result)
{:db-before datomic.db.Db@9ce299cf,
:db-after datomic.db.Db@cade2aa4,
:tx-data
[#datom[13194139534312 50 #inst "2022-10-05T05:34:58.054-00:00"
13194139534312 true]
#datom[63 10 :order/name 13194139534312 true]
#datom[63 40 23 13194139534312 true]
#datom[63 41 35 13194139534312 true]
#datom[63 62 "Display Name of Order" 13194139534312 true]
#datom[63 44 true 13194139534312 true]
#datom[64 10 :order/status 13194139534312 true]
#datom[64 40 23 13194139534312 true]
#datom[64 41 35 13194139534312 true]
#datom[64 62 "Order Status" 13194139534312 true]
#datom[65 10 :order/rating 13194139534312 true]
#datom[65 40 22 13194139534312 true]
#datom[65 41 35 13194139534312 true]
#datom[65 62 "Rating for the order" 13194139534312 true]
#datom[66 10 :order/contact 13194139534312 true]
#datom[66 40 23 13194139534312 true]
#datom[66 41 35 13194139534312 true]
#datom[66 62 "Contact Email Address" 13194139534312 true]
#datom[0 13 65 13194139534312 true]
#datom[0 13 64 13194139534312 true]
#datom[0 13 66 13194139534312 true]
#datom[0 13 63 13194139534312 true]],
:tempids
{-9223301668109598143 63,
-9223301668109598142 64,
-9223301668109598141 65,
-9223301668109598140 66}}
hhorder データベースに属性が定義されると、エンティティに関連付けることができるようになる。新しい注文を追加するには、次の例に示すように、登録された属性を使用してトランザクションを実行する。
(def order-result
(d/transact conn [{:db/id 1
:order/name "Cleaning Order"
:order/status "Done"
:order/rating 5
:order/contact "abc@hh.com"}
{:db/id 2
:order/name "Gardening Order"
:order/status "Pending"
:order/rating 4
:order/contact "def@hh.com"}]))
例えば、IDが1と2の2つの注文がhhorderデータベースに追加され、:db/idや他の定義された属性値で照会できるようになる。
Using Datalog to query
Datomicでは、データベースからデータを取り出す方法として、pullとquery の2つの方法を提供している。Datomicデータベースからファクトを取得する問い合わせ方法は、Datalogの拡張形式を使用している。データベースへの問い合わせを行なうdatomic.api の q 関数は、クエリを実行するデータベースの状態を知っている必要がある。データベースの現在の状態は、datomic.apiのdb関数を用いて取得することができる。
(d/q '[:find ?e ?n ?c ?s
:where [?e :order/rating 5]
[?e :order/name ?n]
[?e :order/contact ?c]
[?e :order/status ?s]]
(d/db conn))
Datomicの各クエリは、クエリ構造の一部として :find 節と :where 節、または :find 節と :in 節を持つ必要がある。クエリは一連の節を与えられると、与えられた節を満たす全ての事実をデータベースからスキャンして、事実のリストを返す。Datomicクエリ文法 は、データベースへの問い合わせの方法をすべて定義している。以下は、hhorderデータベースを問い合わせる例となる。
;; Returns only the entity ID of the entities matching the clause
(d/q '[:find ?e
:where [?e :order/rating 5]]
(d/db conn))
#{[1]}
;; find all the entities with the three attributes and entity ID
(d/q '[:find ?e ?n ?c ?s
:where [?e :order/name ?n]
[?e :order/contact ?c]
[?e :order/status ?s]]
(d/db conn))
#{[1 "Cleaning Order" "abc@hh.com" "Done"] [2 "Gardening Order"
"def@hh.com" "Pending"]}
;; using 'or' clause
(d/q '[:find ?e ?n ?c ?s
:where (or [?e :order/rating 4] [?e :order/rating 5])
[?e :order/name ?n]
[?e :order/contact ?c]
[?e :order/status ?s]]
(d/db conn))
#{[1 "Cleaning Order" "abc@hh.com" "Done"] [2 "Gardening Order"
"def@hh.com" "Pending"]}
;; using predicates
(d/q '[:find ?e ?n ?c ?s
:where [?e :order/rating ?r]
[?e :order/name ?n]
[?e :order/contact ?c]
[?e :order/status ?s]
[(< ?r 5)]]
(d/db conn))
#{[2 "Gardening Order" "def@hh.com" "Pending"]}
Achieving immutability
Datomicデータベースに存在する全てのファクトは不変であり、任意のタイムスタンプに対して有効となる。例えば、現在のデータベースの状態で、:db/id 2の注文のステータスは、Pending(保留)と設定されている。
(d/q '[:find ?e ?s
:where [?e :order/status ?s]]
(d/db conn))
;; #{[1 "Done"] [2 "Pending"]}
ここで、注文ID 2の:db/idとトランザクションを行い、注文ID 2の:order/status属性の値をDoneに更新する。
;; update the status attribute to 'Done' for order ID '2'
(def status-result (d/transact conn [{:db/id 2 :order/status "Done"}]))
;; query the latest state of database
(d/q '[:find ?e ?s :where [?e :order/status ?s]] (d/db conn))
;;#{[2 "Done"] [1 "Done"]}
取引後、注文ID 2のステータスは、データベースの現在の状態において更新されたステータスを表示するようになった。クエリは、注文ID 2のステータスがDoneになったことを示しているが、Datomicは、注文ID 2のステータスの値を上書きしていない。代わりに、最近のトランザクションのタイムスタンプを持つ新しいdatomを追加するものとなる。データベースの現在の状態、つまりdatomic.apiのdb関数を用いてクエリを実行すると、常に最新のタイムスタンプを持つファクトが返される。
注文2の以前の状態を取得するには、状態を更新したトランザクションの前のデータベースの状態を使用する。transactの戻り値には:db-beforeキーが含まれており、これを利用してトランザクション前のデータベースの状態に対して同じステータスクエリを実行することができる。
;; query the status on previous state
(d/q '[:find ?e ?s
:where [?e :order/status ?s]]
(@status-result :db-before))
;; #{[1 "Done"] [2 "Pending"]}
その結果、注文ID 2の以前の状態、つまり「保留」が返される。イミュータビリティはDatomicデータベースの最も強力な機能の一つであり、データベースの変更を追跡するのに非常に有効なものとなる。
Deleting a database
既存のデータベースを削除するには、datomic.api名前空間のdelete-database関数を使用する。削除対象のデータベースのURIを入力とし、以下の例のように削除に成功した場合はtrueを返す。
(d/delete-database dburi)
;; true
次回はClojureでのAuthとPedestalを使ったAPIを使ったマイクロサービスでのセキュリティについて述べる。
コメント
[…] 次世帯DBであるDatomicを利用したマイクロサービスの為のデータベース構築 […]