Datomic:与'重置'的多对多关系的模式操作

时间:2017-02-08 12:00:59

标签: database-design datomic

我正在寻找有关在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是基数 - 一,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属性。

研究一个重置'的用例也更相关。对于这种关系的操作更有意义,对于最喜欢的电影来说并非如此。

示例:带有复选框的表单,用户可以通过选择一组AnswerQuestion提供Option。用户可以将她Answer更新为Question。目标是模拟Answer - Option关系。

此信息模型的规范Datomic架构将是:

  • :answer/id:答案的唯一ID(标量类型,唯一标识)
  • :option/id:选项的唯一ID(标量类型,唯一标识)
  • :answer/selectedOptions(ref-typed,cardinality-many)

2 个答案:

答案 0 :(得分:1)

  • 这种技术比较复杂:你需要管理两个实体而不是一个。
  • 如果您使用通用attr来保存集合成员(示例中为favorite-movies),则:set.string/contains值不再具有有用的索引。要获得有用的索引,您需要一对属性:例如:person/favorite-movies:person.favorite-movies/items
  • 您对用户最喜欢的电影进行更改的历史记录更难以重建。您现在可以更长时间地查看:person/favorite-movies,您需要知道它在任何时刻指向的集合实体,并查看集合实体的历史记录。
  • 您的应用程序需要区分"我正在重置一组" vs"我正在更改一个集合并希望合并更改。"在应用程序模型中可能实际上没有任何这样的区别。
  • 你最终可能会成为孤儿" set"具有未引用数据的实体。例如:同时,一个对等体发送重置(即断言新的集合实体),另一个对等体将项目添加到现有集合。如果第二个对等的事务发生在第一个事务之后,那么现在有一个孤立的数据。

最佳解决方案是进行细化更改。例如,如果用户在集合中添加或删除特定项目,则每个添加或删除应该是仅具有该断言或撤销的事务。设置操作是可交换的,因此在同一组上进行攻击的两个用户不会造成任何伤害。 (除非你有派生数据,否则竞争条件很重要。)

如果您真的需要"重置该设置,请使其显示为"操作,更好的解决方案是使用一个事务函数来接收您想要的整个设置值,并计算使当前值成为您想要的新值所需的加法和缩减。这是一个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