Skip to content

Instantly share code, notes, and snippets.

@augustl
Last active December 24, 2015 13:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save augustl/6806619 to your computer and use it in GitHub Desktop.
Save augustl/6806619 to your computer and use it in GitHub Desktop.
(ns user)
(do
(require '[datomic.api :as d])
(require '[datomic-demo.util :as util])
(import 'java.util.Date)
(def datomic-url "datomic:mem://baksia-demo")
(d/create-database datomic-url)
(def datomic-conn (d/connect datomic-url)))
(d/transact
datomic-conn
[{:db/id #db/id[:db.part/db]
:db.install/_attribute :db.part/db
:db/unique :db.unique/value
:db/ident :user/email
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one}
{:db/id #db/id[:db.part/db]
:db.install/_attribute :db.part/db
:db/ident :user/password-hash
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one}])
;; Tempids are used to "create" new entities. We can use the tempid multiple times
;; to set up relational data for multiple tempids in one transaction, before we have
;; actual entity IDs. See product/order/delivery examples below.
(def user-tempid (d/tempid :db.part/user))
(def insert-tx
(deref
(d/transact
datomic-conn
[[:db/add user-tempid :user/email "august@augustl.com"]
[:db/add user-tempid :user/password-hash "123abc"]])))
;; We ask datomic for the actual entity ID that was created for our tempid.
(def our-user-eid (d/resolve-tempid (:db-after insert-tx) (:tempids insert-tx) user-tempid))
;; We ask datomic for an entity. Datomic just stores fact, entities aren't stored, but we can derive
;; entities from multiple facts about the same entity ID, the "entity" function does this for us.
(def our-user (d/entity (:db-after insert-tx) our-user-eid))
;; The value is lazy. Iterate it, or just read out (:user/email our-user), or use "touch" to
;; do all of that automatically. Note that this is about decompressing facts from the chunks that
;; has probably allready been received from storage - no networking happens, so there's no point in
;; doing "touch" to get an "atomic" fetch over the network or w/e. It's just for convenience/debugging.
(d/touch our-user)
;; Update looks exactly like create, except we use the actual entity ID instead of
;; a tempid for our facts.
(def update-tx
(deref
(d/transact
datomic-conn
[[:db/add (:db/id our-user) :user/email "august@kodemaker.no"]])))
;; Retract an entity
(def retract-tx
(deref
(d/transact
datomic-conn
[[:db/retract (:db/id our-user) :user/email "august@kodemaker.no"]])))
(def retract-entity-tx
(deref
(d/transact
datomic-conn
[[:db.fn/retractEntity (:db/id our-user)]])))
;; At this point, it's fun to look at the DB at different points in time.
(def our-user (d/entity (:db-before insert-tx) our-user-eid))
(def our-user (d/entity (:db-after insert-tx) our-user-eid))
(def our-user (d/entity (:db-before update-tx) our-user-eid))
;; and so on
;; Note that we don't have this power just because we happen to have the db-before/after values
;; from running the transactions, we can at any point ask datomic for, say, the database as of
;; two weeks ago, and get an immutable value (the db is an immutable value) that represents the
;; value of the database as of that point in time.
(comment
;; Direct index lookup.
;; :eavt = entity, attribute, value, time
;; The "datoms" function lets us look up directly from index. It takes a variable
;; amount of arguments.
(seq (d/datoms (:db-after insert-tx) :eavt ,,,))
;; For :eavt, the first argument is the entity ID.
(seq (d/datoms (:db-after insert-tx) :eavt our-user-eid))
;; We can go deeper in the index and get the facts for a specific attribute for an entity ID
(seq (d/datoms (:db-after insert-tx) :eavt our-user-eid :user/email))
;; :avet is another index, where we can look up by attribute
(seq (d/datoms (:db-after insert-tx) :avet :user/email)) ;; email is "unique" and also indexed.
;; As with :avet we can also go deeper. :a is :user/email, :v is "august@augustl.com". This will give
;; us a index lookup for all facts with that value (can be multiple if we use "indexed" instead of "unique" in schema)
(seq (d/datoms (:db-after insert-tx) :avet :user/email "august@augustl.com"))
;; Querying one data source (implicit, see below)
(d/q '[:find ?e :where [?e :user/email "august@augustl.com"]] (:db-after insert-tx))
;; Querying with explicit data source (:in $db)
(d/q '[:find ?e :in $ :where [?e :user/email "august@augustl.com"]] (:db-after insert-tx))
;; Querying with explicit named data source (:in $db).
(d/q '[:find ?e :in $db :where [$db ?e :user/email "august@augustl.com"]] (:db-after insert-tx))
;; Querying with two data sources. Assumes the vector of vectors represents a parsed excel spreadsheet, with
;; three colums: the entity ID, the persons name, and the persons balance.
(d/q '[:find ?balance :in $db $excel :where [$db ?e :user/email "august@augustl.com"] [$excel ?e ?balance]] (:db-after insert-tx) [[123 "John Doe" 56] [our-user-eid "August" 100]]))
(d/transact
datomic-conn
[{:db/id #db/id[:db.part/db]
:db.install/_attribute :db.part/db
:db/ident :user/orders
:db/valueType :db.type/ref
:db/cardinality :db.cardinality/many}
{:db/id #db/id[:db.part/db]
:db.install/_attribute :db.part/db
:db/ident :order/products
:db/valueType :db.type/ref
:db/cardinality :db.cardinality/many}
{:db/id #db/id[:db.part/db]
:db.install/_attribute :db.part/db
:db/ident :product/name
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one}
{:db/id #db/id[:db.part/db]
:db.install/_attribute :db.part/db
:db/ident :product/price
:db/valueType :db.type/bigdec
:db/cardinality :db.cardinality/one}
{:db/id #db/id[:db.part/db]
:db.install/_attribute :db.part/db
:db/ident :delivery/order
:db/valueType :db.type/ref
:db/cardinality :db.cardinality/one}])
(do
(def new-user (util/insert datomic-conn
[[:user/email "newuser@foo.com"]
[:user/password-hash "123abc"]]))
(def product-a (util/insert datomic-conn
[[:product/name "Product A"]
[:product/price (bigdec 150)]]))
(def product-b (util/insert datomic-conn
[[:product/name "Product B"]
[:product/price (bigdec 100)]]))
(def product-c (util/insert datomic-conn
[[:product/name "Product C"]
[:product/price (bigdec 76)]])))
(let [order-tempid (d/tempid :db.part/user)]
(def order-tx
@(d/transact
datomic-conn
[[:db/add (:db/id new-user) :user/orders order-tempid]
[:db/add order-tempid :order/products (:db/id product-a)]
[:db/add order-tempid :order/products (:db/id product-c)]]))
(def order (util/get-inserted-entity order-tx order-tempid)))
(let [delivery-tempid (d/tempid :db.part/user)]
(def delivery-tx
@(d/transact
datomic-conn
[[:db/add delivery-tempid :delivery/order (:db/id order)]]))
(def delivery (util/get-inserted-entity delivery-tx delivery-tempid)))
;; At this point we have a problem. The user contacts support and complains that the delivery
;; confirmation e-mail was not delivered to newuser-changed@foo.com.
(deref
(d/transact
datomic-conn
[[:db/add (:db/id new-user) :user/email "newuser-changed@foo.com"]]))
(def our-db (d/db datomic-conn))
(def new-user (d/entity our-db (:db/id new-user)))
;; We use over-time queries in datomic to find the users e-mail as of the time of the delivery.
(def delivery-txs
(d/q '[:find ?tx :in $ ?order-id
:where [?order-id _ _ ?tx]] (d/history our-db) (:db/id delivery)))
;; We know there's only one. Normally we'd filter based on transaction timestamps (all transactions
;; have timestamps)
(def delivery-tx (d/touch (d/entity our-db (ffirst delivery-txs))))
;; Voila!
(def user-as-of-delivery
(d/entity
(d/as-of our-db (:db/id delivery-tx))
(:db/id new-user)))
;; "Stored procedures" (or transaction functions) written in Java or Clojure allows you to
;; consistently do stuff like "add 10 to a field".
(d/transact
datomic-conn
[{:db/id (d/tempid :db.part/user)
:db/ident :inc
:db/fn (d/function '{:lang "clojure"
:params [db eid attr num]
:code (let [curr-entity (datomic.api/entity db eid)
curr-value (get curr-entity attr)
new-val (+ curr-value num)]
[[:db/add eid attr new-val]])})}
{:db/id #db/id[:db.part/db]
:db.install/_attribute :db.part/db
:db/ident :user/balance
:db/valueType :db.type/long
:db/cardinality :db.cardinality/one}])
(def set-balance-tx
(deref
(d/transact
datomic-conn
[[:db/add (:db/id new-user) :user/balance 100]])))
(def inc-balance-tx
(deref
(d/transact
datomic-conn
[[:inc (:db/id new-user) :user/balance 10]])))
(ns datomic-demo.util
(:require [datomic.api :as d]))
(defn get-inserted-entity
[tx tempid]
(d/touch (d/entity (:db-after tx) (d/resolve-tempid (:db-after tx) (:tempids tx) tempid))))
(defn insert
[datomic-conn facts]
(let [tempid (d/tempid :db.part/user)
tx @(d/transact
datomic-conn
(map #(concat [:db/add tempid] %) facts))]
(get-inserted-entity tx tempid)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment