Skip to content

Instantly share code, notes, and snippets.

@dazld
Forked from stuarthalloway/Datomic News Updates
Created March 18, 2017 10:26
Show Gist options
  • Save dazld/05593e4911b47f2b0f1068a583aa9e09 to your computer and use it in GitHub Desktop.
Save dazld/05593e4911b47f2b0f1068a583aa9e09 to your computer and use it in GitHub Desktop.
Datomic update examples against a social news database
;; Datomic example code
;; demonstrates various update scenarios, using a news database
;; that contains stories, users, and upvotes
;; grab an in memory database
(use '[datomic.api :only (q db) :as d])
(def uri "datomic:mem://foo")
(d/create-database uri)
(def conn (d/connect uri))
;; schema for stories
(d/transact
conn
[{:db/id #db/id[:db.part/db]
:db/ident :story/title
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/fulltext true
:db/index true
:db.install/_attribute :db.part/db}
{:db/id #db/id[:db.part/db]
:db/ident :story/url
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db.install/_attribute :db.part/db}
{:db/id #db/id[:db.part/db]
:db/ident :story/slug
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db.install/_attribute :db.part/db}])
;; schema for comments
(d/transact
conn
[{:db/id #db/id[:db.part/db]
:db/ident :comments
:db/valueType :db.type/ref
:db/cardinality :db.cardinality/many
:db/isComponent true
:db.install/_attribute :db.part/db}
{:db/id #db/id[:db.part/db]
:db/ident :comment/body
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db.install/_attribute :db.part/db}
{:db/id #db/id[:db.part/db]
:db/ident :comment/author
:db/valueType :db.type/ref
:db/cardinality :db.cardinality/one
:db.install/_attribute :db.part/db}])
;; schema for users
(d/transact
conn
[{:db/id #db/id[:db.part/db]
:db/ident :user/firstName
:db/index true
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db.install/_attribute :db.part/db}
{:db/id #db/id[:db.part/db]
:db/ident :user/lastName
:db/index true
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db.install/_attribute :db.part/db}
{:db/id #db/id[:db.part/db]
:db/ident :user/email
:db/index true
:db/unique :db.unique/identity
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db.install/_attribute :db.part/db}
{:db/id #db/id[:db.part/db]
:db/ident :user/upVotes
:db/valueType :db.type/ref
:db/cardinality :db.cardinality/many
:db.install/_attribute :db.part/db}])
;; adding new stories:
;; create new ids with #db/id literals
(d/transact
conn
[{:db/id #db/id [:db.part/user]
:story/title "Teach Yourself Programming in Ten Years"
:story/url "http://norvig.com/21-days.html"}
{:db/id #db/id [:db.part/user]
:story/title "Clojure Rationale"
:story/url "http://clojure.org/rationale"}
{:db/id #db/id [:db.part/user]
:story/title "Beating the Averages"
:story/url "http://www.paulgraham.com/avg.html"}])
;; adding new user who upvotes existing stories:
;; query to find existing story ids
;; create new user id with an explicit #db/id literal
;; e.g #db/id [:db.part/user -1]
;; associate existing stories with the new user id
(def all-stories (q '[:find ?e :where [?e :story/url]] (db conn)))
(let [user-id #db/id [:db.part/user -1]]
(d/transact
conn
(conj
(map
(fn [[story-id]] [:db/add user-id :user/upVotes story-id])
all-stories)
{:db/id user-id
:user/email "[email protected]"
:user/firstName "John"
:user/lastName "Doe"})))
;; updating user's name (without touching upvotes or stories)
;; user/email is a :db.unique/identity attribute, so upsert is
;; implicit (you do not have to know the :db/id in advance)
(d/transact
conn
[{:user/email "[email protected]" ;; this finds the existing entity
:db/id #db/id [:db.part/user] ;; will be replaced by exiting id
:user/firstName "Johnathan"}])
;; for variety, we will grab John's id and stop relying on upsert
(def john
(->> (q '[:find ?e :where [?e :user/email "[email protected]"]]
(db conn))
ffirst))
;; grab the id of single upvote
(def johns-upvote-for-pg
(->> (q '[:find ?story
:in $ ?e
:where [?e :user/upVotes ?story]
[?story :story/url "http://www.paulgraham.com/avg.html"]]
(db conn)
john)
ffirst))
;; remove a single upvote (John doesn't like pg anymore? ...)
(d/transact
conn
[[:db/retract john :user/upVotes johns-upvote-for-pg]])
;; double check that john still has two upvotes
(-> (d/entity (db conn) john)
(get :user/upVotes))
;; remove all of John's upvotes
;; create a query that *makes transaction data*
;; then submit the transaction at any later point
;; possibly compsing it with other data
(def data-that-retracts-johns-upvotes
(q '[:find ?op ?e ?a ?v
:in $ ?op ?e ?a
:where [?e ?a ?v]]
(db conn)
:db/retract
john
:user/upVotes))
(d/transact
conn
(seq data-that-retracts-johns-upvotes))
;; double check that john has no more upvotes
(-> (d/entity (db conn) john)
(get :user/upVotes))
(def all-story-ids
(->> (q '[:find ?e :where [?e :story/url]] (db conn))
(map first)))
(defn choose-some
"Pick zero or more items at random from a collection"
[coll]
(take (rand-int (count coll))
(shuffle coll)))
;; sample data generator
(defn example-users
"Make data for example users, possibly with upvotes"
[prefix n]
(mapcat
(fn [n]
(let [user-id (d/tempid :db.part/user)
upvotes (map (fn [id] [:db/add user-id :user/upVotes id])
(choose-some all-story-ids))]
(conj
upvotes
{:db/id user-id
:user/email (str prefix "-" n "@example.com")})))
(range n)))
;; test example-users
(pprint
(example-users "user" 2))
;; make 10 new users
(d/transact conn (example-users "user" 10))
;; how many users should there be now?
(count (q '[:find ?e :where [?e :user/email]] (db conn)))
;; how many users have upvoted something?
(count (q '[:find ?e
:where [?e :user/email]
[?e :user/upVotes]]
(db conn)))
;; find all users, and print their emails plus their upvotes
;; (This shows why Datomic does not need left joins:
;; query and entity navigation are separate.)
(let [dbval (db conn)] ;; shared basis
(->> (q '[:find ?e :where [?e :user/email]] ;; find all
dbval)
(map (fn [[id]] (d/entity dbval id))) ;; entify
(map (fn [ent] ;; navigate
{:email (:user/email ent)
:upvoted (mapv :story/url (:user/upVotes ent))}))
pprint))
;; schema for publication date
(d/transact
conn
[{:db/id #db/id[:db.part/db]
:db/ident :publication/date
:db/valueType :db.type/instant
:db/cardinality :db.cardinality/one
:db/index true
:db.install/_attribute :db.part/db}])
;; find that pg story
(def beating-the-averages
(->> (q '[:find ?e
:where [?e :story/url "http://www.paulgraham.com/avg.html"]]
(db conn))
ffirst))
;; associate story with pub date
(import java.text.SimpleDateFormat)
(def april-04
(-> (java.text.SimpleDateFormat. "yyyy-MM") (.parse "2001-04")))
(d/transact
conn
[[:db/add beating-the-averages :publication/date april-04]])
;; retrieve pub date (domain specific) and transaction time
;; (intrinsic to Datomic)
(let [[pub-time tx-time]
(first (q '[:find ?pub-time ?tx-time
:in $ ?e
:where
[?e :story/url _ ?tx]
[?tx :db/txInstant ?tx-time]
[?e :publication/date ?pub-time]]
(db conn)
beating-the-averages))]
(println "The essay _Beating the Averages was published " pub-time
" but that fact was added to the database on " tx-time))
;; schema for information source
(d/transact
conn
[{:db/id #db/id[:db.part/db]
:db/ident :audit/app
:db/valueType :db.type/keyword
:db/cardinality :db.cardinality/one
:db/index true
:db.install/_attribute :db.part/db}])
;; use the db.part/tx partition to assert facts about the the
;; current transaction
(d/transact
conn
[{:db/id #db/id [:db.part/user]
:story/title "Are We There Yet?"
:story/url "http://www.infoq.com/presentations/Are-We-There-Yet-Rich-Hickey"}
[:db/add (d/tempid :db.part/tx) :audit/app :admin/shell]])
;; find stories added by the admin/shell app
(q '[:find ?url
:where [_ :story/url ?url ?tx]
[?tx :audit/app :admin/shell]]
(db conn))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment