如何防止事务违反 Datomic 中的应用程序不变量

How to prevent transactions from violating application invariants in Datomic

详细来说,大多数关系数据库都有数据库约束的概念。这是 Postgres documentation on constraints。 Datomic 提供哪些工具来约束数据或维护存储数据的一些不变量集?

一种方法是使用修改数据的事务函数,并在修改期间进行约束验证:http://docs.datomic.com/database-functions.html#uses-for-transaction-functions

编辑 2019-06-28:0.9.5927(Datomic On-Prem)/480-8770(Datomic Cloud)以来,Datomic 支持更精细的写入-通过 Attribute Predicates, Entity Specs and Entity Predicates 进行时间验证。这使得大部分初始答案无效或无关紧要。

特别是观察实体谓词接受数据库值作为参数,因此它们实际上可以强制执行跨越多个实体的不变量。


默认情况下,Datomic 仅对可写入的数据实施一组非常有限的约束,主要包括:

  • 唯一性约束:参见Identity and Uniqueness
  • 类型约束,例如,您不能将数字写入 :db.type/string
  • 的属性
  • 实体解析: 如果 [:my.app/id "fjdsklfjsl"] 查找引用未解析为现有实体
  • [:db/add [:my.app/id "fjdsklfjsl"] :my.app/content 42] 等操作将失败
  • 冲突,例如 Datomic 不会让你 :db/add 如果属性基数是一个,则同一实体属性对有 2 个不同的值。

(我可能忘记了一些,如果有请评论。)

特别是在撰写本文时,没有内置方法可以向给定属性添加自定义验证或 'foreign-key' 约束。

然而,组合 Transaction Functions and speculative writes (a.k.a db.with()) 为您提供了一种强制执行任意不变量的强大方法。例如,您可以将事务包装在事务函数中,该函数使用 db.with() 推测性地应用事务,然后搜索推测性结果以查找不变违规,如果发现一些则抛出异常。您甚至可以通过在 Datalog.

中实现 'search invariant violations' 部分来使此交易功能非常通用

下面是 API 的示例:

[:myapp.fns/checking-invariants
 ;; a description of the invariant
 {:query
  [:find ?message ?user-id
   :in $db-before $db-after ?tx-data ?tempids ?user-id
   :where
   [$db-before ?user :myapp.user/id ?user-id]
   [$db-before ?user :myapp.user/email ?email-before]
   [$db-after ?user :myapp.user/email ?email-after]
   [(not= ?email-before ?email-after)]
   [(ground "A user may not change her email") ?message]]
  :inputs ["user-id-12342141"]}
 ;; the wrapped transaction
 [[:db/add 125315815291 :myapp.user/email "hello.world@yopmail.com"]
  [:db/add 125315815291 :myapp.user/name "Foo Bar"]]]

这是 :myapp.fns/checking-invariants 的(未经测试的)实现:

{:db/ident :myapp.fns/checking-invariants,
 :db/fn #db/fn{:lang :clojure,
               :imports [],
               :requires [[datomic.api :as d]],
               :params [db invariant-q tx],
               :code
               (let [{:keys [query inputs]} invariants-q
                     {:keys [db-before db-after tx-data tempids]}
                     (d/with db tx)]
                 (when-some [violations (apply d/q query
                                          db-before db-after tx-data tempids
                                          inputs)]
                   (throw (ex-info
                            "Transaction would violate invariants."
                            {:tx tx
                             :violations violations
                             :t (d/basis-t db-before)})))
                 tx)}}

限制:

  • 您只能在外部进行保护:客户必须选择使用此不变检查交易功能。
  • 注意性能 - 滥用这种方法可能会给 Transactor 带来过多的负载。在这样做安全的情况下,您可能更愿意使用 db.invoke()
  • 在 Peer 上执行验证
  • 确保你的交易是确定性的,因为它将 运行 两次(更准确地说,确保你的交易是否违反不变量是确定性的)