Datomicは、シンプルなサービスで組み立てられた新しいデータベースです。従来のRDBMSの機能性と新世代の冗長な分散ストレージシステムの柔軟なスケイラビリティを両立しようとしています。
動機
Datomicは、次のゴールを目指しています。
- その場で更新しない、堅牢な情報モデルの提供
- 冗長でスケーラブルなストレージシステムの強化
- ACIDトランザクションと一貫性の提供
- アプリケーションでの宣言的なデータプログラミング
Datomicは、情報システムであるデータベースと考えられています。情報は事実の集合であり、事実は起きたことです。人は過去を変えられません。つまり、データベースはその部分を更新するのではなく、事実を蓄積するものなのです。過去は忘れられる一方で、変えることはできません。そのため、誰かが住所を「変更」したら、Datomicは過去の事実を置き換えるのではなく、新しい住所を持つという事実を記憶します。(現時点のことが単に格納されています。) この不変性により、数多くの重要なアーキテクチャに関する利点と機会が生まれます。前回の記事では、Datomicアーキテクチャのことを書きました。この記事では、情報モデルとプログラミングの経験に注目します。
従来のデータベース(と数多くの新しいデータベース!)は、「今」、目の前にある事実、に注目しますが、そうすることで情報を失っています。ビジネスは歴史的情報の中により多くの価値を見出し、その情報を保存しない理由はほとんどありません。バックアップやログのように、単に履歴を手元に残すかどうかという問題ではなく、アクティブな意思決定プロセスをサポートできるようにするのです。あなたに荷物を送るために現住所を知っておくことは、ビジネス上必要ですが、「どの顧客が頻繁に引っ越しをするのか、そして、どこから移動するのか?」というのは、マーケティング部門や製品開発チームにとって、とても興味深いことかもしれません。サプライヤ価格の履歴等も同様です。サプライヤは、サプライヤ価格の履歴を知るために、バックアップを復元したり、ログを再読み込みしたりしたくはないでしょう。
なぜアクティブな履歴を保持することが問題になるのか考えてみるのはおもしろいでしょう。結局、コンピュータ以前は、記録をどんどん増やし続けました。あることわざのように「会計士は消しゴムを使いません。」 私の推測では、初期の計算システムは単にその容量を持っていませんでした。(または、誰もその余裕がありませんでした) しかし、この推測は再考に値します。過去25年間で容量は何百万倍も増加しています。自分たちのコードベースがもはやフロッピィディスクに合わないからといって、gitのような版管理システムを使わない開発者がいるでしょうか?
データベースは、主にデータを通して提供する力のおかげでデータベースだと言えます。他の点では、単なるストレージシステムです。通常、この力は、データの組織化 (例えば、インデックス) と、組織に影響を与えるクエリシステムの組み合わせから来ています。開発者たちは、興味深い、ますます広く使われる冗長な分散ストレージシステムを、自由に手に入れていますが、しばしばその影響力を失っています。Datomicはこれらのストレージシステム上で動き、スケイラビリティや組織化した情報の保存、そして、開発者自身の手に力を取り戻そうとしています。
構造と表現
データベースは、リレーション、列とドキュメント等、そのモデルの下部にある基本ユニットを持ちます。Datomicのユニットは、私たちがDatomと呼ぶ原子的な事実です。
Datomは以下のコンポーネントを持ちます。
- Entity
- Attribute
- Value
- Transaction (データベース時間)
- Add/Retract
この表現は、RDFステートメントのSubject/Predicate/Objectデータモデルと明らかに似ています。しかし、時間の経過を表す概念や取り消しの適切な表現がないため、RDFステートメントは履歴情報を十分に表現できません。ビジネス情報システム指向であるDatomicはクローズドワールドの仮定を採用しています。そして、semantic webの万国共通の名前を持ち、オープンワールドで共有されたセマンティクスへの挑戦を避けています。Datomは事実の必要最小限の表現です。
モデルの底に原子ユニットを持つことで、トランザクション等の新しいものの表現は、新しい事実自体の大きさでしかありません。これに対して、一部を更新するためにドキュメント全体を再提出したり、それを避けようとしてデルタスキーマで不安定になったりすることを比べてみましょう。
Datomは、1つのフラットで普遍的なリレーションを構成します。Datomicに対する構造的なコンポーネントは他にありません。これは重要です。モデルに構造的なコンポーネントが増えれば増えるほど、アプリケーションは柔軟性を失っていきます。例えば、従来のリレーショナルデータベースでは、リレーションはそれぞれ名前を持たなければならず、データを配置するにはそれらの名前を知らなければなりません。さらに悪いことに、例えば、many-to-manyリレーション等のモデルを作成するために任意のジョインテーブルを作成する必要があります。これらの構造の名前は、同様に知られていなければなりません。物理的な構造化決定からアプリケーションを分離するために、論理ビューを提供するよう最大限の努力を払わなければならないでしょう。しかし、これらのビューは同様に数が多くて、固有のものになっています。ドキュメント内の階層はアプリケーションの中でハードコーディングされているので、構造から間接的に提供するビューのようなツールがあるとしても、ドキュメントの保存は構造的にさらに変更が難しいでしょう。
スキーマ
すべてのデータベースにはスキーマがあります。その違いは、どれだけ明示的なスキーマをサポートするか、または、必要とするかということです。Datomicの場合、属性は使用前に定義しなければなりません。
属性はエンティティそのものであり、とりわけ以下の属性があります。
- 名前
- 値のデータ型
- カーディナリティ (属性が複数の値を持てる)
- 一意性
- インデックスプロパティ
- コンポーネントの本質 (あなたの足はあなたのコンポーネントだが、あなたの母親はそうではない。)
- ドキュメンテーション
エンティティに当てはまる属性に制約はありません。そのため、エンティティはオープンでわずかしかありません。属性はエンティティの中で共有され、名前空間は衝突を避けるために使われます。次のように指定します。
:person/name
属性:
{:db/ident :person/name, :db/valueType :db.type/string, :db/cardinality :db.cardinality/one, :db/doc "A person's name"}
Datomicのすべての相互作用のようにスキーマは、上記でedn形式のマップで表しているデータによって表されます。DDLはありません。
これら単純なdatomのプリミティブ型と、ほんのわずかで、おそらく複数の値を持つ属性を用いて、スキーマは、行のタプル、階層的なドキュメントのエンティティ、列保存のカラム、グラフ等を表します。
トランザクション
最も基本的なレベルにおいて、Datomicのトランザクションは、原子的にデータベースに送られて受け入れられたアサーションや取り消しの単なる一覧です。基本的なトランザクションは、ただのdatomの一覧です。
[[:db/add entity-id attribute value] [:db/add entity-id attribute value]...]
もう一度言いますが、Datomicのすべての相互作用は、データによって表されます。上記では、edn形式で一覧にしたものを表しています。内部リストは次のような順番でdatomを表しています。
[op entity attribute value]
同じエンティティで複数の事実をデータベースに送りたければ、代わりにマップを利用できます。
[{:db/id entity-id, attribute value, attribute value} ...]
記事の中では、このようにテキストで表現する必要がありますが、トランザクションが実は普通のデータ構造(すなわち、j.u.Lists、j.u.Maps、配列等)であり、使用する言語で構築できるものであることは、Datomicの設計にとってとても重要なことです。Datomicへの第一のインタフェースは、文字列でもDMLでもなく、データです。
datomのトランザクションの決め方に注意してください。決めるのはトランザクタです。そうは言っても、トランザクションはそれ自体エンティティであり、その起源となるメタデータ、外部時間、起点となるプロセス等、トランザクション自体の事実をアサートできます。
もちろん、すべての変換が、最後の人が勝つレースやコンフリクトに委ねられることなく、単なるアサーションや取り消しとして表されるのではありません。このように、Datomicはデータベース関数の概念をサポートします。これらはJavaやClojure等、普通のプログラミング言語で書かれた関数であり、もちろんトランザクション中にデータとして送信されて、データベースにインストールされています。一度インストールされると、データベース関数の「call」は、トランザクションの一部になります。
[[:db/add entity-id attribute value] [:my/giveRaise sally-id 100] ...]
トランザクションの一部として使われる時、データベース関数はトランザクション関数とみなされます。そして、データベース自体のトランザクション内の値である追加された最初の引数が渡されます。このようにして、関数はクエリ等を実行します。トランザクション関数はトランザクションデータを返さなければなりません。返されるデータが何であれ、トランザクション内で置き換えられます。すべてのトランザクション関数が単純なadd/retractを返すまで、このプロセスが繰り返されます。こうして、上記のトランザクション内で、giveRaise関数はSallyの現在の給与を調べ、45,000であることを見つけ出し、新しい値に関するアサーションを返し、トランザクションデータの結果は次のように表されます。
[[:db/add entity-id attribute value] [:db/add sally-id :employee/salary 45100] ...]
:employee/salaryはカーディナリティが1なので、Sallyの給与にこの事実を追加することは、暗に前の事実を取り消します。トランザクション関数はトランザクション内で原子的に連続して実行されるので、任意のコンフリクトのない変換を実行するのに使われます。データベース関数については、このドキュメントでさらに読むことができます。
コネクションとデータベースの値
書き出し側について、特に変わったことはありません。URIを利用してデータベースに接続します。URIは、ストレージへ到達する方法やストレージを通して現在の処理者と通信する方法等の情報を含んでいます。トランザクションは上記で述べたようにトランザクションデータを渡して、接続中にtransact関数を呼ぶことで発生します。
読み込み側では、全く異なります。従来のデータベースでは、読み込みとクエリも接続関数です。接続中にクエリを渡し、現在のデータベースの状態の通常は再生できないコンテキストの中で実行されるサーバに到達します。これは、サーバに埋め込まれたクエリ言語の制限次第であり、ライタを含む他のすべてのユーザと共に、リソースと同期を奪い合います。
一方、Datomicでは接続の唯一のread操作はdb()であり、実際に回線を通じて読み出すのではありません。その代わりに、接続は継続的に十分な情報を送り続け、アプリケーション内で不変なオブジェクトとして使うデータベースの値をすぐに届けます。このようにデータの消費やクエリ等はローカルで起こります。 (エンジンは、必要に応じてデータを取得するためにストレージを自由に読み出します。) データベース全体がアプリケーション毎のサーバピアに保持されているのではないことに注意してください。保持されているのは、最新部分とストレージのその他の部分へのポインタだけです。また、「スナップショットをとる」操作は起きません。アプリケーションやクエリエンジンはデータベースを所有している感じがしますが、実際にはかなり軽く、メモリとストレージの永続するデータ構造への2、3の参照があるだけです。広範囲に渡るキャッシングが内部で起きています。
クエリ
Datomicでは、クエリは接続関数ではなく、データベース関数でもありません。その代わり、クエリは引数として1つ以上のデータソースをとるスタンドアロン関数です。これらのデータソースはデータベースの値や普通のデータコレクション、または、その組み合わせでしょう。これは、データベースのコンテキスト内で実行されることからクエリを自由にする大きな恩恵をもたらします。
Datomicピアライブラリには、Datalogに基づくクエリエンジンが付属しています。Datalogは、クエリされたdatomとインメモリコレクションに向いている、パターンマッチング風のロジックに基づく宣言型クエリ言語です。
クエリの基本型は以下の通りです。
{:find [variables...] :where [clauses...]}
または、次のようにタイプするのが簡単なリスト形式があります。
[:find variables... :where clauses...]
もう一度言いますが、これらは、プログラム的に構築できるデータ構造をただテキストで表したものです。クエリはデータであり、文字列ではありません。文字列は提供されたときに、受け入れられてデータに変換されます。
これらのdatomを含むデータベースがある場合、(sally、fred、ethelはエンティティIDの代わりです)
[[sally :age 21] [fred :age 42] [ethel :age 42] [fred :likes pizza] [sally :likes opera] [ethel :likes sushi]]
次のようなクエリが書けます。
;;who is 42? [:find ?e :where [?e :age 42]]
結果は以下の通りです。
[[fred], [ethel]]
:where節は位置的に一致し、データソースにとって各datomは以下のタプルと一致します。
[entity attribute value transaction].
この場合のトランザクションである右側は省略できます。?で始まるシンボルは、変数であり、その結果には一致するソースタプルの変数の値であるタプルが含まれます。
Joinは暗に示され、1回以上変数を使ったときにはいつでも起こります。
;;which 42-year-olds like what? [:find ?e ?x :where [?e :age 42] [?e :likes ?x]
結果は以下のようになります。
[[fred pizza], [ethel sushi]]
クエリのAPIはqと呼ぶ関数です。
Peer.q(query, inputs...);
inputsはデータベース、コレクション、スカラ等です。クエリは、また、(再起的な)ルールを利用し、自分自身のコードを呼びます。クエリに関する情報は、こちらのドキュメントで参照できます。
全部まとめると以下の通りです。
//接続 Connection conn = Peer.connect("a-db-URI"); //データベースの現在の値を取得 Database db = conn.db(); //ストリングを使う。Javaはコレクションを持たない。 String query = "[:find ?e :where [?e :likes pizza]]"; //誰がピザを好きか? Collection result = Peer.q(query, db);
同じクエリ、異なる基礎
dbがすべての履歴情報を持つという事実を活用すると、物事はおもしろくなり始めます。
//先週、ピザを好きだったのは誰? Peer.q(query, db.asOf(lastTuesday));
データベースのasOfメソッドは、日付時間やトランザクションによって決められる前回のポイントで、そのデータベースのビューを返します。接続を戻したり、クエリを変更したりしないことに注意してください。今まで自分のタイムスタンプを動かしたことがあるならば、一時的に当てはまるクエリが、普通、「今」とは異なることをお分かりでしょう。似たようなものでは、sinceメソッドがあります。
//もしBrooklynのみんなを追加したらどうなるだろうか? Peer.q(query, db.with(everyoneFromBrooklyn));
withメソッドはトランザクションデータを取得して、データが追加されたデータベースのローカルの値を返します。接続を通してトランザクションは発生しません。このように推論的な仮のクエリを実行したり、トランザクションが発生したりする前に、トランザクションデータを確認できます。また、filterメソッドは、ある属性によってフィルタリングされたデータベースを返します。もう一度言いますが、この時に接続やdb、クエリには触れていません。
それでは、データベースを設定せずにクエリをテストしたい場合はどうでしょうか? 同じ形でデータを提供できます。
//データベースを使わずにクエリをテストする Peer.q(query, aCollectionOfListsWithTestData);
ここでまた言いますが、クエリは変更されていなくても、実際に動いています。対照的に、データベース接続をモックしています。
今までのところ、すべてのテクニックは、過去か未来の特定のポイントで動いています。しかし、多くの興味深い分析では、時間を超えて見たいはずです。
//今までピザが好きだった人は誰? Peer.q(query, db.history());
historyメソッドは、時間を超えてすべてのdatomを返します。これは、asOf等と組み合わせられます。このクエリはそのままの状態で動くために使われますが、通常、時間を超えるクエリは異なっていて、集合体等になります。
クエリは1つ以上のデータソースを取得でき、データベースを簡単に組み合わせたり、同じデータベースの様々なビューを使ったりすることができます。クエリにコレクションを渡せることは、ステロイド剤を使用したパラメータ化状態のようなものです。
異なるクエリ(または、参加者)、同じ基礎
データベースの値は不変なため、トランザクションではないマルチステップの計算ができ、何も変更されていないことが分かります。同様に、データベースの基礎ポイントを取得して、別プロセスに渡せます。そして、同じ状態のデータベースの値を取得できます。このように、プロセスや時間によって分けられた異なるクエリは、まったく同じ基礎で動きます。
ダイレクトインデックスアクセス
最後に、データベースの値は、不変なインデックスから基礎となるソートされたdatomに何度もアクセスするために、ハイパフォーマンスなAPIを提供します。これは、他のクエリアプローチをビルトする原料になります。例えば、このAPIを通して、ClojureのPrologのようなcore.logicライブラリを使い、Datomicデータベースをクエリできます。
サマリ
この記事で、Datomic情報モデルの本質といくつかの詳細を知ってもらうことを望んでいます。データベースを値として扱うことは、とても特別で、効果的です。私は、みんなでその可能性を明らかにしているところだと思っています。Datomicのドキュメントからさらに学ぶことができます。
著者について
Rich Hickey氏はClojureの作者であり、Datomicの設計者です。また、さまざまなドメインで、25年以上の経験を持つソフトウェア開発者です。スケジューリングシステム、放送の自動化、オーディオ分析と指紋採取、データベース設計、収益管理、出口調査システム、マシンリスニングに、様々な言語で取り組んできています。