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?
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.
Related
Tested on datascript 1.3.0
datoms:
[{:db/id -1 :name "Oliver Smith" :hobbies ["reading" "sports" "music"]}]
tried to run the query below to find who like sports, but the empty set returned.
'[:find ?name
:where
[?p :name ?name]
[?p :hobbies ?hobbies]
[(some #{"sports"} ?hobbies)]]
How to formulate the query correctly to get the expected result below?
#{[Oliver Smith]}
We have to explicitly define the schema with cardinality/many against the attribute of multiple values to solve the problem since schemaless doesn't work here.
(require '[datascript.core :as d])
(def schema {:hobbies {:db/cardinality db.cardinality/many}})
(def conn (d/create-conn schema))
(def datoms [{:db/id -1 :name "Oliver Smith" :hobbies ["reading" "sports" "music"]}])
(d/transact! conn datoms)
(def query '[:find ?name :where [?p :name ?name] [?p :hobbies "sports"]])
(-> (d/q query #conn) println)
Tested on datascript 1.3.0
datoms:
[{:db/id -1 :name "Smith" :firstname "Oliver" :age 20}
{:db/id -2 :name "Jones" :firstname "Oliver" :age 20}
{:db/id -3 :name "Smith" :firstname "Amelia" :age 16}
{:db/id -4 :name "Jones" :firstname "Amelia" :age 16}]
tried to query with logical and predicates below who are named Smith and aged older than 18 years, why did it return the unfiltered whole set?
'[:find ?firstname ?name
:where
[?p :name ?name]
[?p :firstname ?firstname]
[?p :age ?age]
[(and (= ?name "Smith") (> ?age 18))]]
;;; wrong result: #{[Oliver Smith] [Oliver Jones] [Amelia Smith] [Amelia Jones]}
then changed to query with discrete predicates and got the satisfied result as expected.
'[:find ?firstname ?name
:where
[?p :name ?name]
[?p :firstname ?firstname]
[?p :age ?age]
[(= ?name "Smith")]
[(> ?age 18)]]
;;; correct result: #{[Oliver Smith]}
Do datomic and datascript or datalog in general only support data patterns scattered to discrete clauses? Are conventional logical operations and etc. incompatible here?
This is because AND is implicit. All clauses are implicitly joined by AND as in they all must be true at the same time for the query to match
According to the manual you cannot use an and-clause just like that. The only way you can use an and-clause is when it is inside an or-clause:
Inside the or clause, you may use an and clause to specify
conjunction. This clause is not available outside of an or clause,
since conjunction is the default in other clauses.
Setup Consider the following DataScript database of films and cast, with data stolen from learndatalogtoday.org: the following code can be executed in a JVM/Clojure REPL or a ClojureScript REPL, as long as project.clj contains [datascript "0.15.0"] as a dependency.
(ns user
(:require [datascript.core :as d]))
(def data
[["First Blood" ["Sylvester Stallone" "Brian Dennehy" "Richard Crenna"]]
["Terminator 2: Judgment Day" ["Linda Hamilton" "Arnold Schwarzenegger" "Edward Furlong" "Robert Patrick"]]
["The Terminator" ["Arnold Schwarzenegger" "Linda Hamilton" "Michael Biehn"]]
["Rambo III" ["Richard Crenna" "Sylvester Stallone" "Marc de Jonge"]]
["Predator 2" ["Gary Busey" "Danny Glover" "Ruben Blades"]]
["Lethal Weapon" ["Gary Busey" "Mel Gibson" "Danny Glover"]]
["Lethal Weapon 2" ["Mel Gibson" "Joe Pesci" "Danny Glover"]]
["Lethal Weapon 3" ["Joe Pesci" "Danny Glover" "Mel Gibson"]]
["Alien" ["Tom Skerritt" "Veronica Cartwright" "Sigourney Weaver"]]
["Aliens" ["Carrie Henn" "Sigourney Weaver" "Michael Biehn"]]
["Die Hard" ["Alan Rickman" "Bruce Willis" "Alexander Godunov"]]
["Rambo: First Blood Part II" ["Richard Crenna" "Sylvester Stallone" "Charles Napier"]]
["Commando" ["Arnold Schwarzenegger" "Alyssa Milano" "Rae Dawn Chong"]]
["Mad Max 2" ["Bruce Spence" "Mel Gibson" "Michael Preston"]]
["Mad Max" ["Joanne Samuel" "Steve Bisley" "Mel Gibson"]]
["RoboCop" ["Nancy Allen" "Peter Weller" "Ronny Cox"]]
["Braveheart" ["Sophie Marceau" "Mel Gibson"]]
["Mad Max Beyond Thunderdome" ["Mel Gibson" "Tina Turner"]]
["Predator" ["Carl Weathers" "Elpidia Carrillo" "Arnold Schwarzenegger"]]
["Terminator 3: Rise of the Machines" ["Nick Stahl" "Arnold Schwarzenegger" "Claire Danes"]]])
(def conn (d/create-conn {:film/cast {:db/valueType :db.type/ref
:db/cardinality :db.cardinality/many}
:film/name {:db/unique :db.unique/identity
:db/cardinality :db.cardinality/one}
:actor/name {:db/unique :db.unique/identity
:db/cardinality :db.cardinality/one}}))
(def all-datoms (mapcat (fn [[film actors]]
(into [{:film/name film}]
(map #(hash-map :actor/name %) actors)))
data))
(def all-relations (mapv (fn [[film actors]]
{:db/id [:film/name film]
:film/cast (mapv #(vector :actor/name %) actors)}) data))
(d/transact! conn all-datoms)
(d/transact! conn all-relations)
Description In a nutshell, there are two kinds of entities in this database—films and actors (word intended to be ungendered)—and three kinds of datoms:
film entity: :film/name (a unique string)
film entity: :film/cast (multiple refs)
actor entity: :actor/name (unique string)
Question I would like to construct a query which asks: which films have these N actors, and these N actors alone, appeared as the sole stars, for N>=2?
E.g., RoboCop starred Nancy Allen, Peter Weller, Ronny Cox, but no film starred solely the first two of these, Allen and Weller. Therefore, I would expect the following query to produce the empty set:
(d/q '[:find ?film-name
:where
[?film :film/name ?film-name]
[?film :film/cast ?actor-1]
[?film :film/cast ?actor-2]
[?actor-1 :actor/name "Nancy Allen"]
[?actor-2 :actor/name "Peter Weller"]]
#conn)
; => #{["RoboCop"]}
However, the query is flawed because I don't know how to express that any matches should exclude any actors who are not Allen or Weller—again, I want to find the movies where only Allen and Weller have collaborated without any other actors, so I want to adapt the above query to produce the empty set. How can I adjust this query to enforce this requirement?
Because DataScript doesn't have negation (as of May 2016), I don't believe that's possible with one static query in 'pure' Datalog.
My way to go would be:
build the query programmatically to add the N clauses that state that the cast must contain the N actors
Add a predicate function which, given a movie, the database, and the set of actors ids, uses the EAVT index to find if each movie has an actor that is not in the set.
Here's a basic implementation
(defn only-those-actors? [db movie actors]
(->> (datoms db :eavt movie :film/cast) seq
(every? (fn [[_ _ actor]]
(contains? actors actor)))
))
(defn find-movies-with-exact-cast [db actors-names]
(let [actors (set (d/q '[:find [?actor ...] :in $ [?name ...] ?only-those-actors :where
[?actor :actor/name ?name]]
db actors-names))
query {:find '[[?movie ...]]
:in '[$ ?actors ?db]
:where
(concat
(for [actor actors]
['?movie :film/cast actor])
[['(only-those-actors? ?db ?movie ?actors)]])}]
(d/q query db actors db only-those-actors?)))
You can use predicate fun and d/entity together for filtering datoms by :film/cast field of an entity. This approach looks much more straightforward until Datascript doesn't support negation (not operator and so on).
Look at the row (= a (:age (d/entity db e)) in the test case of the Datascript here
[{:db/id 1 :name "Ivan" :age 10}
{:db/id 2 :name "Ivan" :age 20}
{:db/id 3 :name "Oleg" :age 10}
{:db/id 4 :name "Oleg" :age 20}]
...
(let [pred (fn [db e a]
(= a (:age (d/entity db e))))]
(is (= (q/q '[:find ?e
:in $ ?pred
:where [?e :age ?a]
[(?pred $ ?e 10)]]
db pred)
#{[1] [3]})))))
In your case, the predicate body could look something like this
(clojure.set/subset? actors (:film/cast (d/entity db e))
In regards to performance, the d/entity call is fast because it is a lookup by index.
I touch an entity and get many entity ids. I want all the attribute values instead of the ids while keeping the nested structure.
(d/touch (d/entity (get-db) (ffirst (find-all-families))))
=> {:family/parent #{{:db/id 17592186045423}
{:db/id 17592186045424}
{:db/id 17592186045426}
{:db/id 17592186045427}},
:family/child #{{:db/id 17592186045420}
{:db/id 17592186045421}},
:family/address {:db/id 17592186045428},
:family/email "someemail#gmail.com",
:db/id 17592186045429}
Thought about using something like simply touching all the entity ids but seems like complexity creeps up if I want all of them:
(map d/touch (:family/parent (d/touch (d/entity (get-db) (ffirst (find-all-families))))))
Not sure what the idiomatic approach is: finding a way to do it more through the querying side or through clojure.
The idiomatic way to do this in Datomic is to declare components in your schema. touch will touch all of the attributes of the entity, including any components recursively
You will probably wish to use the Datomic Pull API for this purpose. It can recursively return the attr/value pairs for all sub-entities that the user designates as "component". An example:
(def dark-side-of-the-moon [:release/gid #uuid "24824319-9bb8-3d1e-a2c5-b8b864dafd1b"])
(d/pull db [:release/media] dark-side-of-the-moon)
; result
{:release/media
[{:db/id 17592186121277,
:medium/format {:db/id 17592186045741},
:medium/position 1,
:medium/trackCount 10,
:medium/tracks
[{:db/id 17592186121278,
:track/duration 68346,
:track/name "Speak to Me",
:track/position 1,
:track/artists [{:db/id 17592186046909}]}
{:db/id 17592186121279,
:track/duration 168720,
:track/name "Breathe",
:track/position 2,
:track/artists [{:db/id 17592186046909}]}
{:db/id 17592186121280,
:track/duration 230600,
:track/name "On the Run",
:track/position 3,
:track/artists [{:db/id 17592186046909}]}
...]}]}
You may also use the Tupelo Datomic Pull API, which I think is nicer. As an example:
; If you wish to retain duplicate results on output, you must use td/query-pull and the Datomic
; Pull API to return a list of results (instead of a set).
(let [result-pull (td/query-pull :let [$ (live-db)] ; $ is the implicit db name
:find [ (pull ?eid [:location]) ] ; output :location for each ?eid found
:where [ [?eid :location] ] ) ; find any ?eid with a :location attr
result-sort (sort-by #(-> % first :location) result-pull)
]
(is (s/validate [ts/TupleMap] result-pull)) ; a list of tuples of maps
(is (= result-sort [ [ {:location "Caribbean"} ]
[ {:location "London" } ]
[ {:location "London" } ] ] )))
I'm getting into datomic and still don't grok it. How do I build a transaction that has references to a variable number of entities?
For example this creates a transaction with a child entity and a family entity with a child attribute that references the new child entity:
(defn insert-child [id child]
{:db/id #db/id id
:child/first-name (:first-name child)
:child/middle-name (:middle-name child)
:child/last-name (:last-name child)
:child/date-of-birth {:date-of-birth child}})
(defn insert-family [id]
(let [child-id #db/id[:db.part/user]]
(vector
(insert-child child-id
{:first-name "Richard"
:middle-name "M"
:last-name "Stallman"})
{:db/id id
:family/child child-id})))
(insert-family #db/id[:db.part/user])
=> [{:db/id #db/id[:db.part/user -1000012],
:child/first-name "Richard",
:child/middle-name "M",
:child/last-name "Stallman",
:child/date-of-birth nil}
{:db/id #db/id[:db.part/user -1000013],
:family/child #db/id[:db.part/user -1000012]}]
Notice I used let for child-id. I'm not sure how to write this such that I can map over insert-child while having a family entity that references each one.
I thought about using iterate over #db/id[:db.part/user] and the number of children then mapping over both the result of iterate and a vector of children. Seems kind of convoluted and #db/id[:db.part/user] isn't a function to iterate over to begin with.
Instead of using the macro form #db/id[:db.part/user] which is meant for EDN files and data literals, you should use d/tempid.
You could do something like this (using simplified child entities):
(ns family-tx
(:require [datomic.api :refer [q db] :as d]))
(def uri "datomic:mem://testfamily")
(d/delete-database uri)
(d/create-database uri)
(def conn (d/connect uri))
(def schema [
{:db/id (d/tempid :db.part/db)
:db/ident :first-name
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db.install/_attribute :db.part/db}
{:db/id (d/tempid :db.part/db)
:db/ident :last-name
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db.install/_attribute :db.part/db}
{:db/id (d/tempid :db.part/db)
:db/ident :family/child
:db/valueType :db.type/ref
:db/cardinality :db.cardinality/many
:db.install/_attribute :db.part/db}
])
#(d/transact conn schema)
(defn make-family-tx [kids]
(let [kids-tx (map #(into {:db/id (d/tempid :db.part/user)} %) kids)
kids-id (map :db/id kids-tx)]
(conj kids-tx {:db/id (d/tempid :db.part/user)
:family/child kids-id})))
(def kids [{:first-name "Billy" :last-name "Bob"}
{:first-name "Jim" :last-name "Beau"}
{:first-name "Junior" :last-name "Bacon"}])
#(d/transact conn (make-family-tx kids))
There are a few strategies for this discussed in the Transactions docs as well (see the "Identifying Entities" section).