-
-
Save dazld/05593e4911b47f2b0f1068a583aa9e09 to your computer and use it in GitHub Desktop.
Datomic update examples against a social news database
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;; 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