Why Datomic rocks, in code snippets (2017)

This article predates Datomic Cloud so the APIs may be slightly different but the information model is unchanged.

1. Datomic has a reference type

This is what makes Datomic simple and easy. First-class reference type means you can walk to anything you need. Datomic is naturally graph-shaped, graphs are meant to be walked.

(def $ (d/db (d/connect "datomic:free://datomic:4334/seattle")))

(->> (d/q '[:find ?e . :where [?e :community/name "Alki News"]] $) (d/entity $) :community/neighborhood :neighborhood/district :district/name) "Southwest"

The entity API d/entity is supposed to evoke feelings of programming directly with Clojure values like sets and maps. This property of Datomic is called database as a value.

With table-oriented RDBMS, to work with sets and maps, first you need to deal with the foreign key joins, cartesian products, sparse resultsets, many-to-many join tables. This is all your job and this mapping is part of an abstraction-resistant class of problems.

2. You can mix your application code and your database code

The following Datomic Peer query calls into Clojure functions from inside a database query.

(d/q '[:find (pull ?e [:db/id :community/name :community/url]) :where [?e :community/name ?name] [(.startsWith ?name "Alki")]] ; <------ JVM call from datalog $) [[{:db/id 17592186045448, :community/name "Alki News", :community/url "http://groups.yahoo.com/group/alkibeachcommunity/"}] [{:db/id 17592186045449, :community/name "Alki News/Alki Community Council", :community/url "http://alkinews.wordpress.com/"}]]

This is fast and idiomatic. Here is a more advanced example.

; Define a fn at the REPL (defn matches [$ e a v] (-> (datomic.api/entity $ e) (get a) (= v)))

(d/q '[:in $ ?needle :find (pull ?e [:community/name :community/category]) :where [?e :community/category] ; Call the function [(user/matches $ ?e :community/category ?needle)]] $ #{"events" "news"}) [[#:community{:name "belltown", :category ["events" "news"]}] [#:community{:name "Greenwood Blog", :category ["events" "news"]}]]

To do that in RDBMS you'd need to do a lot of service/database round trips over network (or stored procedures). This property of Datomic is called code/data locality and it makes possible to do functional programming – to code with pure functions and map and reduce – inside your database programs.

The fact that this works gives a major hint as to how Datomic works. My user/matches function was defined at a REPL, it is only available right here inside my process, which means the Datomic query engine is also running in my process. The datalog query engine is known as the Datomic Peer library, it really is just a function, a jar file running in your REPL. 🤯

3. Schema can be queried

(d/touch (d/entity $ :post/content)) #:db{:id 135, :ident :post/content, :valueType :db.type/string, :cardinality :db.cardinality/one}

4. No need to batch queries, so you can spread them over many functions

We can use functions to build little query abstractions:

(ref? $ :post/content) ; false (many? $ :post/content) ; false (one? $ :post/content) ; true

(defn ref? [$ k] (= :db.type/ref (:db/valueType (d/entity $ k)))) (defn one? [$ k] (= :db.cardinality/one (:db/cardinality (d/entity $ k)))) (defn many? [$ k] (= :db.cardinality/many (:db/cardinality (d/entity $ k)))) (defn component? [$ k] (:db/isComponent (d/entity $ k)))

Splitting queries into functions works because these queries can be done independently, it is not idiomatic to batch them all into one monster query. These functions are silly, but they make the below more readable:

5. Walk the database as if it were a local data structure

You can use complex functions inside your queries, like Loom , a general purpose graph library for Clojure that doesn't know anything of Datomic, it predates Datomic by two years.

(->> (d/q '[:find [?e ...] :where [?e :community/name]] $) (clone-entities $) ; <---- awesome abstraction (take 5)) ({:db/id "0", :community/name "Beacon Hill Burglaries", :community/url "http://maps.google.com/maps/ms?ie=UTF8&hl=en&msa=0&msid=107398592337461190820.000449fcf97ff8bfbe281&z=14or", :community/neighborhood #:db{:id "1"}, :community/category ["criminal activity"], :community/orgtype #:db{:id "66"}, :community/type [#:db{:id "200"}]} {:db/id "1", :neighborhood/district #:db{:id "179"}, :neighborhood/name "Beacon Hill"} {:db/id "2", :db/ident :region/se} {:db/id "3", :community/name "ballardite blog", :community/url "http://www.ballardite.blogspot.com/", :community/neighborhood #:db{:id "123"}, :community/category ["news" "personal"], :community/orgtype #:db{:id "43"}, :community/type [#:db{:id "156"}]} {:db/id "4", :neighborhood/district #:db{:id "40"}, :neighborhood/name "Greenwood"})

(defn clone-entities "Clone a Datomic entity and any reachable entities by walking the entity graph breadth-first with Loom." [$ es] (let [traverse #(loom/bf-traverse (partial datomic-entity-successors $) %) reachable-es (->> es (mapcat traverse) set)] (->> (seq reachable-es) (d/pull-many $ ['*]) (mapv (partial alter-ids (tempids reachable-es)))))) (defn datomic-entity-successors "Excludes component because component is considered part of its parent" [$ e] (->> (-> (d/pull $ ['*] e) (dissoc :db/id)) (mapcat (fn [[k v]] (if (and (ref? $ k) (not (component? $ k))) (cond (one? $ k) [(:db/id v)] (many? $ k) (mapv :db/id v)))))))

To do this in RDBMS you'd need a crazy complicated stored procedure. The Datomic solution is general purpose and beautiful and abstract.

6. Datomic can query N databases at the same time, in the same query

This Hyperfiddle screenshot (from 2017) uses color to distinguish data from two databases:

This query is efficient! It works because Datomic databases are logs and logs can be interleaved. Nobody is really talking about it yet but this one is the real game changer:

Datomic solves data silos.