禁用約束和觸發器

約束是在數據庫中維護數據一致性的重要工具。但是,有時禁用一個或多個約束來執行某些任務(例如,使用脫機數據庫進行數據同步)會更方便。在使用 SQL Server™ 複製技術同步數據庫之間的數據時,可以告知單個對象在複製過程中不強制約束。
例如,在使用 NOT FOR REPLICATION 語句定義外鍵約束時,SQL Server 在複製過程中將不會強制約束。實際上,NOT FOR REPLICATION 語句可以直接用於定義外鍵約束、檢查約束、標識和觸發器的 T-SQL 語句中。對於使用 SQL Server 複製的操作,在適當的對象上使用 NOT FOR REPLICATION 語句是一種不錯的選擇。但是,如果您要手動執行數據同步,則另一個方法是手動禁用約束和觸發器。
通常在需要同步數據子集以及需要更好地控制數據同步方法的聯機/脫機應用程序中執行手動同步。在本月的專欄中,我將討論何時手動禁用和啓用約束會更有利,此方法可以幫助您解決哪些類型的問題以及一些解決問題的技巧。

禁用外鍵
我不建議從關係數據庫中刪除外鍵約束。但是,有時(例如在一系列表上執行大量的插入和更新操作以及需要更準確的結果和更佳的性能)您可能需要臨時減少對一個或多個外鍵的引用完整性檢查。當然,您只能在以正常的關係順序無法對整個數據庫執行大批量的數據更新時使用此方法。
因此何時才應禁用外鍵約束呢?假設您的關係數據結構有許多表,所有表都通過外鍵約束以某種方式彼此相關。與此數據庫交互的應用程序具有一個脫機/移動版本,該版本與可能駐留在便攜式計算機上的數據庫的第二個實例進行通信。對主數據庫所做的數據更改可能需要與脫機/移動數據庫同步,而同步數據的方法有好幾種。
同步數據的一個方法是將插入、更新和刪除操作應用於脫機/移動數據庫,以便與關係結構相一致。例如,在客戶相應的訂單前面插入客戶,並在相應的訂單詳細信息前面插入訂單。記錄刪除將按相反的方向進行(從子記錄到父記錄)。但是,在大型數據庫結構上應用此方法時,可能因爲太複雜而無法實現和維護。
另一個方法是刪除外鍵約束,之後同步數據,然後重新創建外鍵約束。此方法只需進行比較小的改動(只需禁用外鍵約束)即可正常運行。禁用外鍵約束之後,可以同步數據,然後可以再次啓用外鍵。禁用外鍵的語法如下所示:
-- 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 在父“客戶”表中不存在。由於外鍵已禁用,完整性檢查會被忽略,訂單記錄將成功插入。
以下代碼顯示了重新啓用外鍵約束然後測試外鍵約束工作是否正常的方法。執行此代碼時,由於強制約束,訂單並未被插入。系統將返回錯誤消息,表明插入語句與外鍵約束出現衝突。
-- 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')
在禁用外鍵、觸發器和其他約束時,一定要確保在該時間段內不會在數據庫上執行任何數據操作語言 (DML) 操作。這必須在手動執行禁用選項時進行處理。如果您使用 SQL Server 複製和 NOT FOR REPLICATION 語句同步數據,將會自動處理該條件。

禁用觸發器
也可以在需要的時候禁用觸發器。有時您並不想在同步數據時激發觸發器。例如,如果您更新脫機/移動數據庫(如上例所示)時,您可能沒有注意到幾個表中可能有觸發器。在將行插入觸發器所在的表從而導致該行被再次插入另一個表時,可能會激發這些觸發器。但是在大型數據同步中,您可能並不希望再次插入這些類型的插入。爲了避免出現這種不確定的事情,您可以臨時禁用觸發器,如下所示:
DISABLE TRIGGER MyTriggerName ON MyTableName
與之相反,若要啓用觸發器,請執行以下代碼:
ENABLE TRIGGER MyTriggerName ON MyTableName
以下代碼顯示了當在“區域”表中插入或更新一行或多行時會激發的觸發器(並顯示一條信息)。
CREATE TRIGGER trRegion_InsertUpdate ON Region
    FOR INSERT, UPDATE
AS
    PRINT ' Trigger is running. ' 
          +  CAST(@@ROWCOUNT AS VARCHAR(10)) 
          + ' row(s) affected.'
GO
您可以通過將一個新區域插入到“區域”表中測試此觸發器,如下所示:
INSERT INTO Region (RegionID, RegionDescription) 
VALUES (5, 'Some New Region')
在查詢窗口中執行此代碼時,新區域將被插入“區域”表中,然後觸發器會被激發,消息窗格中將顯示一條消息。若要禁用觸發器,您可以執行以下代碼:
DISABLE TRIGGER trRegion_InsertUpdate ON Region
那麼,如果您執行此代碼來更新新區域,則數據會被更新而不會激發觸發器:
UPDATE Region SET RegionDescription = 'A Great Region' 
WHERE RegionID = 5
若要重新啓用觸發器,只需執行以下查詢:
ENABLE TRIGGER trRegion_InsertUpdate ON Region

同步脫機/移動數據庫
禁用單個觸發器或外鍵會很有用,但是在我介紹的脫機/移動數據庫同步方案中,一次禁用所有觸發器或外鍵將非常有用。若要執行此操作,您應執行以下步驟:禁用所有外鍵約束,禁用所有觸發器,執行插入、更新和刪除操作,啓用所有外鍵約束,並最終重新啓用所有觸發器。
通過禁用觸發器和外鍵約束,已經修改了數據的訂單會變得不一致。在小型數據庫(例如,羅斯文數據庫)中,這種節省可能微不足道;但是在具有數十個或數百個表和關係的大型數據庫中,將會節省大量時間。此外,如果沒有此方法,則每次數據庫架構添加新表和新關係時,您將不得不在腳本中修改表的順序以便插入/更新/刪除數據。禁用外鍵約束和觸發器之後,由於順序不再重要,因此您只需將表添加到腳本末尾即可。
請注意最後啓用觸發器和外鍵很重要,而不管腳本是否成功。例如,如果您的腳本禁用約束和觸發器,並嘗試修改數據,但未成功;您仍希望最後啓用約束和觸發器。

光標和信息架構視圖
若要禁用所有外鍵,必須首先收集外鍵以及每個外鍵適用的表的列表。這裏,您可以藉助於光標和 INFORMATION_SCHEMA 視圖。名爲 INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS 的視圖將返回當前數據庫中的外鍵列表。若要獲得外鍵適用的表的名稱,請使用名爲 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 

禁用所有外鍵
有了這個列表後,您可以編寫腳本來禁用外鍵。您可以聲明並打開光標,循環訪問該列表,然後創建用於禁用外鍵的 T-SQL 命令,並針對每個外鍵執行該命令(請參見圖 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 語句。接下來,打開光標並執行初始 FETCH 語句。此 FETCH 語句將第一行的數據讀取到局部變量 @foreignKeyName 和 @tableName。
在循環光標時,您可以看到 @@FETCH_STATUS 的值爲 0,這表示提取成功。這意味着循環將會繼續,因此可以從行集中獲取各個後續外鍵。
連接上的所有光標都可以使用 @@FETCH_STATUS。因此,如果您正在循環多個光標,一定要檢查緊跟着 FETCH 語句的語句中 @@FETCH_STATUS 的值。@@FETCH_STATUS 將反映連接中最近的 FETCH 操作的狀態。@@FETCH_STATUS 的有效值包括:
  • 0 = FETCH 成功
  • -1 = FETCH 失敗
  • -2 = 提取的行丟失
在循環內,代碼會根據不同意圖(禁用還是啓用外鍵約束)來構建不同的 ALTER TABLE 命令(使用 CHECK 或 NOCHECK 關鍵字)。接下來,語句將被打印成一條消息(這樣可以看到其進度),然後執行該語句。最後在循環處理所有行之後,存儲過程關閉並釋放光標。

禁用所有觸發器
圖 1 中的存儲過程將禁用或啓用數據庫中的所有外鍵。在某些情況下,您可能希望在數據同步過程中禁用所有觸發器。圖 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 存儲過程將獲取一個包含當前數據庫中所有觸發器及其相應表名稱(和架構)的行集。由於沒有 INFORMATION_SCHEMA.TRIGGERS 視圖,我將從 SQL Server 2005 系統表和 INFORMATION_SCHEMA 視圖中收集觸發器列表和相關信息(如果可能)。
sysobjects 系統表位於所有數據庫中,可以通過查詢來返回所有觸發器或表的列表。此係統表本身也可以加入以獲取要操作的觸發器和表列表,如圖 2 中的光標聲明所示。
如果在數據庫上使用諸如 AdventureWorks 的存儲過程(表在特定的架構中),您必須將架構名稱作爲表名稱的前綴。pr_Disable_Triggers 存儲過程通過加入 INFORMATION_SCHEMA.TABLES 視圖來處理此情況,該視圖返回表的 SCHEMA_NAME。
編寫要禁用和重新啓用觸發器和外鍵的例程後,您可以在腳本中使用這些例程(該腳本用於修改脫機/移動數據庫以使其與主數據庫保持同步)。例如,您可以使用以下腳本(將中間的註釋替換爲數據操作運算)。
pr_Disable_Foreign_Keys  1
pr_Disable_Triggers 1
-- Perform data operations
pr_Disable_Foreign_Keys  0
pr_Disable_Triggers 0

迅速禁用所有觸發器
您可以修改圖 2 中顯示的 pr_Disable_Triggers 存儲過程來執行與前面稍有不同的 T-SQL 命令(該命令將啓用或禁用所有觸發器)。其中的 T-SQL 語句將使用 ALTER TABLE 命令(禁用或啓用一個表中的所有觸發器)的風格。通過使用該方法,您可以修改 pr_Disable_Triggers 存儲過程,針對每個表執行以下語句來禁用該表上的所有觸發器:
ALTER TABLE MySchemaName.MyTableName DISABLE TRIGGER ALL
通過使用該語法,光標的查詢將收集全部表名稱,而無需收集觸發器本身的名稱。因此,可以通過修改存儲過程來使用該方法遍歷表列表並啓用或禁用每個表上的所有觸發器。修改後的存儲過程如圖 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 應用程序逐一執行各個數據庫操作相比,從 T-SQL 腳本執行多個數據庫操作(如本文所述)會更高效。例如,您可以使用 ADO.NET 收集 .NET 應用程序的外鍵列表,然後執行相應的命令來禁用各個外鍵。同樣可以將其擴展爲獲取觸發器列表,然後禁用觸發器。所有這些操作都需要在 .NET 代碼和數據庫之間來回進行操作,這與完全在 T-SQL 的數據庫服務器上執行代碼相比會佔用更多的資源。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章