Thursday, July 26, 2012

A little Clojure wrapper for Datomic

I wrote yesterday about getting started with Datomic in a lein based project. Probably because I am not up to speed with Datomic idioms, a lot of the data boilerplate bugs me so I wrote a little wrapper to hide all of this from my view. Starting with a some code by Michael Nygard I saw on the Datomic newsgroup I wrapped creating database attributes and adding data to the data store. I formated the following code in a funky way to make it fit on this web page:

(ns datomic-test.core
  (:use [datomic.api :as api]))

(defn attribute [id t c doc]  ; by Michael Nygard
  {:db/id (api/tempid :db.part/db)
   :db/ident id
   :db/valueType t
   :db/cardinality c
   :db/doc doc
   :db.install/_attribute :db.part/db})

(defn string-singleton-attribute [conn id doc]
  @(api/transact conn
     [(attribute id
         :db.type/string :db.cardinality/one doc)]))

(defn string-multiple-attribute [conn id doc]
  @(api/transact conn
     [(attribute id
         :db.type/string :db.cardinality/many doc)]))


(defn long-singleton-attribute [conn id doc]
  @(api/transact conn
     [(attribute id
        :db.type/long :db.cardinality/one doc)]))

(defn long-multiple-attribute [conn id doc]
  @(api/transact conn
     [(attribute id
        :db.type/long :db.cardinality/many doc)]))

(defn do-tx-user [conn data-seq]
  (let [data
        (for [data data-seq]
          (assoc data :db/id (api/tempid :db.part/user)))]
     @(api/transact conn data)))
Michael's code wraps schema attribute definitions like I showed in the file data/schema.dtm in yesterday's blog. The function do-tx-user takes a seq of maps, adds the user database partition specification to each map, and runs a transaction. With this wrapper, I don't use a separate schema input data file anymore. Here is the example I showed yesterday using the wrapper:
(ns datomic-test.test.core
  (:use [datomic-test.core])
  (:use [clojure.test]))

(use '[datomic.api :only [q db] :as api])
(use 'clojure.pprint)

;;(def uri "datomic:free://localhost:4334//news")
(def uri "datomic:mem://news")

(api/create-database uri)
(def conn (api/connect uri))

;; create two singleton string attributes and a number
;; attribute and add them to the :db.part/db partition:
(string-singleton-attribute
  conn :news/title "A news story's title")
(string-singleton-attribute
  conn :news/url "A news story's URL")
(long-singleton-attribute
  conn :news/reader-count "Number of readers")

;; add some data to the :db.part/user partition:
(do-tx-user conn
  [{:news/title "Rain Today",
    :news/url "http://test.com/news1",
    :news/reader-count 11}
   {:news/title "Sunshine tomorrow",
    :news/url "http://test.com/news2",
    :news/reader-count 8}])


(def results
  (q '[:find ?n :where [?n :news/title]] (db conn)))
(println (count results))
(doseq [result results]
  (let [id (first result)
        entity (-> conn db (api/entity id))]
    (println (:news/title entity) (:news/reader-count entity))))
Since I use many different tools, I sometimes like to figure out the subset of APIs, etc. that I need and wrap them in a form that is easier for me to remember and use. This may be a bad habit because I can end up permanently using a subset of tool functionality.

No comments: