clojuredatomic

Adding entities and 1:M references in Datomic


I have a situation where I have an entity A with a cardinality:many ref to an entity B.

A is already transacted to the database. I'm looking for the most efficient way to add entity B and add it to the ref attribute of A.

For example, already-transacted entity A might look like this:

(def john
  {:teacher/first "John"
   :teacher/last "Doe"
   :teacher/email "johndoe@university.edu"})

And yet-to-be entity B might look like this:

(def brandon {:student/id (d/squuid)
              :student/first "Brandon"
              :student/last "Smith"})

I hoped I could just issue a transaction with B tacked on to A via the ref field, but that didn't seem to work:

(d/transact conn [(conj john {:user/students brandon})])
;; ==> ":db.error/not-an-entity Unable to resolve entity: :user/students",

;; ==> ":db.error/not-an-entity Unable to resolve entity: :user/students",

The only way I've gotten this to work is an unwieldily sequence where I transact entity B, query back its ID, and then link them up:

(let [brandon-tx (d/transact conn [brandon])
      new-db (:db-after @brandon-tx)
      brandon-id (-> (d/q '[:find (pull ?e [*])
                            :in $ ?bid
                            :where [?e :student/id ?bid]] new-db (:student/id brandon)) ffirst :db/id)
      tx [:db/add john-id :teacher/students brandon-id]
      result (d/transact conn [tx])
      ]
  result)

Is there a better way to transact entity B and reference it in A? Full code below:

(require '[datomic.api :as d])

(def uri "datomic:mem://db")

(d/create-database uri)

(def conn (d/connect uri))
(def db (d/db conn))

(def schema
  ;; teachers
  [{:db/ident :teacher/first
    :db/valueType :db.type/string
    :db/cardinality :db.cardinality/one}
   {:db/ident :teacher/last
    :db/valueType :db.type/string
    :db/cardinality :db.cardinality/one}
   {:db/ident :teacher/email
    :db/valueType :db.type/string
    :db/cardinality :db.cardinality/one
    :db/unique :db.unique/identity}
   {:db/ident :teacher/students
    :db/valueType :db.type/ref
    :db/cardinality :db.cardinality/many}

   ;; students
   {:db/ident :student/first
    :db/valueType :db.type/string
    :db/cardinality :db.cardinality/one}
   {:db/ident :student/last
    :db/valueType :db.type/string
    :db/cardinality :db.cardinality/one}
   {:db/ident :student/id
    :db/valueType :db.type/uuid
    :db/cardinality :db.cardinality/one
    :db/unique :db.unique/identity}
   ])

(d/transact conn schema)

;; teachers
(def john
  {:teacher/first "John"
   :teacher/last "Doe"
   :teacher/email "johndoe@university.edu"})

(def jane
  {:teacher/first "Jane"
   :teacher/last "Doe"
   :teacher/email "janedoe@university.edu"})

(d/transact conn [john jane])
;; #<promise$settable_future$reify__8061@7dd5e085:
;; {:db-before datomic.db.Db@723be74,
;;  :db-after datomic.db.Db@35baf542,
;;  :tx-data
;;  [#datom[13194139534316 50 #inst "2023-08-05T17:37:44.466-00:00" 13194139534316 true]],
;;  :tempids
;;  {-9223301668109598125 17592186045418,
;;   -9223301668109598124 17592186045419}}>

@(def all-teachers (d/q '[:find (pull ?e [*])
                          :where [?e :teacher/email]] (d/db conn)))

;; ==> [[{:db/id 17592186045418,
;;        :teacher/first "John",
;;        :teacher/last "Doe",
;;        :teacher/email "johndoe@university.edu"}]
;;        :db/id 17592186045419,
;;        :teacher/first "Jane",
;;        :teacher/last "Doe",
;;        :teacher/email "janedoe@university.edu"}]]

;; students

(def ashley {:student/id (d/squuid)
             :student/first "Ashley"
             :student/last "Johnson"})

(def brandon {:student/id (d/squuid)
              :student/first "Brandon"
              :student/last "Smith"})


;; add students to a teacher

@(def john-id (ffirst (d/q '[:find ?e
                             :where
                             [?e :teacher/email "johndoe@university.edu"]] (d/db conn))))

@(def add-brandon-to-john (conj john {:user/students brandon}))
;; ==> {:teacher/first "John",
;;      :teacher/last "Doe",
;;      :teacher/email "johndoe@university.edu",
;;      :user/students #:student{:id #uuid"7e098355-3ce8-4a7d-bd47-d7ea4e0819a4",
;;                               :first "Brandon",
;;                               :last "Smith"}}

;; fails

(d/transact conn [(conj john {:user/students brandon})])
;; ==> ":db.error/not-an-entity Unable to resolve entity: :user/students",

;; succeeds

(let [brandon-tx (d/transact conn [brandon])
      new-db (:db-after @brandon-tx)
      brandon-id (-> (d/q '[:find (pull ?e [*])
                            :in $ ?bid
                            :where [?e :student/id ?bid]] new-db (:student/id brandon)) ffirst :db/id)
      tx [:db/add john-id :teacher/students brandon-id]
      result (d/transact conn [tx])
      ]
  result)

Solution

  • It turns out I didn't update some of my code after I refactored. I can add entity B and reference it in one step. user should have been teacher.

    (d/transact conn [(conj john {:teacher/students brandon})])