如果你不熟悉CHECK約束,那麼習慣上它們通過限制插入到一個字段中的值來執行字段域完整性。我發現CHECK約束很不幸地被用於大多數的數據模型中。在大多數的案例中,我看到使用觸發器來執行業務規則,而這本來是應該用一個CHECK約束來執行的。我個人使用CHECK而不是觸發器是基於很多個原因的。首先,我不需要浪費時間來寫專門的代碼。另外,CHECK約束是在數據庫修改通過數據庫引擎完成之前執行的,然而觸發器是事故發生後檢查的。使用觸發器延長了一個事務的生命,如果檢測到一個回滾事件那麼代價可能是昂貴的。
SQL Server允許你定義檢查單獨字段完整性的字段級別的CHECK約束。此外,SQL Server允許你使用一個表級別CHECK約束來檢查多個字段的值。雖然它們被歸類為表級別約束,但是這些CHECK約束的類型實際上是在行級別上檢查的。最後,CHECK約束通過檢查一個定義好的情形來看它的值為TRUE還是FALSE。
讓我們來看看一些例子。我將建立一張示例表EMPLOYEE。作為表定義的一部分,我們將執行一個公司規則:在我們的系統中沒有一個員工被允許賺取超過$100,000或者少於$30,000。
CREATE TABLE DBO.EMPLOYEE
(
EMPLOYEEID INT IDENTITY(1,1) NOT NULL,
FIRSTNAME VARCHAR(50) NOT NULL,
LASTNAME VARCHAR(50) NOT NULL,
IS_MANAGER BIT NULL,
SALARY MONEY NOT NULL,
BONUSPCT FLOAT NOT NULL
)
GO
ALTER TABLE DBO.EMPLOYEE
ADD CONSTRAINT PK_EMPLOYEE PRIMARY KEY (EMPLOYEEID)
GO
ALTER TABLE DBO.EMPLOYEE
ADD CONSTRAINT DF_EMPLOYEE_BONUSPCT DEFAULT 0.00 FOR BONUSPCT
GO
ALTER TABLE DBO.EMPLOYEE
ADD CONSTRAINT CK_EMPLOYEE_SALARY CHECK (SALARY BETWEEN 30000 AND 100000)
GO
EXEC SP_HELPCONSTRAINT EMPLOYEE
GO
查看我們表中的約束,我們看到我們簡單的字段級別約束已經定義好了:
圖一
嘗試把值插入到SALARY字段超出了我們定義的范圍,這也被我們的數據庫引擎成功捕捉到了。
INSERT INTO DBO.EMPLOYEE (FIRSTNAME, LASTNAME, SALARY)
SELECT 'GEORGE', 'WASHINGTON', 110000
GO
圖二
到目前為止這些都進展得很順利。但是,這裡還有另外一個公司規則需要我們來執行。我們公司的管理人員可以賺取任何獎金比例但是他們是唯一被允許賺取他們工資5%或者更高比例獎金的員工。非管理人員只能賺取低於5%的任何比例獎金。讓我們來試著通過一個表級別的CHECK約束來執行這個公司規則:
ALTER TABLE DBO.EMPLOYEE
ADD CONSTRAINT CK_EMPLOYEE_BONUSPCT
CHECK (IS_MANAGER = 1 AND BONUSPCT >= 5.00)
GO
INSERT INTO DBO.EMPLOYEE (FIRSTNAME, LASTNAME, IS_MANAGER, SALARY, BONUSPCT)
SELECT 'GEORGE', 'WASHINGTON', 1, 100000, 5.00
GO
INSERT INTO DBO.EMPLOYEE (FIRSTNAME, LASTNAME, IS_MANAGER, SALARY, BONUSPCT)
SELECT 'BEN', 'FRANKLIN', 0, 75000, 2.50
GO
圖三
然後發生了什麼事呢?員工Washington成功插入但是Franklin插入失敗。那麼,正如它被定義的,CHECK約束迫使所有的員工成為一個管理人員並且具有一個大於或等於5%的獎金比例。如果所有的員工必須是具有大於或者等於5%的獎金比例的管理人員,那麼這就沒有什麼問題。然而,這不是我們想要執行的業務規則。我們想要確保如果一個員工不是管理人員,那麼他就不能執行查果5%獎金的界限。因此我們應該怎樣使CHECK約束有條件地執行完整性呢?你可能想使用一個觸發器。盡管如此,正如我在開篇提到的,CHECK是用來檢查TRUE/FALSE條件的。因此,你可以平衡這兩種方法來產生一個可以執行條件檢查的CHECK約束。讓我們通過使用這些原則來重新創建我們的CHECK並試著重新插入員工Franklin。
ALTER TABLE DBO.EMPLOYEE
DROP CONSTRAINT CK_EMPLOYEE_BONUSPCT
GO
ALTER TABLE DBO.EMPLOYEE
ADD CONSTRAINT CK_EMPLOYEE_BONUSPCT
CHECK (CASE WHEN IS_MANAGER <> 1 AND BONUSPCT >= 5.00 THEN 1 ELSE 0 END = 0)
GO
INSERT INTO DBO.EMPLOYEE (FIRSTNAME, LASTNAME, IS_MANAGER, SALARY, BONUSPCT)
SELECT 'BEN', 'FRANKLIN', 0, 75000, 2.50
GO
現在試著使用正確的CHECK約束定義來插入Franklin,結果成功了。
圖四
現在讓我們嘗試插入一個新的員工Jefferson。
INSERT INTO DBO.EMPLOYEE (FIRSTNAME, LASTNAME, IS_MANAGER, SALARY, BONUSPCT)
SELECT 'THOMAS', 'JEFFERSON', NULL, 80000, 7.50
GO
圖五
等待一秒鐘,Jefferson沒有被標記成一個管理人員但是他的引擎允許他的獎金比例變成7.5%。為什麼會發生這種情況呢?再次回顧CHECK約束是用於檢查TRUE/FALSE條件的。看看我們的模式,IS_MANAGER被聲明成NULL。由於這個字段中的任何空值都會導致CHECK的IS_MANAGER條件等同於不知道的並且導致CASE表達求取我們的默認布爾值為零。當使用等於零的字段時,這是一個需要明白的地方。有很多種方法可以糾正這點。其中一種是把IS_MANAGER標記定義為NOT NULL從而把NULL值轉化為零。如果你不能改變這個模式,那麼另外的方法就是把CASE重新寫到NULL IS_MANAGER 標記的賬戶中。下面是其中一種重寫CASE的方法。(你可能有自己的更改;我的版本是為了達到例證的目的)。
TRUNCATE TABLE DBO.EMPLOYEE
GO
ALTER TABLE DBO.EMPLOYEE
DROP CONSTRAINT CK_EMPLOYEE_BONUSPCT
GO
ALTER TABLE DBO.EMPLOYEE
ADD CONSTRAINT CK_EMPLOYEE_BONUSPCT
CHECK (CASE WHEN IS_MANAGER IS NULL AND BONUSPCT >= 5.00 THEN 1
WHEN IS_MANAGER <> 1 AND BONUSPCT >= 5.00 THEN 1
ELSE 0 END = 0)
GO
INSERT INTO DBO.EMPLOYEE (FIRSTNAME, LASTNAME, IS_MANAGER, SALARY, BONUSPCT)
SELECT 'JAMES', 'MADISON', NULL, 60000, 5.50
GO
執行一個新員工Madison,的腳本,我們可以看到現在這個引擎成功捕捉了這個條件。
圖六
現在你可能在想“我打賭我能寫一個純量函數來讀取我整個表並且執行一個真實的表級別CHECK以便檢查超過的數目和總量”。你是對的,你可以做到。但是正如SQL Server MVP David Portas所提出的,這裡也有風險。
正如你看到的,CHECK約束是執行單個和多個字段域完整性而不需要寫專門的觸發器或者存儲過程代碼的強有力的方法。