使用基数更新值很多

Updating value with cardinality many

我有这样的架构:

[{:db/id #db/id[:db.part/db]
  :db/ident :person/name
  :db/valueType :db.type/string
  :db/cardinality :db.cardinality/one
  :db/doc "A person's name"
  :db.install/_attribute :db.part/db}
 {:db/id #db/id[:db.part/db]
  :db/ident :person/roles
  :db/valueType :db.type/keyword
  :db/cardinality :db.cardinality/many
  :db/doc "A person's role"
  :db.install/_attribute :db.part/db}]

还有这样的代码:

;; insert new person
(def new-id (-> (d/transact conn [{:db/id (d/tempid :db.part/user)
                                   :person/name "foo"
                                   :person/roles #{:admin}}])
                (:tempids)
                (vals)
                (first)))

(defn get-roles
  [db eid]
  (d/q '[:find ?roles .
         :in $ ?eid 
         :where [?eid :user/roles ?roles]]))

(get-roles (d/db conn) new-id) ;; => [:admin]

;; update a person 
(d/transact conn [{:db/id new-id 
                   :person/roles #{:client}}])

(get-roles (d/db conn) new-id) ;; => [:admin :client]

它的默认行为似乎是,它只会关联新值。

在完成更新事务后,我怎样才能得到这个结果:

(get-roles (d/db conn) new-id) ;; => [:client]

如果你想要的是 "reset" 角色列表到一个新值('absolute' 操作'与仅添加或删除角色的 'relative' 操作相反) ,您必须使用事务函数来执行差异并收回需要的值。

这是一个基本的通用实现:

{:db/id (d/tempid :db.part/user),
 :db/ident :my.fns/reset-to-many,
 :db/fn
 (d/function
   {:lang :clojure,
    :requires '[[datomic.api :as d]],
    :params '[db e attr new-vals],
    :code
    '(let [ent (or (d/entity db e)
                 (throw (ex-info "Entity not found" 
                          {:e e :t (d/basis-t db)})))
           entid (:db/id ent)
           old-vals (get ent attr)]
       (into
         [{:db/id (:db/id ent)
           ;; adding the new values
           attr new-vals}]
         ;; retracting the old values
         (comp
           (remove (set new-vals))
           (map (fn [v]
                  [:db/retract entid attr v])))
         old-vals)
       )})}

;; Usage
(d/transact conn [[:my.fns/reset-to-many new-id :person/roles #{:client}]])

这是 Robert Stuttaford

曾经提出的解决方案
(defn many-delta-tx
  "Produces the transaction necessary to have
   `new` be the value for `entity-id` at `attr`
   `new` is expected to be a set."
  [db entity-id attr new]
  (let [current (into #{} (map :v)
                      (d/datoms db :eavt
                                entity-id
                                (d/entid db attr)))]
    (concat (for [id (clojure.set/difference new current)]
              [:db/add entity-id attr id])
            (for [id (clojure.set/difference current new)]
              [:db/retract entity-id attr id]))))

为了测试方便,我想稍微修改一下原题的部分例子。

  • 架构更改。我添加 db/unique
[{:db/id #db/id[:db.part/db]
   :db/ident :person/name 
   :db/valueType :db.type/string
   :db/cardinality :db.cardinality/one
   :db/unique     :db.unique/identity
   :db/doc "A person's name"
   :db.install/_attribute :db.part/db}
  {:db/id #db/id[:db.part/db]
   :db/ident :person/roles
   :db/valueType :db.type/keyword
   :db/cardinality :db.cardinality/many
   :db/doc "A person's role"
   :db.install/_attribute :db.part/db}]
  • get-roles
(defn get-roles
  [db eid]
  (d/q '[:find [?roles ...]
         :in $ ?eid
         :where [?eid :person/roles ?roles]] db eid))
  • 我的测试例子
;; (many-delta-tx (d/db conn) [:person/name "foo"] :person/roles #{:sales :client})
;; => ([:db/add [:person/name "foo"] :person/roles :sales] [:db/retract [:person/name "foo"] :person/roles :admin]))