-
Notifications
You must be signed in to change notification settings - Fork 20
Datomic database functions used from normal code
eponai.common.database.functions
is an old namespace that we used for our first product. It contains a macro for defining functions that can be used from ClojureScript and Clojure as well as a Datomic function.
A Datomic function is just data. That they can be sent over the wire, stored in Datomic, read back out and executed. You can create Datomic functions with datomic.api/function
which takes code, params, language and which namespaces to require and import. The problem is that you can't freely split these functions apart to reuse functionally as you normally do with functions, and there's (to my knowledge) no way to compose these functions. So we created a def-dbfn
macro to allow us to define these functions like clojure.core/defn
, use them from Clojure and ClojureScript as well as get a composed datomic.api/function
.
The eponai.common.database.functions/def-dbfn
macro is similar to clojure.core/defn
but it takes a map of :requires
(namespaces to require) and :deps
, which is a sequence of other functions that have been defined with def-dbfn.
In this example we'll define 2 functions, silent-cas
and silent-cas-update
.
-
silent-cas
compares the current value for an attribute of an entity, and if they are equal, sets a new value. -
silent-cas-update
compares the current value with a passed one, sets the new value by running a function on the current value.
(def-dbfn silent-cas {:requires ['[datomic.api]]}
[db entity attr value new-val]
(when (= value (get (datomic.api/entity db entity) attr))
[[:db/add entity attr new-val]]))
(comment
;; Callable from Clojure or ClojureScript as it's just a function.
(silent-cas db 1 :foo/bar 2 3)
;; Can return a `datomic.api/function` by calling dbfn on it.
(dbfn silent-cas)
;; outputs =>
#db/fn{:lang :clojure,
:imports [],
:requires [[datomic.api]],
:params [db entity attr value new-val],
:code "(when (= value (get (datomic.api/entity db entity) attr)) [[:db/add entity attr new-val]])"})
(def-dbfn silent-cas-update
{:requires ['[datomic.api]]
:deps [{:dbfn silent-cas
;; Uses this symbol for a let to surround the body of this code.
;; Was originally used to use different implementations of the var.
:provides 'silent-cas
;; Can optionally memoize the function it depends on.
:memoized? false}]}
[db entity attr value f]
(silent-cas db entity attr value (f (get (datomic.api/entity db entity) attr))))
(comment
(dbfn silent-cas-update)
;; outputs =>
;; The silent-cas function is inlined in the let expression.
#db/fn{:lang :clojure,
:imports [],
:requires [[datomic.api]],
:params [db entity attr value f],
:code "(clojure.core/let [silent-cas (clojure.core/cond-> (clojure.core/fn [db entity attr value new-val] (do (clojure.core/require (quote [datomic.api]))) (when (= value (get (datomic.api/entity db entity) attr)) [[:db/add entity attr new-val]])) (clojure.core/or false false) (clojure.core/memoize))] (silent-cas db entity attr value (f (get (datomic.api/entity db entity) attr))))"})
Note: After having written the example, I realize that you can't pass a function - as I have in the examples - to the database function. Hopefully you get the idea anyway.