我正在寻找有关在Datomic中建模某些多对关系的方法的反馈。
假设我想为Person拥有最喜欢的电影列表的域设计Datomic架构。例如,John
最喜欢的电影是Gladiator
,Star Wars
和Fight Club
。
在Datomic中对此进行建模的最明显的架构是具有基数多属性,例如:
#{["John" :person/favorite-movies "Gladiator"]
["John" :person/favorite-movies "Star Wars"]
["John" :person/favorite-movies "Fight Club"]}
这种方法可以轻松地从列表中添加或删除电影(只需使用:db/add
和:db/retract
),但我发现重置整个列表是不切实际的电影 - 你基本上需要计算旧列表和新列表之间的差异,并且必须在事务函数中运行。当列表的元素不是标量时,这会变得更糟。
作为替代方法,我考虑使用集实体引入间接:
#{["John" :person/favorite-movies 42]
[42 :set.string/contains "Gladiator"]
[42 :set.string/contains "Star Wars"]
[42 :set.string/contains "Fight Club"]}
使用这种方法,:person/favorite-movies
是基数 - 一,ref-typed属性,:set.string/contains
是基数 - 多,字符串类型属性。重置列表只需创建一个新的集实体:
[{:db/id "John"
:person/favorite-movies {:db/id (d/tempid :db.part/user)
:set.string/contains ["Gladiator"
"The Lord of the Rings"
"A Clockwork Orange"
"True Romance"]}}]
这种建模对多种关系的方法存在已知限制吗?
在关系是ref-typed而不是标量类型的情况下研究这个问题更相关,因为某些问题在Datomic中出现了ref-typed属性。
研究一个重置'的用例也更相关。对于这种关系的操作更有意义,对于最喜欢的电影来说并非如此。
示例:带有复选框的表单,用户可以通过选择一组Answer
向Question
提供Option
。用户可以将她Answer
更新为Question
。目标是模拟Answer - Option
关系。
此信息模型的规范Datomic架构将是:
:answer/id
:答案的唯一ID(标量类型,唯一标识):option/id
:选项的唯一ID(标量类型,唯一标识):answer/selectedOptions
(ref-typed,cardinality-many)答案 0 :(得分:1)
favorite-movies
),则:set.string/contains
值不再具有有用的索引。要获得有用的索引,您需要一对属性:例如:person/favorite-movies
和:person.favorite-movies/items
。:person/favorite-movies
,您需要知道它在任何时刻指向的集合实体,并查看集合实体的历史记录。最佳解决方案是进行细化更改。例如,如果用户在集合中添加或删除特定项目,则每个添加或删除应该是仅具有该断言或撤销的事务。设置操作是可交换的,因此在同一组上进行攻击的两个用户不会造成任何伤害。 (除非你有派生数据,否则竞争条件很重要。)
如果您真的需要"重置该设置,请使其显示为"操作,更好的解决方案是使用一个事务函数来接收您想要的整个设置值,并计算使当前值成为您想要的新值所需的加法和缩减。这是一个tx函数,它将执行此操作:
{:db/ident :db.fn/resetAttribute
:db/doc "Unconditionally set an entity's attribute's values to those provided,
retracting all other existing values.
Values must be a collection (list, seq, vector), even for cardinality-one
attributes. An empty collection (or nil) will retract all values. The values
themselves must be primitive, i.e. no map forms are permitted for refs, use
tempids directly. If the attribute is-component, removed values will be
:db.fn/retractEntity-ed."
:db/fn
#db/fn {:lang "clojure"
:params [db ent attr values]
:code (let [eid (datomic.api/entid db ent)
aid (datomic.api/entid db attr)
{:keys [value-type is-component]} (datomic.api/attribute db aid)
newvalues (if (= value-type :db.type/ref)
(into #{} (map #(if (string? %) % (d/entid db %))) values)
(into #{} values))
oldvalues (into #{} (map :v) (datomic.api/datoms db :eavt eid aid))]
(-> []
(into (comp
(remove newvalues)
(map (if is-component
#(do [:db.fn/retractEntity %])
#(do [:db/retract eid aid %]))))
oldvalues)
(into (comp
(remove oldvalues)
(map #(do [:db/add eid aid %])))
newvalues)))}}
您可以这样使用它:
[:db.fn/resetAttribute [:person/id "John"] :person/favorite-movies
["Gladiator" "The Lord of the Rings" "A Clockwork Orange" "True Romance"]]]
;; Or to retract *all* existing values:
[:db.fn/resetAttribute [:person/id "John"] :person/favorite-movies nil]
答案 1 :(得分:0)
用这种方法进行了几个月的试验,这是我的结论。
两种策略(A - 使用直接属性与B - 使用中间,一次性实体)在阅读和写作方面具有实际优点和缺点,可以在the question和{{3}中阅读}。但恕我直言,最重要的原则是: 架构应主要由域模型决定,而不是由读写模式决定 。
策略B适用的域模型是否合适?我相信。
例如,在问题中提出的问题/选项/答案示例域中,将答案集解释为一个有凝聚力的整体而不是单独的个别事实可能更有意义。向中间实体添加:submittedTime
即时类型属性,您现在已经建立了答案的修订版(您不希望依赖于Datomic历史记录)模型,)。
注意:
使用策略A,实施重置'操作需要交易功能;由于与实体生命周期相关的棘手问题('这个实体是否已经存在'),这种交易功能在最一般的情况下写作并不容易。我最好的拍摄可能是Francis Avila's answer。