与“重置”操作有很多关系

我正在寻找关于在Datomic中建模某些多对多关系的方法的反馈。

问题

假设我想为一个Person有一个喜欢的电影列表的域设计Datomic模式。 例如, John最喜欢的电影是GladiatorStar WarsFight 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来提供对QuestionAnswer 。 用户可以将她的Answer更新为Question 。 目标是对Answer - Option关系进行建模。

该信息模型的规范Datomic模式将是:

  • :answer/id唯一标识(标量类型,唯一标识)
  • :option/id唯一标识(标量类型,唯一标识)
  • :answer/selectedOptions (ref-typed,cardinality-many)

  • 这种技术更复杂:您需要管理两个实体而不是一个实体。
  • 如果您使用泛型attr来保存集合成员(在您的示例中为:set.string/contains ,那么您不再拥有对favorite-movies值的有用索引。 要获得有用的索引,您需要一对属性:person/favorite-movies:person.favorite-movies/items
  • 用户喜爱的电影更改的历史记录要重建起来更为复杂。 您现在可以更长时间地查看:person/favorite-movies ,您需要随时了解它指向的实体集合,并查看集合实体的历史记录。
  • 您的应用程序需要区分“我正在重置一组”和“我正在更改一组并且希望更改合并。” 在应用程序模型中实际上可能没有任何此类区别。
  • 您最终可能会在孤立的“设置”实体上使用未引用的数据。 例如:同时,一个对等方发送一个重置(即,断言一个新的集合实体),另一个同伴将一个项目添加到现有集合中。 如果第二个同行的交易在第一个交易之后发生,那么您现在拥有一个孤立的datom。
  • 最好的解决方案是进行细微的更改。 例如,如果用户添加或删除集合中的特定项目,则每个添加或删除应该是只有该断言或撤消的事务。 集合操作是可交换的,因此在同一集合上闯入的两个用户不会造成任何伤害。 (除非你有衍生数据,在这种情况下,竞争条件很重要。)

    如果您确实需要“重置集合,使其看起来像这样”操作,则更好的解决方案是使用事务函数,该函数接收您希望的整个设置值并计算获取当前值所需的添加和撤销你想要的新价值。 这是一个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

    上一篇: many relationship with a 'reset' operation

    下一篇: Annotated relationships in Datomic