在 Day25 我們談到了列舉類型 (enum),某種程度來講,列舉類型也算是一種條件限制,它限制了填入特定欄位的值只能是某個固定的集合。
條件限制在 SQL 資料庫來講,最常用的條件限制不外乎下列三種:
就我個人的經驗,在簡單的使用案例,使用上述三種已足以確保相當程度的系統正確性。接下來,我們來對 Datomic 在常見應用的對應作法做討論。
在 SQL 來講,當設定主鍵 (primary) 給特定欄位時,也就等同於設定了兩個條件限制:不容許空值 (NULL)、且值不可重複。
然而,在 Datomic 不需思考這個問題,因為主鍵就是資料實體編碼 (entity id),資料實體編碼在整個資料庫之內都不會重複使用、且任何的資料實體都一定會有一個編碼,所以必然滿足上述的兩個性質。
Datomic 並沒有這種條件限制的對應語法,因為 Datomic 不容許你對任何的屬性寫入空值 NULL
,所以也不需要這種語法。
那該怎麼表現某個資料實體沒有某個屬性呢?
很簡單,不要寫入資料庫即可。比方說,像下方的 交易資料 (tx-data),我們寫入了兩個 item,每個 item 都有兩個屬性。
[{:db/id item-1-id
:line-item/product chocolate
:line-item/quantity 1}
{:db/id item-2-id
:line-item/product whisky
:line-item/quantity 2}]
如果說,我們要讓 item-3 沒有 :line-item/quantity
呢?寫成下方的形式即可:
[{:db/id item-3-id
:line-item/product happy}]
讀者注意到了嗎?這就是欄位綱要 (column schema) 的優勢,它帶來了更清晰的語意。
參考下方的範例,在 SQL 的不可重複條件限制,在 Datomic 只要在對應的屬性加上:db/unique :db.unique/identity
即可。
{:db/ident :user/uuid,
:db/valueType :db.type/uuid,
:db/doc "Unique user identifier",
:db/cardinality :db.cardinality/one,
:db/unique :db.unique/identity}
Datomic 沒有提供簡潔的語法來讓使用者寫出外鍵的語意,
外鍵固然有確保資料一致性好處,但是它也會造成了額外的開發成本,特別是在做測試的時候。很多時候,我想做個簡單的整合測試,需要在資料庫裡放入一些測試資料,本來想說,只要生成一張資料表的測試資料即可。結果,因為有了外鍵,結果變成我得生成三張資料表的測試資料。
也因此,我傾向認為,Datomic 是刻意不對外鍵條件限制設計簡潔的語法,因為它不鼓勵一定要這麼做。
在常見應用的外鍵,我們提到了 Datomic 沒有提供簡潔的外鍵寫法。前面這句話只說了一半,實際上,一旦使用了 Datomic 的自訂斷言函數,當然也是沒有什麼複雜的條件限制表達不出來的。
在之前 Day18 時,我們有示範,如果有一些比較複雜的聚合查詢難以表達,我們可以用自訂聚合函數,而且這還可以讓語意更加清晰。Datomic 在條件限制也是一樣的設計,它提供兩種自訂的條件限制,一種是屬性斷言函數 (Attribute Predicates),可用來強化屬性的語意;另一種是資料實體規格 (Entity Specs),可用來對資料實體本身做出種種限制。
這邊又要再做一個詞彙的釐清,在 Clojure/Datomic 的語境裡,條件限制 (constraints) 與規格 (spec) 幾乎是同義詞,常常會交替使用。
有時候,我們指定某個屬性的資料型態是字串,但是,實際上,我們存入該屬性的永遠是 Email,而 Email 有固定的格式。在這種情況下,我們就可以用屬性斷言函數來進一步強化語意。
下方,我們用「檢查姓名的字串的長度必須大於等於 3 且小於等於 15 個字元」來示範屬性斷言函數。
'datomic.samples.attr-preds/user-name?
ns datomic.samples.attr-preds)
(defn user-name?
[s]
(<= 3 (count s) 15))
:db.attr/preds
,就完成屬性斷言函數的設定。{:db/ident :user/name,
:db/valueType :db.type/string,
:db/cardinality :db.cardinality/one,
:db.attr/preds 'datomic.samples.attr-preds/user-name?}
屬性斷言函數可以強化單一屬性的限制語意,那如果我們想要限制的特性,它是屬於整個資料實體的呢?比方說,特定的資料實體,至少要包含某些特定的屬性,又或是資料彼此之間的互相關系。
這種隸屬於資料實體的條件限制,Datomic 提供了資料實體規格 (Entity Specs) 的語法來我們表達。
首先,資料實體規格本身也是一種資料實體。讀者從一路讀過來,已經讀過了很多 Datomic 用資料實體來表現的語意了。這邊做一個小整理,本系列文中用資料實體表現過的資料包含:
先看一個例子:利用資料實體規格來限制「必須欄位」,作法有兩個步驟:
下方,我們定義一個叫做 :user/validate
的資料實體規格,它規定必須包含 :user/name
和 :user/email
兩個欄位。
{:db/ident :user/validate
:db.entity/attrs [:user/name :user/email]}
:db/ensure
觸發檢查下方是一筆交易資料,注意到,我們額外加上了一個 :db/ensure
這個虛擬屬性,它會讓 Datomic 在寫入資料時,特別檢查此時此刻寫入的資料實體是否滿足對應的資料實體規格。
{:user/name "John Doe"
:db/ensure :user/validate}
資料之間的關系就必須要在資料實體規格裡也呼叫函數,即資料實體斷言函數 (Entity Predicates)。下方看一個例子,它由三個步驟構成:
datomic.samples.entity-preds/scores-are-ordered?
(ns datomic.samples.entity-preds
(:require [datomic.api :as d]))
(defn scores-are-ordered?
[db eid]
(let [m (d/pull db [:score/low :score/high] eid)]
(<= (:score/low m) (:score/high m))))
:db.entity/preds
,就完成資料實體斷言函數的設定。{:db/ident :score/guard
:db.entity/attrs [:score/low :score/high] ;; required attributes
:db.entity/preds 'datomic.samples.entity-preds/scores-are-ordered?} ;; entity predicate
:db/ensure
觸發檢查下方的 :db/ensure
會讓這筆交易資料在寫入時,觸發 :score/guard
來做檢查。
{:score/low 100
:score/high 20
:db/ensure :score/guard}