{"id":1922,"date":"2014-12-30T00:00:00","date_gmt":"2014-12-30T00:00:00","guid":{"rendered":"https:\/\/test.simple-talk.com\/uncategorized\/archiving-hierarchical-deleted-transactions-using-xml\/"},"modified":"2021-09-29T16:21:32","modified_gmt":"2021-09-29T16:21:32","slug":"archiving-hierarchical-deleted-transactions-using-xml","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/archiving-hierarchical-deleted-transactions-using-xml\/","title":{"rendered":"Archiving Hierarchical, Deleted Transactions Using XML"},"content":{"rendered":"<div class=\"article-content\">\n<p> \tAlthough I&#8217;m not generally a big proponent of storing XML in a SQL database, I feel that the archiving of deleted data that relates to a single business transaction is one of the rare cases where it is reasonable option.  <\/p>\n<h2>Some Sample Data<\/h2>\n<p> \tLet&#8217;s start by using <a href=\"http:\/\/en.wikipedia.org\/wiki\/Data_definition_language\">Data Definition Language<\/a> (DDL) to create a couple of example tables, and use <a href=\"http:\/\/en.wikipedia.org\/wiki\/Data_manipulation_language\">Data Manipulation Language<\/a> (DMS) to populate those tables. These are borrowed from a previous article (I love reusing code). The data model is an <strong>Invoices<\/strong> table with child, detail records stored in an <strong>Invoice_Details<\/strong> table. <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">CREATE TABLE dbo.Invoices\n(\n    InvoiceNo           VARCHAR(20)     NOT NULL\n    ,InvoiceDate        DATE            NOT NULL\n    ,CustomerID         VARCHAR(20)     NOT NULL\n    ,CustomerName       VARCHAR(100)    NOT NULL\n    ,CustomerAddr1      VARCHAR(100)    NOT NULL\n    ,CustomerAddr2      VARCHAR(100)    NULL\n    ,CustomerZipcode    VARCHAR(10)     NOT NULL\n    ,PRIMARY KEY (InvoiceNo)\n);\n\n-- Seed the Invoices table\nINSERT INTO dbo.Invoices \n(\n    InvoiceNo, InvoiceDate, CustomerID, CustomerName\n    ,CustomerAddr1, CustomerAddr2, CustomerZipcode\n)\nSELECT 'IN20141000001', '2014-11-03', '20001', 'ACME MINERALS', '35 WESTCHESTER ST'\n    ,'CAIRNS, AU', '4810'\nUNION ALL\nSELECT 'IN20141000002', '2014-11-04', '20002', 'ACE CHEMICALS', '201 BROADWAY AVE'\n    ,'CAIRNS, AU', '4810';\n\nCREATE TABLE dbo.Invoice_Details\n(\n    InvoiceNo           VARCHAR(20)     NOT NULL\n    ,LineItem           BIGINT IDENTITY\n    ,ItemNo             VARCHAR(20)     NOT NULL\n    ,Quantity           INT             NOT NULL\n    ,Price              MONEY           NOT NULL\n    ,ExtendedPrice      AS (Quantity * Price)\n    ,PRIMARY KEY (InvoiceNo, LineItem)\n    ,CONSTRAINT invd_fk1 FOREIGN KEY (InvoiceNo) REFERENCES dbo.Invoices (InvoiceNo)\n);\n\n-- Seed the Invoice Details table\nINSERT INTO dbo.Invoice_Details (InvoiceNo, ItemNo, Quantity, Price)\nSELECT 'IN20141000001', '1000055', 12, 233.22\nUNION ALL SELECT 'IN20141000001', '1000056', 3, 189.33\nUNION ALL SELECT 'IN20141000002', '1000055', 1, 222.33\nUNION ALL SELECT 'IN20141000002', '1000056', 4, 123.33;\n\nSELECT *\nFROM dbo.Invoices;\n\nSELECT *\nFROM dbo.Invoice_Details;\n\n--GO\n\n--DROP TABLE dbo.Invoice_Details;\n--DROP TABLE dbo.Invoices;\n<\/pre>\n<p> \tIn this sample, we have a hierarchical structure of two tables with a parent-child relationship between them, implemented using the FOREIGN KEY constraint. This is a simplification; in the real world there could be many tables originating from that single parent. In the particular case where I used this, there were a total of nine tables hanging off that same parent and the hierarchy was four levels deep. <\/p>\n<p> \tThe DROPs are provided so you can clean up your sandbox later. <\/p>\n<h2>Deleting the Data with Archiving<\/h2>\n<p> \tThe only reason you&#8217;d ever need to archive deleted data is on the off-chance that someone deletes a record that shouldn&#8217;t have been deleted, and then they want it to be restored. There are alternative, traditional ways to do this: <\/p>\n<ul>\n<li>Create &#8220;shadow&#8221; archiving tables into which you simply move the deleted transactions. This can become rather cumbersome as the number of tables participating in the relationship grows large, cluttering up your data model.<\/li>\n<li>Introduce flag column(s), for example the user that did the delete and a date\/time stamp, in each table to represent a &#8220;soft-delete.&#8221; In my experience, this can be rather a nuisance because it requires programming all over the place to ensure that deleted transactions are excluded from query results. You could be a bit more clever and use indexed VIEWs to achieve excluding the deleted rows, but you may still run the risk of later developers not realizing why these VIEWs need to be used.<\/li>\n<li>Conceivably you could dump the deleted transactions as some sort of text-based file (CSV or XML) or even into an Excel spreadsheet residing somewhere on your database server, but that sounds like an awful lot of complexity to me.<\/li>\n<\/ul>\n<p> \tLet&#8217;s now look at how we can use XML to keep our deleted data around for future restoration or inspection. <\/p>\n<h2>Use One Table to Store the Entire Parent to Great-great-great-grandchild Relationships<\/h2>\n<p> \tHere is some more DDL to define a table we&#8217;ll use to archive deleted transactions. <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">CREATE TABLE dbo.Deleted_Invoices\n(\n    -- The PRIMARY KEY of the transaction in the parent table that was deleted\n    InvoiceNo           VARCHAR(20)     NOT NULL\n    -- Who deleted the transaction and when\n    ,UserID             VARCHAR(20)     NOT NULL\n    ,DeletedDT          DATETIME        NOT NULL\n    -- One column for each table in the hierarchy\n    ,Invoices           XML             NULL\n    ,Invoice_Details    XML             NULL\n    ,PRIMARY KEY (InvoiceNo, UserID, DeletedDT)\n);\nGO\n-- DROP TABLE dbo.Deleted_Invoices;\n<\/pre>\n<p> \tAs you can see there is one XML column for each of the tables participating in our transaction&#8217;s hierarchy. There are also a couple of leading columns to capture the: <\/p>\n<ul>\n<li>PRIMARY KEY of the deleted transaction (InvoiceNo).<\/li>\n<li>User ID and date\/time that the transaction was deleted.<\/li>\n<\/ul>\n<h2>To Delete\/Archive a Transaction<\/h2>\n<p> \tWithin a transaction, we can first copy off the invoice and details to our archiving table, then do a cascade delete of the invoice from our tables. The code below is best put into a Stored Procedure rather than constructed within your application&#8217;s front end. <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">CREATE PROCEDURE dbo.Delete_InvoiceParent\n(\n    @UserID             VARCHAR(20)     \n    ,@InvoiceNo         VARCHAR(20)     \n)\nAS BEGIN;\n    SET NOCOUNT ON;\n    SET XACT_ABORT ON;\n    DECLARE @Error_Code        INT           = 0\n\n    BEGIN TRANSACTION T1;\n\n    BEGIN TRY;\n        -- Archive the invoices we'll delete\n        INSERT INTO dbo.Deleted_Invoices\n        (\n            InvoiceNo, UserID, DeletedDT, Invoices, Invoice_Details\n        )\n        SELECT @InvoiceNo, @UserID, GETDATE()\n            ,(\n                -- The parent transaction record\n                SELECT InvoiceNo, InvoiceDate, CustomerID, CustomerName\n                    ,CustomerAddr1, CustomerAddr2, CustomerZipcode\n                FROM dbo.Invoices\n                WHERE InvoiceNo = @InvoiceNo\n                FOR XML PATH('Invoices')\n            )\n            ,(\n                -- The child transaction records (details)\n                SELECT InvoiceNo, LineItem, ItemNo, Quantity, Price\n                FROM dbo.Invoice_Details\n                WHERE InvoiceNo = @InvoiceNo\n                FOR XML PATH('Invoice_Details')\n            );\n\n        -- Cascade delete details first\n        DELETE FROM dbo.Invoice_Details\n        WHERE InvoiceNo = @InvoiceNo;\n\n        DELETE FROM dbo.Invoices\n        WHERE InvoiceNo = @InvoiceNo;\n\n    END TRY\n\n    BEGIN CATCH;\n\n        SELECT @Error_Code = ERROR_NUMBER();\n\n         -- Useful information for debugging            \n         PRINT 'ERROR_NUMBER() '+   CAST(@Error_Code        AS VARCHAR(10));\n         PRINT 'ERROR_SEVERITY() '+ CAST(ERROR_SEVERITY()   AS VARCHAR(10));\n         PRINT 'ERROR_STATE() '+    CAST(ERROR_STATE()      AS VARCHAR(10));\n         PRINT 'ERROR_PROCEDURE() '+CAST(ERROR_PROCEDURE()  AS VARCHAR(8000));\n         PRINT 'ERROR_LINE() '+     CAST(ERROR_LINE()       AS VARCHAR(100));\n         PRINT 'ERROR_MESSAGE() '+  CAST(ERROR_MESSAGE()    AS VARCHAR(8000));\n         PRINT 'XACT_STATE() '+     CAST(XACT_STATE()       AS VARCHAR(5));\n\n    END CATCH;\n\n    IF @Error_Code &lt;&gt; 0 OR XACT_STATE() = -1 ROLLBACK TRANSACTION T1;\n    ELSE IF @Error_Code = 0 AND XACT_STATE() = 1 COMMIT TRANSACTION T1;\n\nEND\n<\/pre>\n<p> You can now run the following to archive\/cascade delete the records. <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">-- A cascade delete that works\nEXEC dbo.Delete_InvoiceParent\n    @UserID             = 'DCAMPS'\n    ,@InvoiceNo         = 'IN20141000002';\n\n-- This will cause an error when the SP is run\n-- and is why we set XACT_ABORT in the SP (to\n-- clean-up the transaction)    \nALTER TABLE dbo.Invoices\nDROP COLUMN CustomerZipcode;\nGO\n\n-- A cascade delete that fails\nEXEC dbo.Delete_InvoiceParent\n    @UserID             = 'DCAMPS'\n    ,@InvoiceNo         = 'IN20141000001';\n\n<\/pre>\n<p> \tWe did not include the computed column (<strong>ExtendedPrice<\/strong> of our <strong>Invoice_Details <\/strong>table) into our XML tables because it will not be needed when we restore this archived transaction. You&#8217;ll need to sequence the DELETEs so that the tables that are the deepest in the hierarchy are deleted first. <\/p>\n<p> \tNote that you should determine if there are any DML TRIGGERs that run on DELETE and examine what they do, then decide whether to ENABLE\/DISABLE them prior to running the script above. For example, the Invoice_Details might have an audit trail being created when invoice details are inserted, updated or deleted. <\/p>\n<p> \tAt this time, you&#8217;ll want to rerun the initial code that sets up the Invoices and Invoices_Details tables (drop them first) to repopulate the tables, then rerun the first SP EXEC statement above. <\/p>\n<h2>Using a Trigger<\/h2>\n<p> \tThis same thing can be done with an INSTEAD OF TRIGGER, although the only thing you cannot do is record the user that performed the DELETE. Here is that TRIGGER code: <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">CREATE TRIGGER dbo.Invoice_Delete\n   ON  dbo.Invoices \n   INSTEAD OF DELETE\nAS \nBEGIN\n\tSET NOCOUNT ON;\n\tSET XACT_ABORT ON;\n\n    -- Insert statements for trigger here\n    BEGIN TRANSACTION T1;\n\n    BEGIN TRY;\n        -- Archive the invoices we'll delete\n        INSERT INTO dbo.Deleted_Invoices\n        (\n            InvoiceNo, UserID, DeletedDT, Invoices, Invoice_Details\n        )\n        SELECT InvoiceNo, '', GETDATE()\n            ,(\n                -- The parent transaction record\n                SELECT InvoiceNo, InvoiceDate, CustomerID, CustomerName\n                    ,CustomerAddr1, CustomerAddr2, CustomerZipcode\n                FROM dbo.Invoices b\n                WHERE a.InvoiceNo = b.InvoiceNo\n                FOR XML PATH('Invoices')\n            )\n            ,(\n                -- The child transaction records (details)\n                SELECT InvoiceNo, LineItem, ItemNo, Quantity, Price\n                FROM dbo.Invoice_Details b\n                WHERE a.InvoiceNo = b.InvoiceNo\n                FOR XML PATH('Invoice_Details')\n            )\n        FROM DELETED a;\n\n        -- Cascade delete details first\n        DELETE a\n        FROM dbo.Invoice_Details a\n        JOIN DELETED b\n        ON a.InvoiceNo = b.InvoiceNo;\n\n        DELETE a \n        FROM dbo.Invoices a\n        JOIN DELETED b\n        ON a.InvoiceNo = b.InvoiceNo;\n\n    END TRY\n\n    BEGIN CATCH;\n         -- Useful information for debugging            \n         PRINT 'ERROR_NUMBER() '+   CAST(ERROR_NUMBER()     AS VARCHAR(10));\n         PRINT 'ERROR_SEVERITY() '+ CAST(ERROR_SEVERITY()   AS VARCHAR(10));\n         PRINT 'ERROR_STATE() '+    CAST(ERROR_STATE()      AS VARCHAR(10));\n         PRINT 'ERROR_PROCEDURE() '+CAST(ERROR_PROCEDURE()  AS VARCHAR(8000));\n         PRINT 'ERROR_LINE() '+     CAST(ERROR_LINE()       AS VARCHAR(100));\n         PRINT 'ERROR_MESSAGE() '+  CAST(ERROR_MESSAGE()    AS VARCHAR(8000));\n         PRINT 'XACT_STATE() '+     CAST(XACT_STATE()       AS VARCHAR(5));\n\n    END CATCH;\n\n    IF XACT_STATE() = -1 ROLLBACK TRANSACTION T1;\n    ELSE IF XACT_STATE() = 1 COMMIT TRANSACTION T1;\n\nEND\n<\/pre>\n<p> \tNote how we&#8217;ve set the UserID column in the Deleted_Invoices table to blank because we don&#8217;t have access to that information in the TRIGGER (although you could substitute it with the SQL Server login account if that is applicable). <\/p>\n<p> \tAssuming you&#8217;ve restored the original data to the two invoices tables, you can run the following code to delete and archive invoice IN20141000002 the same as before. <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">DELETE FROM dbo.Invoices\nWHERE InvoiceNo = 'IN20141000002';\n\nSELECT *\nFROM dbo.Invoices;\n\nSELECT *\nFROM dbo.Invoice_Details;\n\nSELECT *\nFROM dbo.Deleted_Invoices;\n<\/pre>\n<h2>The XML that our Archiving Process Created<\/h2>\n<p> \tThe XML that was created in the prior step is element-based. We could have just as well chosen attribute-based XML. Since element-based XML tends to be a little more verbose it will take up marginally more storage. <\/p>\n<pre class=\"lang:xhtml theme:github\">&lt;Invoices&gt;\n  &lt;InvoiceNo&gt;IN20141000002&lt;\/InvoiceNo&gt;\n  &lt;InvoiceDate&gt;2014-11-04&lt;\/InvoiceDate&gt;\n  &lt;CustomerID&gt;20002&lt;\/CustomerID&gt;\n  &lt;CustomerName&gt;ACE CHEMICALS&lt;\/CustomerName&gt;\n  &lt;CustomerAddr1&gt;201 BROADWAY AVE&lt;\/CustomerAddr1&gt;\n  &lt;CustomerAddr2&gt;CAIRNS, AU&lt;\/CustomerAddr2&gt;\n  &lt;CustomerZipcode&gt;4810&lt;\/CustomerZipcode&gt;\n&lt;\/Invoices&gt; \n\n&lt;Invoice_Details&gt;\n  &lt;InvoiceNo&gt;IN20141000002&lt;\/InvoiceNo&gt;\n  &lt;LineItem&gt;3&lt;\/LineItem&gt;\n  &lt;ItemNo&gt;1000055&lt;\/ItemNo&gt;\n  &lt;Quantity&gt;1&lt;\/Quantity&gt;\n  &lt;Price&gt;222.3300&lt;\/Price&gt;\n&lt;\/Invoice_Details&gt;\n\n&lt;Invoice_Details&gt;\n  &lt;InvoiceNo&gt;IN20141000002&lt;\/InvoiceNo&gt;\n  &lt;LineItem&gt;4&lt;\/LineItem&gt;\n  &lt;ItemNo&gt;1000056&lt;\/ItemNo&gt;\n  &lt;Quantity&gt;4&lt;\/Quantity&gt;\n  &lt;Price&gt;123.3300&lt;\/Price&gt;\n&lt;\/Invoice_Details&gt; \n<\/pre>\n<h2>Restore a Transaction<\/h2>\n<p> \tWhen you need to restore a transaction, you can parse the XML back into the original tables. Once again we&#8217;ll put that code into a SP. <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">CREATE PROCEDURE dbo.Restore_Invoice\n(\n    @UserID             VARCHAR(20)     \n    ,@InvoiceNo         VARCHAR(20)     \n)\nAS BEGIN;\n    SET NOCOUNT ON;\n    SET XACT_ABORT ON;\n\n    DECLARE @Error_Code     INT             = 0;\n\n    BEGIN TRANSACTION T1;\n        \n    BEGIN TRY;\n\n        -- Restore the invoice header first\n        INSERT INTO dbo.Invoices\n        (\n            InvoiceNo, InvoiceDate, CustomerID, CustomerName\n            ,CustomerAddr1, CustomerAddr2, CustomerZipcode\n        )\n        SELECT InvoiceNo        = b.Invoices.value('InvoiceNo[1]',        'VARCHAR(20)')\n            ,InvoiceDate        = b.Invoices.value('InvoiceDate[1]',      'DATE')\n            ,CustomerID         = b.Invoices.value('CustomerID[1]',       'VARCHAR(20)')\n            ,CustomerName       = b.Invoices.value('CustomerName[1]',     'VARCHAR(100)')  \n            ,CustomerAddr1      = b.Invoices.value('CustomerAddr1[1]',    'VARCHAR(100)')  \n            ,CustomerAddr2      = b.Invoices.value('CustomerAddr2[1]',    'VARCHAR(100)')       \n            ,CustomerZipcode    = b.Invoices.value('CustomerZipcode[1]',  'VARCHAR(10)')        \n        FROM dbo.Deleted_Invoices a\n        CROSS APPLY Invoices.nodes('\/Invoices') b (Invoices)\n        WHERE a.InvoiceNo = @InvoiceNo;\n\n        SET IDENTITY_INSERT dbo.Invoice_Details ON;\n\n        -- Restore the invoice details (child table) next\n        INSERT INTO dbo.Invoice_Details\n        (\n            InvoiceNo, LineItem, ItemNo, Quantity, Price\n        )\n        SELECT InvoiceNo        = b.Invoice_Details.value('InvoiceNo[1]',   'VARCHAR(20)')\n            ,LineItem           = b.Invoice_Details.value('LineItem[1]',    'BIGINT')\n            ,ItemNo             = b.Invoice_Details.value('ItemNo[1]',      'VARCHAR(20)')\n            ,Quantity           = b.Invoice_Details.value('Quantity[1]',    'INT')  \n            ,Price              = b.Invoice_Details.value('Price[1]',       'MONEY')  \n        FROM dbo.Deleted_Invoices a\n        CROSS APPLY Invoice_Details.nodes('\/Invoice_Details') b (Invoice_Details)\n        WHERE a.InvoiceNo = @InvoiceNo;\n\n        -- Remove the deleted transaction from the archives table\n        DELETE FROM dbo.Deleted_Invoices\n        WHERE InvoiceNo = @InvoiceNo;\n\n        SET IDENTITY_INSERT dbo.Invoice_Details OFF;\n\n    END TRY\n\n    BEGIN CATCH;\n\n        SELECT @Error_Code = ERROR_NUMBER();\n\n        PRINT @InvoiceNo;\n\n         -- Useful information for debugging            \n         PRINT 'ERROR_NUMBER() '+   CAST(@Error_Code        AS VARCHAR(10));\n         PRINT 'ERROR_SEVERITY() '+ CAST(ERROR_SEVERITY()   AS VARCHAR(10));\n         PRINT 'ERROR_STATE() '+    CAST(ERROR_STATE()      AS VARCHAR(10));\n         PRINT 'ERROR_PROCEDURE() '+CAST(ERROR_PROCEDURE()  AS VARCHAR(8000));\n         PRINT 'ERROR_LINE() '+     CAST(ERROR_LINE()       AS VARCHAR(100));\n         PRINT 'ERROR_MESSAGE() '+  CAST(ERROR_MESSAGE()    AS VARCHAR(8000));\n         PRINT 'XACT_STATE() '+     CAST(XACT_STATE()       AS VARCHAR(5));\n\n    END CATCH;\n\n    IF @Error_Code &lt;&gt; 0 OR XACT_STATE() = -1 ROLLBACK TRANSACTION T1;\n    ELSE IF @Error_Code = 0 AND XACT_STATE() = 1 COMMIT TRANSACTION T1;\n\nEND\n<\/pre>\n<p> \tWe can restore our archived transaction with the following: <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">EXEC dbo.Restore_Invoice\n    @UserID             = 'DCAMPS'     \n    ,@InvoiceNo         = 'IN20141000002';\n<\/pre>\n<p> \tNote that because the LineItem is an IDENTITY column, we must use the IDENTITY_INSERT setting to allow us to restore the value corresponding to the invoice detail rows that were originally deleted. You&#8217;ll find that you must run the INSERTs in exactly the reverse order for the cascade delete in the prior section. We also deleted the archived transaction, because you&#8217;d never want to attempt the restore a second time because you&#8217;d get PRIMARY KEY violations on the INSERTs as you&#8217;d be attempting to re-insert the same invoice number. <\/p>\n<p> \tYou will also need to be careful to DISABLE\/ENABLE any TRIGGER on these tables that runs on INSERT. <\/p>\n<h2>The Drawback and How to Mitigate that Effect<\/h2>\n<p> \tLet&#8217;s think of our table hierarchy that has been archived as an &#8220;archiving object.&#8221; There may potentially be many of these objects you need to keep track of in your database. There are a number of operations that can affect your code that creates\/restores a transaction from one of these objects: <\/p>\n<ul>\n<li>Adding a column to any table that is a part of the hierarchy tree.<\/li>\n<li>Dropping a column from any table that is a part of the hierarchy tree.<\/li>\n<li>Adding a new child table anywhere within the hierarchy tree.<\/li>\n<li>Dropping a table that is located somewhere (presumably one of the lowest level children) in the hierarchy tree.<\/li>\n<\/ul>\n<p> \tIf any of those actions occur (particularly in either of the two cases where something is being added), your code could be &#8220;silently&#8221; impacted, which is to say you may go on archiving transactions that are now incomplete without your code generating any error messages. That is not the case for all of the examples above (some will generate errors during the archiving process). <\/p>\n<p> \tTo guard against issues being caused by most of these, we can create a DDL trigger that generates a warning message on some specific event types. Assuming of course that you&#8217;ve properly defined your table hierarchy using FOREIGN KEY constraints to tie the lot together. <\/p>\n<p> \tLet&#8217;s look at an interesting bit of SQL. <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">DECLARE @parent_object_id   BIGINT = OBJECT_ID('Invoices', 'U')\n    ,@start_object_id       BIGINT = OBJECT_ID('Invoice_Details', 'U');\n\nWITH TraverseTableHierarchy AS\n(\n    -- Using the starting object's ID, identify all of its foreign keys\n    SELECT a.name, a.object_id, a.parent_object_id, a.referenced_object_id\n    FROM sys.foreign_keys a\n    JOIN sys.all_objects b ON a.parent_object_id = b.object_id\n    WHERE a.parent_object_id = @start_object_id\n    \n    UNION ALL\n    \n    -- Traverse the hiearchy through the foreign keys and identify the parents\n    SELECT b.name, b.object_id, b.parent_object_id, b.referenced_object_id\n    FROM TraverseTableHierarchy a\n    JOIN sys.foreign_keys b ON b.parent_object_id = a.referenced_object_id\n    JOIN sys.all_objects c ON b.parent_object_id = c.object_id\n)\nSELECT a.name, a.object_id, a.parent_object_id, a.referenced_object_id, b.name\nFROM TraverseTableHierarchy a\nJOIN sys.all_objects b ON a.referenced_object_id = b.object_id\n-- Show a result that only includes the parent object for the hierarchy\nWHERE a.referenced_object_id = @parent_object_id;\n<\/pre>\n<p> \tWhen we run this SQL, we get something like the following where the object IDs will be different in your database. <\/p>\n<pre class=\"listing\">name      object_id   parent_object_id  referenced_object_id   name\ninvd_fk1  1512392457  1448392229        1384392001             Invoices\n<\/pre>\n<p> \tThis shows us that Invoices (in the rightmost &#8220;name&#8221; column) is affected by anything modified within the @start_object_id (Invoice_Details). The TraverseTableHierarchy <a href=\"http:\/\/technet.microsoft.com\/en-us\/library\/ms186243(v=sql.105).aspx\">Recursive Common Table Expression<\/a> (rCTE) actually walks through the table hierarchy (upwards) using FOREIGN KEYs until it reaches the topmost table in your data model. It returns only rows matching the @parent_object_id. You may wish to add an OPTION(MAXRECURSION 0) if for some reason your hierarchy could be more than 100 levels in depth. You may need to modify this code if you have tables with self-referencing FOREIGN KEYS. <\/p>\n<p> \tWe can then construct a DDL trigger that looks for Archived Object structures within our database and displays a warning for the various cases. <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">CREATE TRIGGER Check_Archiving\nON DATABASE\nFOR CREATE_TABLE, ALTER_TABLE\nAS\n    SET NOCOUNT ON;\n\n    DECLARE @ArchivedObjects TABLE\n    (\n        Archived_Object_Name        VARCHAR(100) NOT NULL\n        ,Archived_Object_ID         BIGINT NOT NULL\n    );\n\n    DECLARE @DependentObjects TABLE\n    (\n        Name                        VARCHAR(100)\n        ,ID                         BIGINT\n        ,Parent_ID                  BIGINT\n        ,Referenced_ID              BIGINT\n        ,Name2                      VARCHAR(100)\n    );\n\n    -- Include here all of your archiving objects (parent object of each)\n    INSERT INTO @ArchivedObjects (Archived_Object_Name, Archived_Object_ID)\n    SELECT 'Invoices', OBJECT_ID('Invoices', 'U');\n\n    WITH TraverseTableHierarchy AS\n    (\n        -- Using the starting object's ID, identify all of its foreign keys\n        SELECT a.name, a.object_id, a.parent_object_id, a.referenced_object_id\n        FROM sys.foreign_keys a\n        JOIN sys.all_objects b ON a.parent_object_id = b.object_id\n        WHERE a.parent_object_id = \n            OBJECT_ID(\n                EVENTDATA().value('(\/EVENT_INSTANCE\/ObjectName)[1]','NVARCHAR(MAX)')\n                , 'U')\n        \n        UNION ALL\n        \n        -- Traverse the hiearchy through the foreign keys and identify the parents\n        SELECT b.name, b.object_id, b.parent_object_id, b.referenced_object_id\n        FROM TraverseTableHierarchy a\n        JOIN sys.foreign_keys b ON b.parent_object_id = a.referenced_object_id\n        JOIN sys.all_objects c ON b.parent_object_id = c.object_id\n    )\n    INSERT INTO @DependentObjects\n    SELECT a.name, a.object_id, a.parent_object_id, a.referenced_object_id, b.name\n    FROM TraverseTableHierarchy a\n    JOIN sys.all_objects b ON a.referenced_object_id = b.object_id\n    -- The results include only archived objects for each hierarchy\n    WHERE a.referenced_object_id = (SELECT Archived_Object_ID FROM @ArchivedObjects);\n    \n    DECLARE @ArchivedObject     VARCHAR(100) =\n        (\n            SELECT TOP 1 Name2\n            FROM @DependentObjects\n        );\n    \n    IF EXISTS\n    (\n        SELECT 1\n        FROM @DependentObjects\n    )\n    PRINT 'Archived Object Warning - DDL operation impacted archived object: ' + @ArchivedObject;\nGO\n<\/pre>\n<p> \tAt the heart of the modifications made to the rCTE is the use of <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/ms173781.aspx\">EVENTDATA()<\/a> to query the <a href=\"http:\/\/schemas.microsoft.com\/sqlserver\/2006\/11\/eventdata\/\">SQL Server Event Data Schema<\/a> and return the name of the object that was modified. The <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/ms190328.aspx\">OBJECT_ID()<\/a> function can then be used to return the ID of the object and use that as the starting point for the upward table hierarchy traversal. We&#8217;ve put the top-level object IDs into the @ArchivedObjects table variable to allow for cases where you may have several archiving objects you want to keep track of. <\/p>\n<p> \tThis DATABASE TRIGGER can be located in the Object Explorer window in SQL Server Management Studio (SSMS) by expanding the database &gt; Programmability &gt; Database Triggers. <\/p>\n<p> \tYou can test the DDL trigger with this DDL. <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">-- Generates a warning\nALTER TABLE dbo.Invoice_Details\nADD xxx INT NULL;\nGO\n\n-- Generates a warning\nALTER TABLE dbo.Invoice_Details\nDROP COLUMN xxx;\nGO\n\n-- Generates a warning\nCREATE TABLE dbo.Invoice_Details_Stuff\n(\n    InvoiceNo           VARCHAR(20)     NOT NULL\n    ,LineItem           BIGINT IDENTITY\n    ,[Stuff]            INT\n    ,CONSTRAINT xxxx1   FOREIGN KEY (InvoiceNo, LineItem) REFERENCES dbo.Invoice_Details(InvoiceNo, LineItem)\n);\nGO\n\n-- Does not generate a warning\nDROP TABLE dbo.Invoice_Details_Stuff;\nGO\n\n-- Does not generate a warning\nCREATE TABLE dbo.Invoice_Details_Stuff\n(\n    InvoiceNo           VARCHAR(20)     NOT NULL\n    ,LineItem           BIGINT IDENTITY\n    ,[Stuff]            INT\n);\nGO\n\n-- Does not generate a warning\nDROP TABLE dbo.Invoice_Details_Stuff;\nGO\n<\/pre>\n<p> \tComments indicate where a warning is generated. Note that dropping a TABLE from the hierarchy does not generate a warning, and will still not do so if you changed the definition of the DDL trigger to this: <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">ALTER TRIGGER Check_Archiving\nON DATABASE\nFOR CREATE_TABLE, ALTER_TABLE, DROP_TABLE\n<\/pre>\n<p> \tPresumably this is because the FOREIGN KEY relationship has been deleted before the trigger is run. But in that case, your delete\/archiving code will probably fail anyway. <\/p>\n<p> \tThe second to last case doesn&#8217;t generate a warning until a FOREIGN KEY relationship is established between <strong>Invoice_Details_Stuff<\/strong> and <strong>Invoice_Details<\/strong> (or any other TABLE within the hierarchy). <\/p>\n<p> \tTo DROP this TRIGGER from your sandbox: <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">\tDROP TRIGGER Check_Archiving ON DATABASE;\n<\/pre>\n<h2>Conclusion<\/h2>\n<p> \tIn this article we explored an alternative method of archiving deleted business transactions. Which method you use probably depends on you and your preferences. Any method you choose will have some potential drawbacks. <\/p>\n<p> \tFor our method, we have at least provided a path to mitigating the drawback. As long as you remember to take action on the warning that will be generated by DDL changes to your archived table hierarchies, you&#8217;ll probably be fine. <\/p>\n<p> \tWe&#8217;d love to hear stories about any other methods readers have used to archive deleted transactions. <\/p>\n<\/p><\/div>\n","protected":false},"excerpt":{"rendered":"<p>When you delete a business transaction from the database, there are times when you might want to keep a record of the data for posterity.  This can become somewhat complicated if the transaction you need to delete is in a table that is the parent of a deeply nested hierarchy of dependent tables based on the foreign key relationships.  In this article, Dwain Camps looks at a tidy means of doing just that.&hellip;<\/p>\n","protected":false},"author":221942,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[143531],"tags":[4168,5986,5987,5985,4150,4252,4217],"coauthors":[],"class_list":["post-1922","post","type-post","status-publish","format-standard","hentry","category-t-sql-programming-sql-server","tag-database","tag-ddl","tag-dml","tag-hierarchies","tag-sql","tag-t-sql-programming","tag-xml"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/1922","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/221942"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=1922"}],"version-history":[{"count":2,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/1922\/revisions"}],"predecessor-version":[{"id":39501,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/1922\/revisions\/39501"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=1922"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=1922"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=1922"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=1922"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}