數(shù)據(jù)操作技巧
禁用約束和觸發(fā)器
John Papa
代碼下載位置: DataPoints2007_04.exe (153 KB) Browse the Code Online
約束是在數(shù)據(jù)庫中維護數(shù)據(jù)一致性的重要工具。但是,有時禁用一個或多個約束來執(zhí)行某些任務(例如,使用脫機數(shù)據(jù)庫進行數(shù)據(jù)同步)會更方便。在使用 SQL Server? 復制技術同步數(shù)據(jù)庫之間的數(shù)據(jù)時,可以告知單個對象在復制過程中不強制約束。
例如,在使用 NOT FOR REPLICATION 語句定義外鍵約束時,SQL Server 在復制過程中將不會強制約束。實際上,NOT FOR REPLICATION 語句可以直接用于定義外鍵約束、檢查約束、標識和觸發(fā)器的 T-SQL 語句中。對于使用 SQL Server 復制的操作,在適當?shù)膶ο笊鲜褂?NOT FOR REPLICATION 語句是一種不錯的選擇。但是,如果您要手動執(zhí)行數(shù)據(jù)同步,則另一個方法是手動禁用約束和觸發(fā)器。
通常在需要同步數(shù)據(jù)子集以及需要更好地控制數(shù)據(jù)同步方法的聯(lián)機/脫機應用程序中執(zhí)行手動同步。在本月的專欄中,我將討論何時手動禁用和啟用約束會更有利,此方法可以幫助您解決哪些類型的問題以及一些解決問題的技巧。
禁用外鍵
我不建議從關系數(shù)據(jù)庫中刪除外鍵約束。但是,有時(例如在一系列表上執(zhí)行大量的插入和更新操作以及需要更準確的結果和更佳的性能)您可能需要臨時減少對一個或多個外鍵的引用完整性檢查。當然,您只能在以正常的關系順序無法對整個數(shù)據(jù)庫執(zhí)行大批量的數(shù)據(jù)更新時使用此方法。
因此何時才應禁用外鍵約束呢?假設您的關系數(shù)據(jù)結構有許多表,所有表都通過外鍵約束以某種方式彼此相關。與此數(shù)據(jù)庫交互的應用程序具有一個脫機/移動版本,該版本與可能駐留在便攜式計算機上的數(shù)據(jù)庫的第二個實例進行通信。對主數(shù)據(jù)庫所做的數(shù)據(jù)更改可能需要與脫機/移動數(shù)據(jù)庫同步,而同步數(shù)據(jù)的方法有好幾種。
同步數(shù)據(jù)的一個方法是將插入、更新和刪除操作應用于脫機/移動數(shù)據(jù)庫,以便與關系結構相一致。例如,在客戶相應的訂單前面插入客戶,并在相應的訂單詳細信息前面插入訂單。記錄刪除將按相反的方向進行(從子記錄到父記錄)。但是,在大型數(shù)據(jù)庫結構上應用此方法時,可能因為太復雜而無法實現(xiàn)和維護。
另一個方法是刪除外鍵約束,之后同步數(shù)據(jù),然后重新創(chuàng)建外鍵約束。此方法只需進行比較小的改動(只需禁用外鍵約束)即可正常運行。禁用外鍵約束之后,可以同步數(shù)據(jù),然后可以再次啟用外鍵。禁用外鍵的語法如下所示:
-- Disable foreign key constraint ALTER TABLE Orders NOCHECK CONSTRAINT FK_Orders_Customers -- Add a new Order record for a non-existent customer INSERT INTO Orders (CustomerID) VALUES ('BLAH') -- Select all orders for the non-existent customer SELECT * FROM Orders WHERE CustomerID = 'BLAH' 此外鍵強制“訂單”表中的 CustomerID 必須是“客戶”表中的有效 CustomerID。代碼會禁用外鍵然后將訂單插入到“訂單”表。插入的訂單記錄的 CustomerID 在父“客戶”表中不存在。由于外鍵已禁用,完整性檢查會被忽略,訂單記錄將成功插入。
以下代碼顯示了重新啟用外鍵約束然后測試外鍵約束工作是否正常的方法。執(zhí)行此代碼時,由于強制約束,訂單并未被插入。系統(tǒng)將返回錯誤消息,表明插入語句與外鍵約束出現(xiàn)沖突。
-- Enable foreign key constraint ALTER TABLE Orders CHECK CONSTRAINT FK_Orders_Customers -- Add a new Order record for a non-existent customer INSERT INTO Orders (CustomerID) VALUES ('BLEH') 在禁用外鍵、觸發(fā)器和其他約束時,一定要確保在該時間段內不會在數(shù)據(jù)庫上執(zhí)行任何數(shù)據(jù)操作語言 (DML) 操作。這必須在手動執(zhí)行禁用選項時進行處理。如果您使用 SQL Server 復制和 NOT FOR REPLICATION 語句同步數(shù)據(jù),將會自動處理該條件。
禁用觸發(fā)器
也可以在需要的時候禁用觸發(fā)器。有時您并不想在同步數(shù)據(jù)時激發(fā)觸發(fā)器。例如,如果您更新脫機/移動數(shù)據(jù)庫(如上例所示)時,您可能沒有注意到幾個表中可能有觸發(fā)器。在將行插入觸發(fā)器所在的表從而導致該行被再次插入另一個表時,可能會激發(fā)這些觸發(fā)器。但是在大型數(shù)據(jù)同步中,您可能并不希望再次插入這些類型的插入。為了避免出現(xiàn)這種不確定的事情,您可以臨時禁用觸發(fā)器,如下所示:
DISABLE TRIGGER MyTriggerName ON MyTableName 與之相反,若要啟用觸發(fā)器,請執(zhí)行以下代碼:
ENABLE TRIGGER MyTriggerName ON MyTableName 以下代碼顯示了當在“區(qū)域”表中插入或更新一行或多行時會激發(fā)的觸發(fā)器(并顯示一條信息)。
CREATE TRIGGER trRegion_InsertUpdate ON Region FOR INSERT, UPDATE AS PRINT ' Trigger is running. ' + CAST(@@ROWCOUNT AS VARCHAR(10)) + ' row(s) affected.' GO 您可以通過將一個新區(qū)域插入到“區(qū)域”表中測試此觸發(fā)器,如下所示:
INSERT INTO Region (RegionID, RegionDescription) VALUES (5, 'Some New Region') 在查詢窗口中執(zhí)行此代碼時,新區(qū)域將被插入“區(qū)域”表中,然后觸發(fā)器會被激發(fā),消息窗格中將顯示一條消息。若要禁用觸發(fā)器,您可以執(zhí)行以下代碼:
DISABLE TRIGGER trRegion_InsertUpdate ON Region 那么,如果您執(zhí)行此代碼來更新新區(qū)域,則數(shù)據(jù)會被更新而不會激發(fā)觸發(fā)器:
UPDATE Region SET RegionDescription = 'A Great Region' WHERE RegionID = 5 若要重新啟用觸發(fā)器,只需執(zhí)行以下查詢:
ENABLE TRIGGER trRegion_InsertUpdate ON Region 同步脫機/移動數(shù)據(jù)庫
禁用單個觸發(fā)器或外鍵會很有用,但是在我介紹的脫機/移動數(shù)據(jù)庫同步方案中,一次禁用所有觸發(fā)器或外鍵將非常有用。若要執(zhí)行此操作,您應執(zhí)行以下步驟:禁用所有外鍵約束,禁用所有觸發(fā)器,執(zhí)行插入、更新和刪除操作,啟用所有外鍵約束,并最終重新啟用所有觸發(fā)器。
通過禁用觸發(fā)器和外鍵約束,已經(jīng)修改了數(shù)據(jù)的訂單會變得不一致。在小型數(shù)據(jù)庫(例如,羅斯文數(shù)據(jù)庫)中,這種節(jié)省可能微不足道;但是在具有數(shù)十個或數(shù)百個表和關系的大型數(shù)據(jù)庫中,將會節(jié)省大量時間。此外,如果沒有此方法,則每次數(shù)據(jù)庫架構添加新表和新關系時,您將不得不在腳本中修改表的順序以便插入/更新/刪除數(shù)據(jù)。禁用外鍵約束和觸發(fā)器之后,由于順序不再重要,因此您只需將表添加到腳本末尾即可。
請注意最后啟用觸發(fā)器和外鍵很重要,而不管腳本是否成功。例如,如果您的腳本禁用約束和觸發(fā)器,并嘗試修改數(shù)據(jù),但未成功;您仍希望最后啟用約束和觸發(fā)器。
光標和信息架構視圖
若要禁用所有外鍵,必須首先收集外鍵以及每個外鍵適用的表的列表。這里,您可以借助于光標和 INFORMATION_SCHEMA 視圖。名為 INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS 的視圖將返回當前數(shù)據(jù)庫中的外鍵列表。若要獲得外鍵適用的表的名稱,請使用名為 INFORMATION_SCHEMA.TABLE_CONSTRAINTS 的視圖。通過將以下代碼加入這兩個視圖可以返回所有外鍵及其相應表的列表。
SELECT ref.constraint_name AS FK_Name, fk.table_name AS FK_Table FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS ref INNER JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS fk ON ref.constraint_name = fk.constraint_name ORDER BY fk.table_name, ref.constraint_name 禁用所有外鍵
有了這個列表后,您可以編寫腳本來禁用外鍵。您可以聲明并打開光標,循環(huán)訪問該列表,然后創(chuàng)建用于禁用外鍵的 T-SQL 命令,并針對每個外鍵執(zhí)行該命令(請參見圖 1)。
![]() CREATE PROCEDURE pr_Disable_Triggers_v2 @disable BIT = 1 AS DECLARE @sql VARCHAR(500), @tableName VARCHAR(128), @tableSchema VARCHAR(128) -- List of all tables DECLARE triggerCursor CURSOR FOR SELECT t.TABLE_NAME AS TableName, t.TABLE_SCHEMA AS TableSchema FROM INFORMATION_SCHEMA.TABLES t ORDER BY t.TABLE_NAME, t.TABLE_SCHEMA OPEN triggerCursor FETCH NEXT FROM triggerCursor INTO @tableName, @tableSchema WHILE ( @@FETCH_STATUS = 0 ) BEGIN IF @disable = 1 SET @sql = ‘ALTER TABLE ‘ + @tableSchema + ‘.[‘ + @tableName + ‘] DISABLE TRIGGER ALL’ ELSE SET @sql = ‘ALTER TABLE ‘ + @tableSchema + ‘.[‘ + @tableName + ‘] ENABLE TRIGGER ALL’ PRINT ‘Executing Statement - ‘ + @sql EXECUTE ( @sql ) FETCH NEXT FROM triggerCursor INTO @tableName, @tableSchema END CLOSE triggerCursor DEALLOCATE triggerCursor 首先,foreignKeyCursor 光標被聲明為收集外鍵及其表名稱列表的 SELECT 語句。接下來,打開光標并執(zhí)行初始 FETCH 語句。此 FETCH 語句將第一行的數(shù)據(jù)讀取到局部變量 @foreignKeyName 和 @tableName。
在循環(huán)光標時,您可以看到 @@FETCH_STATUS 的值為 0,這表示提取成功。這意味著循環(huán)將會繼續(xù),因此可以從行集中獲取各個后續(xù)外鍵。
連接上的所有光標都可以使用 @@FETCH_STATUS。因此,如果您正在循環(huán)多個光標,一定要檢查緊跟著 FETCH 語句的語句中 @@FETCH_STATUS 的值。@@FETCH_STATUS 將反映連接中最近的 FETCH 操作的狀態(tài)。@@FETCH_STATUS 的有效值包括:
在循環(huán)內,代碼會根據(jù)不同意圖(禁用還是啟用外鍵約束)來構建不同的 ALTER TABLE 命令(使用 CHECK 或 NOCHECK 關鍵字)。接下來,語句將被打印成一條消息(這樣可以看到其進度),然后執(zhí)行該語句。最后在循環(huán)處理所有行之后,存儲過程關閉并釋放光標。
禁用所有觸發(fā)器
圖 1 中的存儲過程將禁用或啟用數(shù)據(jù)庫中的所有外鍵。在某些情況下,您可能希望在數(shù)據(jù)同步過程中禁用所有觸發(fā)器。圖 2 中的 pr_Disable_Triggers 存儲過程正好可以完成該操作。
![]() CREATE PROCEDURE pr_Disable_Triggers @disable BIT = 1 AS DECLARE @sql VARCHAR(500), @tableName VARCHAR(128), @triggerName VARCHAR(128), @tableSchema VARCHAR(128) -- List of all triggers and tables that exist on them DECLARE triggerCursor CURSOR FOR SELECT so_tr.name AS TriggerName, so_tbl.name AS TableName, t.TABLE_SCHEMA AS TableSchema FROM sysobjects so_tr INNER JOIN sysobjects so_tbl ON so_tr.parent_obj = so_tbl.id INNER JOIN INFORMATION_SCHEMA.TABLES t ON t.TABLE_NAME = so_tbl.name WHERE so_tr.type = ‘TR’ ORDER BY so_tbl.name ASC, so_tr.name ASC OPEN triggerCursor FETCH NEXT FROM triggerCursor INTO @triggerName, @tableName, @tableSchema WHILE ( @@FETCH_STATUS = 0 ) BEGIN IF @disable = 1 SET @sql = ‘DISABLE TRIGGER [‘ + @triggerName + ‘] ON ‘ + @tableSchema + ‘.[‘ + @tableName + ‘]’ ELSE SET @sql = ‘ENABLE TRIGGER [‘ + @triggerName + ‘] ON ‘ + @tableSchema + ‘.[‘ + @tableName + ‘]’ PRINT ‘Executing Statement - ‘ + @sql EXECUTE ( @sql ) FETCH NEXT FROM triggerCursor INTO @triggerName, @tableName, @tableSchema END CLOSE triggerCursor DEALLOCATE triggerCursor pr_Disable_Triggers 存儲過程將獲取一個包含當前數(shù)據(jù)庫中所有觸發(fā)器及其相應表名稱(和架構)的行集。由于沒有 INFORMATION_SCHEMA.TRIGGERS 視圖,我將從 SQL Server 2005 系統(tǒng)表和 INFORMATION_SCHEMA 視圖中收集觸發(fā)器列表和相關信息(如果可能)。
sysobjects 系統(tǒng)表位于所有數(shù)據(jù)庫中,可以通過查詢來返回所有觸發(fā)器或表的列表。此系統(tǒng)表本身也可以加入以獲取要操作的觸發(fā)器和表列表,如圖 2 中的光標聲明所示。
如果在數(shù)據(jù)庫上使用諸如 AdventureWorks 的存儲過程(表在特定的架構中),您必須將架構名稱作為表名稱的前綴。pr_Disable_Triggers 存儲過程通過加入 INFORMATION_SCHEMA.TABLES 視圖來處理此情況,該視圖返回表的 SCHEMA_NAME。
編寫要禁用和重新啟用觸發(fā)器和外鍵的例程后,您可以在腳本中使用這些例程(該腳本用于修改脫機/移動數(shù)據(jù)庫以使其與主數(shù)據(jù)庫保持同步)。例如,您可以使用以下腳本(將中間的注釋替換為數(shù)據(jù)操作運算)。
pr_Disable_Foreign_Keys 1 pr_Disable_Triggers 1 -- Perform data operations pr_Disable_Foreign_Keys 0 pr_Disable_Triggers 0 迅速禁用所有觸發(fā)器
您可以修改圖 2 中顯示的 pr_Disable_Triggers 存儲過程來執(zhí)行與前面稍有不同的 T-SQL 命令(該命令將啟用或禁用所有觸發(fā)器)。其中的 T-SQL 語句將使用 ALTER TABLE 命令(禁用或啟用一個表中的所有觸發(fā)器)的風格。通過使用該方法,您可以修改 pr_Disable_Triggers 存儲過程,針對每個表執(zhí)行以下語句來禁用該表上的所有觸發(fā)器:
ALTER TABLE MySchemaName.MyTableName DISABLE TRIGGER ALL 通過使用該語法,光標的查詢將收集全部表名稱,而無需收集觸發(fā)器本身的名稱。因此,可以通過修改存儲過程來使用該方法遍歷表列表并啟用或禁用每個表上的所有觸發(fā)器。修改后的存儲過程如圖 3 所示。
![]() CREATE PROCEDURE pr_Disable_Foreign_Keys @disable BIT = 1 AS DECLARE @sql VARCHAR(500), @tableName VARCHAR(128), @foreignKeyName VARCHAR(128) -- A list of all foreign keys and table names DECLARE foreignKeyCursor CURSOR FOR SELECT ref.constraint_name AS FK_Name, fk.table_name AS FK_Table FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS ref INNER JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS fk ON ref.constraint_name = fk.constraint_name ORDER BY fk.table_name, ref.constraint_name OPEN foreignKeyCursor FETCH NEXT FROM foreignKeyCursor INTO @foreignKeyName, @tableName WHILE ( @@FETCH_STATUS = 0 ) BEGIN IF @disable = 1 SET @sql = ‘ALTER TABLE [‘ + @tableName + ‘] NOCHECK CONSTRAINT [‘ + @foreignKeyName + ‘]’ ELSE SET @sql = ‘ALTER TABLE [‘ + @tableName + ‘] CHECK CONSTRAINT [‘ + @foreignKeyName + ‘]’ PRINT ‘Executing Statement - ‘ + @sql EXECUTE(@sql) FETCH NEXT FROM foreignKeyCursor INTO @foreignKeyName, @tableName END CLOSE foreignKeyCursor DEALLOCATE foreignKeyCursor 結束語
與從 .NET 應用程序逐一執(zhí)行各個數(shù)據(jù)庫操作相比,從 T-SQL 腳本執(zhí)行多個數(shù)據(jù)庫操作(如本文所述)會更高效。例如,您可以使用 ADO.NET 收集 .NET 應用程序的外鍵列表,然后執(zhí)行相應的命令來禁用各個外鍵。同樣可以將其擴展為獲取觸發(fā)器列表,然后禁用觸發(fā)器。所有這些操作都需要在 .NET 代碼和數(shù)據(jù)庫之間來回進行操作,這與完全在 T-SQL 的數(shù)據(jù)庫服務器上執(zhí)行代碼相比會占用更多的資源。
將您想向 John 詢問的問題和提出的意見發(fā)送至:mmdata@microsoft.com mmdata@microsoft.com.
John Papa 是 ASPSOFT () 的高級 .NET 顧問,也是一名棒球迷,在夏季的夜晚,他的大多數(shù)時光都是和家人及其忠實的狗 Kadi 一起為洋基隊加油助威度過的。John 是 C# 領域的一位 MVP,他撰寫了多本有關 ADO、XML 和 SQL Server 的書籍,而且經(jīng)常在行業(yè)會議(如 VSLive)上發(fā)表演講,或者在 /blogs/john.papa 上發(fā)表博客文章。
|
|