Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to update/overwrite a ref attribute with cardinality many in datomic?

Let’s say I have a schema that includes an attribute :x/value, where :x/value is a component, is a ref, and has cardinality many. The schema also has an id for x :x/id.

Now let’s say I say I transact the following:

(d/transact conn [{:x/id "1234" :x/value [{:text "test"}]}])

Then later I want to update the value, meaning really that I want to replace :x/value, so that in the end I have an entity like this:

{:db/id <some eid>
 :x/id "1234"
 :x/value [{:text "replacement"}]}

How would I do this?

So far, I've tried the following:

(d/transact conn [{:x/id "1234" :x/value [{:text "replacement"}]}])

But this simply added a new ref, so I got an entity looking like:

{:db/id <some eid>
 :x/id "1234"
 :x/value [{:text "test"} {:text "replacement"}]}

One way to achieve what I want here, I think, would be to manually retract both of the :text attributes by their entity id, and then do a new add transaction for the x entity.

But I wonder if there's a better way to do this. Any ideas?

like image 919
fraxture Avatar asked Mar 14 '17 12:03

fraxture


1 Answers

You need to retract the old value and then update it with a new value:

[:db/retract  entity-id attribute old-value]
[:db/add      entity-id attribute new-value]

See http://docs.datomic.com/transactions.html

You can see more details in the James Bond example from Tupelo Datomic. Here is how the attributes are created:

  (td/transact *conn* ;   required              required              zero-or-more
                      ;  <attr name>         <attr value type>       <optional specs ...>
    (td/new-attribute   :person/name         :db.type/string         :db.unique/value)      ; each name      is unique
    (td/new-attribute   :person/secret-id    :db.type/long           :db.unique/value)      ; each secret-id is unique
    (td/new-attribute   :weapon/type         :db.type/ref            :db.cardinality/many)  ; one may have many weapons
    (td/new-attribute   :location            :db.type/string)     ; all default values
    (td/new-attribute   :favorite-weapon     :db.type/keyword ))  ; all default values

Suppose James throws his knife at a villan. We need to remove it from the DB.

(td/transact *conn* 
  (td/retract-value james-eid :weapon/type :weapon/knife))
(is (= (td/entity-map (live-db) james-eid)  ; lookup by EID 
       {:person/name "James Bond" :location "London" :weapon/type #{:weapon/wit :weapon/gun} :person/secret-id 7 } ))

Once James has defeated Dr No, we need to remove him (& everything he possesses) from the database.

; We see that Dr No is in the DB...
(let [tuple-set   (td/find  :let    [$ (live-db)]
                            :find   [?name ?loc] ; <- shape of output tuples
                            :where  {:person/name ?name :location ?loc} ) ]
  (is (= tuple-set #{ ["James Bond"     "London"]
                      ["M"              "London"]
                      ["Dr No"          "Caribbean"]
                      ["Honey Rider"    "Caribbean"] } )))
; we do the retraction...
(td/transact *conn*
  (td/retract-entity [:person/name "Dr No"] ))
; ...and now he's gone!
(let [tuple-set   (td/find  :let    [$ (live-db)]
                            :find   [?name ?loc]
                            :where  {:person/name ?name :location ?loc} ) ]
  (is (= tuple-set #{ ["James Bond"     "London"]
                      ["M"              "London"]
                      ["Honey Rider"    "Caribbean"] } )))

Update: Native Datomic Solution

Using native datomic is almost identical, just not quite as sweet as Tupelo:

; Dr No is no match for James. He gives up trying to use guile...
; Remove it using native Datomic.
(spy :before (td/entity-map (live-db) [:person/name "Dr No"]))
(d/transact *conn*
            [[:db/retract [:person/name "Dr No"] :weapon/type :weapon/guile]])
(is (= (spy :after (td/entity-map (live-db) [:person/name "Dr No"])) ; LookupRef
       {:person/name "Dr No"
        :location "Caribbean"
        :weapon/type #{:weapon/knife :weapon/gun}}))

:before => {:person/name "Dr No",
            :weapon/type #{:weapon/guile :weapon/knife :weapon/gun},
            :location "Caribbean"}
:after  => {:person/name "Dr No",
            :weapon/type #{:weapon/knife :weapon/gun},
            :location "Caribbean"}

Update #2:

Side Note: I noticed that your example shows :arb/value [{:db/id 17592186045435, :content/text "tester"}], which is a list of maps of length 1. This is different than my example where :weapon/type is just a plain set of N items. This output difference is because you are using the pull API of Datomic. However, this won't affect your original problem.

We all know that James has had many Bond girls over the years. Here is an example of how to add in some honeys and them demote one of them:

(defn get-bond-girl-names []
    (let [result-pull (d/pull (live-db) [:bond-girl] [:person/name "James Bond"])
          bond-girl-names (forv [girl-entity (grab :bond-girl result-pull) ]
                               (grab :person/name (td/entity-map (live-db) (grab :db/id girl-entity))))
          ]
      bond-girl-names))

  (td/transact *conn*
    (td/new-attribute :bond-girl :db.type/ref :db.cardinality/many))  ; there are many Bond girls

  (let [tx-result          @(td/transact *conn*
                              (td/new-entity {:person/name "Sylvia Trench"})
                              (td/new-entity {:person/name "Tatiana Romanova"})
                              (td/new-entity {:person/name "Pussy Galore"})
                              (td/new-entity {:person/name "Bibi Dahl"})
                              (td/new-entity {:person/name "Octopussy"})
                              (td/new-entity {:person/name "Paris Carver"})
                              (td/new-entity {:person/name "Christmas Jones"}))
        tx-datoms          (td/tx-datoms (live-db) tx-result)
        girl-datoms        (vec (remove #(= :db/txInstant (grab :a %)) tx-datoms))
        girl-eids          (mapv :e girl-datoms)
        txr-2              (td/transact *conn*
                             (td/update [:person/name "James Bond"] ; update using a LookupRef
                               {:bond-girl girl-eids})
                             (td/update [:person/name "James Bond"] ; don't forget to add Honey Rider!
                               {:bond-girl #{[:person/name "Honey Rider"]}}))

  ]
    (is (= (get-bond-girl-names)
          ["Sylvia Trench" "Tatiana Romanova" "Pussy Galore" "Bibi Dahl"
           "Octopussy" "Paris Carver" "Christmas Jones" "Honey Rider"]))
    ; Suppose Bibi Dahl is just not refined enough for James. Give her a demotion.
    (td/transact *conn*
      (td/retract-value [:person/name "James Bond"] :bond-girl [:person/name "Bibi Dahl"]))

    (newline)
    (is (= (get-bond-girl-names)  ; Note that Bibi Dahl is no longer listed
          ["Sylvia Trench" "Tatiana Romanova" "Pussy Galore"
           "Octopussy" "Paris Carver" "Christmas Jones" "Honey Rider"] ))
    )

Note that you can only use a LookupRef like [:person/name "Honey Rider"] since the attribute :person/name has :db.unique/value. If your :content/text is not :db.unique/value you'll have to use an EID to detach it from the parent entity.

like image 192
Alan Thompson Avatar answered Oct 23 '22 05:10

Alan Thompson