Friday, February 8, 2019

從 SQL 到 Datomic query (datalog)

要理解新的概念,一般而言,用舊的、已經熟悉概念來加以連結新的概念,會是比較容易的方法。另一方面,要讓人們去接受新的解決方案,先確定舊的問題都可以在新的解法中解決,也是有效增加人們信心的作法。

當我使用 SQL 來開發 web application 時,最常用的 SQL query 是什麼呢? 其實未必是複雜的 join operation 。最常用的,反而是樸實無華的查看單一或是多個 rows:
(1) SELECT * FROM A
(2) SELECT * FROM A WHERE A.col = b

那麼,這麼簡單的 SQL query ,如果是在 Datomic 的世界,又是怎麼對應呢?我在 datomic 官方的 mbrainz-sample 找出的一段 query 。用了之後,覺得非常容易可以對應上述的兩種情境。sample code  在下方
;; These functions borrowed from https://github.com/Datomic/mbrainz-sample
(ns datomic.samples.query
(:require [datomic.api :as d]))
(defn only
"Return the only item from a query result"
[query-result]
(assert (= 1 (count query-result)))
(assert (= 1 (count (first query-result))))
(ffirst query-result))
(defn qe
"Returns the single entity returned by a query."
[query db & args]
(let [res (apply d/q query db args)]
(d/entity db (only res))))
;; Given that A.attr is primary key
;; Similar usage pattern to SQL expression: `SELECT * FROM A WHERE A.attr = val`
(defn find-by
"Returns the unique entity identified by attr and val."
[db attr val]
(qe '[:find ?e
:in $ ?attr ?val
:where [?e ?attr ?val]]
db attr val))
;; similar to `find-by`, but more general because this function allow nil as answer.
(defn find-one-by
"Given db value and an (attr/val), return the user as EntityMap (datomic.query.EntityMap)"
[db attr val]
(d/entity db
(d/q '[:find ?e .
:in $ ?attr ?val
:where [?e ?attr ?val]]
db attr val)))
(defn qes
"Returns the entities returned by a query, assuming that
all :find results are entity ids."
[query db & args]
(->> (apply d/q query db args)
(mapv (fn [items]
(mapv (partial d/entity db) items)))))
;; Similar usage pattern to SQL expression: `SELECT * FROM A`
(defn find-all-by
"Returns all entities possessing attr."
[db attr]
(qes '[:find ?e
:in $ ?attr
:where [?e ?attr]]
db attr))
view raw SQL2Datomic.clj hosted with ❤ by GitHub
舉例來說明:
如果有一個「使用者」的概念,要用資料庫加以建模。使用者有電子郵件、密碼、名字三種屬性。同時,我們需要一個資料庫查詢 (query) ,可以根據電子郵件來查出對應的使用者

1. 用 RDB 來建模的話,這個 sql query 會長成這樣子:
SELECT * FROM user WHERE user.email = "ecoboy@qwerty.com";

2. 用 Datomic 來建模的話,一旦利用上述的 utility functions ,這個 query 就可以用下列的函數產生。
(find-by db :user/email "ecoboy@qwerty.com")

附註:
(a) 在 SQL best practices 裡,因為要考慮效率,往往不建議用 select * 這種 query 直接放在 production code 裡。
(b) 在 Datomic  best practices 裡,因為 datomic 的 query 並不需要往返 client-server ,同時 Entity 有 lazy evaluation 的特性,一般而言,推荐的作法是直接取回 Entity ,相當於 SQL 裡的 row 概念。