与“重置”操作有很多关系
我正在寻找关于在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
是基数-1,ref-typed属性,并且:set.string/contains
是cardinality-many,string-typed属性。 重置列表就是创建一个新的集合实体的问题:
[{: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属性一起出现。
研究一个用户关系的“重置”操作更有意义的用例也更为相关,而“最喜欢的电影”并非如此。
示例:带复选框的表单,用户可以通过选择一组Option
来提供对Question
的Answer
。 用户可以将她的Answer
更新为Question
。 目标是对Answer - Option
关系进行建模。
该信息模型的规范Datomic模式将是:
:answer/id
唯一标识(标量类型,唯一标识) :option/id
唯一标识(标量类型,唯一标识) :answer/selectedOptions
(ref-typed,cardinality-many) :set.string/contains
,那么您不再拥有对favorite-movies
值的有用索引。 要获得有用的索引,您需要一对属性: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]
用这种方法进行了几个月的试验,这里是我的结论。
这两种策略(A - 使用直接属性对B - 使用中间一次性实体)在阅读和写作方面具有实际的优点和缺点,可以在问题和弗朗西斯阿维拉的答案中阅读。 但恕我直言,最重要的原则是: 模式应该主要由域模型决定,而不是读写模式 。
是否存在策略B适合的域模型? 我相信是这样。
例如,在问题中提出的问题/选项/答案示例域中,可能更有意义的是将一组答案解释为一个有凝聚力的整体而不是单独的单个事实。 为中间实体添加一个:submittedTime
instant-typed属性,现在您已经对答案进行了修改(您不想依赖Datomic历史来对其进行建模)。
注意:
通过策略A,实施“重置”操作需要交易功能; 由于与实体生命周期相关的棘手问题('这个实体是否已经存在'),在最一般情况下,这样的事务函数并不是微不足道的。 我最好的照片可以在Datofu库中找到。
链接地址: http://www.djcxy.com/p/65271.html