将MERGE语句中的更新列作为不同表中的行进行审计。

问题描述 投票:1回答:1

我正在努力实现以下目标

  • MERGE一个TARGET和SOURCE表。
  • 使用输出子句来审核变化。

我能够使用CASE语句进行以下操作。

  • 轻松审计插入和删除。

我得到的插入和删除的格式如下。

| Action   | Record | ChangedFrom | ChangedTo |  
|----------|--------|-------------|-----------|  
| Inserted | 1      | NULL        | 1         |  
| Deleted  | 2      | 2           | NULL      |  

我无法得到的是:

| Action  | Record | ChangedFrom        | ChangedTo         |  
|---------|--------|--------------------|-------------------|  
| UPDATED | 1      | ColA_Content_Before| ColA_Content_After| 

合并语句将更改后的值输出为每行的列。我需要将这些列对改成行,这样记录行的每一个改变的列就会成为审计表中的一个条目。在这里为了举例的缘故,我使用了3列,在实践中,我需要在我不知道列数是多少的情况下工作。

目标表

| Id | ColA | ColB   | ColC   |
|----|------|--------|--------|
| 1  | Orig | Orig_B | Orig_C |

来源表

| Id | ColA       | ColB       | ColC   |
|----|------------|------------|--------|
| 1  | Modified_A | Modified_B | Orig_C |

合并结果

| Id | ColA | ColB   | ColC   | Id | ColA      | ColB       | ColC |
|---:|------|--------|--------|----|-----------|------------|------|
| 1  | Orig | Orig_B | Orig_C | 1  | Modifed_A | Modified_C |      |

我需要这些列作为行

|  Action | Record | ChangedFrom | ChangedTo  |
|--------:|--------|-------------|------------|
| Updated | 1      | Orig        | Modified_A |
| Updated | 1      | Orig_B      | MOdified_B |

我试过的方法。

  • 在proc中使用merge来执行批处理语句
  • 这里我尝试使用游标迭代行,然后在case语句里面插入cols。
  • 使用动态sql和pivot表。

但是没有成功。

有什么好的建议吗?

复制的代码

CREATE TABLE [dbo].[Test_Source]
(
   Id int NOT NULL,
   ColA VARCHAR(50) NOT NULL,
   ColB VARCHAR(50) NOT NULL,
   ColC VARCHAR(50) NOT NULL,
   ColD VARCHAR(50) NOT NULL,
   [UDate] [DATETIME] NOT NULL
) ON [PRIMARY];
GO
CREATE TABLE [dbo].[Test_Target]
(
   Id int NOT NULL,
   ColA VARCHAR(50) NOT NULL,
   ColB VARCHAR(50) NOT NULL,
   ColC VARCHAR(50) NOT NULL,
   ColD VARCHAR(50) NOT NULL,
   [UDate] [DATETIME] NOT NULL
) ON [PRIMARY];
GO


---Insert some test values
INSERT INTO [dbo].[Test_Source]
(
   Id,ColA,ColB,ColC,ColD,UDate
)
VALUES
(
  1,'ColA','ColB','ColC','ColD',GETDATE()
);

INSERT INTO [dbo].[Test_Target]
(
   Id,ColA,ColB,ColC,ColD,UDate
)
VALUES
(
  1,'ColA_After','ColB_After','ColC_After','ColD_After',GETDATE()
);

CREATE PROCEDURE MERGE_TEST 
As
BEGIN
        DECLARE @MERGERESULTS TABLE 
        (
         [Action] nvarchar(50),
         Id_After int ,
         ColA_After nvarchar(50),
         ColB_After nvarchar(50),
         ColC_After nvarchar(50),
         ColD_After nvarchar(50),
         UDate_After nvarchar(50),
         Id_Before int,
         ColA_Before nvarchar(50),
         ColB_Before nvarchar(50),
         ColC_Before nvarchar(50),
         ColD_Before nvarchar(50),
         UDate_Before nvarchar(50)
        );

        DECLARE @AUDITRESULTS TABLE 
        (
         [Action] nvarchar(50),
         Id_After int ,
         ChangedFrom nvarchar(50),
         ChangedTo nvarchar(50)      
         );

        DECLARE
         @cols AS NVARCHAR(MAX),
         @query  AS NVARCHAR(MAX)

        MERGE Test_Target as T
        USING Test_Source as S
        ON T.Id = S.Id
        WHEN MATCHED AND
        T.ColA <> S.ColA OR
        T.ColB <> S.ColB OR
        T.ColC <> S.ColC OR
        T.ColD <> S.ColD OR
        T.UDate <> S.UDate 
        THEN UPDATE SET
        T.ColA = S.ColA ,
        T.ColB = S.ColB ,
        T.ColC = S.ColC ,
        T.ColD = S.ColD ,
        T.UDate = S.UDate 
        WHEN NOT MATCHED BY TARGET
        THEN INSERT 
        (Id,ColA,ColB,ColC,ColD,UDate)
         VALUES
        (S.Id,S.ColA,S.ColB,S.ColC,S.ColD,S.UDate)
         WHEN NOT MATCHED BY SOURCE 
        THEN DELETE 
        OUTPUT $action,Inserted.*,Deleted.* into @MERGERESULTS ;

        select * from @MERGERESULTS;

        select * into #mytemp from @MERGERESULTS

        -- Dynamic sql to pivot table
            select @cols = STUFF((SELECT distinct ',' + QUOTENAME(ColA_After) 
                        from @MERGERESULTS
                FOR XML PATH(''), TYPE
                ).value('.', 'NVARCHAR(MAX)') 
                 ,1,1,'')

            set @query = 'Select * from #mytemp
                    pivot 
                    (
                        max(Id_After)
                        for ColA_After in (' + @cols + ')
                    ) p '

            select  @Cols as 'Columns';

            execute(@query)

     -- Dynamic sql to pivot Table

-- Trying cursor and row count (incomplete)
            --declare @Id int

            --set rowcount 0
            --select 
            --t.Action ,
            --CASE WHEN (t.ColA_After <> t.ColA_Before)
            --  THEN t.ColA_After  
            --END,
            --CASE WHEN (t.ColA_After <> t.ColA_Before)
            --  THEN t.ColA_Before  
            --END,

            --CASE WHEN (t.ColB_After <> t.ColB_Before)
            --  THEN t.ColB_After  
            --END,
            --CASE WHEN (t.ColB_After <> t.ColB_Before)
            --  THEN t.ColB_Before  
            --END,

            --CASE WHEN (t.ColC_After <> t.ColC_Before)
            --  THEN t.ColC_After  
            --END,
            --CASE WHEN (t.ColC_After <> t.ColC_Before)
            --  THEN t.ColC_Before 
            --END,

            --CASE WHEN (t.ColD_After <> t.ColD_Before)
            --  THEN t.ColD_After  
            --END,
            --CASE WHEN (t.ColD_After <> t.ColD_Before)
            --  THEN t.ColD_Before  
            --END
            --from #mytemp t
            --inner join #mytemp t1
            --on t.Id_After = t1.Id_Before
            --where 
            --t.ColA_After <> t1.ColA_Before OR
            --t.ColB_After <> t1.ColB_Before OR
            --t.ColC_After <> t1.ColC_Before OR
            --t.ColD_After <> t1.ColD_Before 
            --set rowcount 1

            --select @Id = Id_After from #mytemp

            --while @@rowcount <> 0
            --begin
            --  set rowcount 0
            --  (select * from #mytemp where Id_After = @Id)

            --  delete #mytemp where au_id = @au_id

            --  set rowcount 1
            --  select @au_id = au_id from #mytemp
            --end
            --set rowcount 0

END

EXEC MERGE_TEST

这好像是在走偏门了

.net sql-server tsql merge audit
1个回答
0
投票

我已经通过使用unpivot实现了上述目标。

不知道这是不是最好的方法,但能完成工作。

我的解决方案是

  • 使用一个程序,选择MERGE结果进入TABLE变量,后缀为_Before和_After的列。
  • 使用where条件来审计插入和更新。
  • 使用UNPIVOT来审计更新。
  • 可以使用pivot col值来获取更新后的列名。
  • UNPIVOT需要所有的cols都是相同的数据类型,所以可以CASTCONVERT它们,我只是在选择进入时将所有的cols存储为nvarchars()。

CREATE AUDIT SCRIPT

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[Audit](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [ChangedFrom] [nvarchar](100) NULL,
    [ChangedTo] [nvarchar](100) NULL,
    [Action] [nvarchar](100) NULL,
    [ChangedDate] [datetime2](7) NULL,
    [Column] [nvarchar](100) NULL
 CONSTRAINT [PK_Audit] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO

CREATE PROCEDURE SCRIPT

CREATE PROCEDURE MERGE_AUDIT
As
BEGIN
        --Temporary TABLE variable to store the MERGE results
        DECLARE @MERGERESULTS TABLE 
        (
         [Action] nvarchar(50),
         Id_After nvarchar(50),
         ColA_After nvarchar(50),
         ColB_After nvarchar(50),
         ColC_After nvarchar(50),
         ColD_After nvarchar(50),
         UDate_After nvarchar(50),
         Id_Before nvarchar(50),
         ColA_Before nvarchar(50),
         ColB_Before nvarchar(50),
         ColC_Before nvarchar(50),
         ColD_Before nvarchar(50),
         UDate_Before nvarchar(50)
        );

        MERGE Test_Target as T
        USING Test_Source as S
        ON T.Id = S.Id
        WHEN MATCHED AND
        T.ColA <> S.ColA OR
        T.ColB <> S.ColB OR
        T.ColC <> S.ColC OR
        T.ColD <> S.ColD OR
        T.UDate <> S.UDate 
        THEN UPDATE SET
        T.ColA = S.ColA ,
        T.ColB = S.ColB ,
        T.ColC = S.ColC ,
        T.ColD = S.ColD ,
        T.UDate = S.UDate 
        WHEN NOT MATCHED BY TARGET
        THEN INSERT 
        (Id,ColA,ColB,ColC,ColD,UDate)
         VALUES
        (S.Id,S.ColA,S.ColB,S.ColC,S.ColD,S.UDate)
         WHEN NOT MATCHED BY SOURCE 
        THEN DELETE 
        OUTPUT $action,Inserted.*,Deleted.* into @MERGERESULTS ;

        -- Audit the Inserts
        insert into [Audit]
        (
            ChangedFrom,
            ChangedTo,
            Action,
            ChangedDate
        )
        select 
            Id_Before,
            Id_After,
            [Action],
            GETDATE()
        from @MERGERESULTS where [Action] = 'INSERT' ;

        --Audit the DELETES
        insert into [Audit]
        (
            ChangedFrom,
            ChangedTo,
            Action,
            ChangedDate
        )
        select 
            Id_Before,
            Id_After,
            [Action],
            GETDATE()
        from @MERGERESULTS where [Action] = 'DELETE' ;

        --unpivot to audit the UPDATES,
        --USING REPLACE to remove the AFTER suffix when getting the column name
        -- When storing the MERGE results in the TABLE variable having all columns as NVARCHAR is needed as to UNPIVOT the datataypes are needed to be same
        insert into [Audit]
        (
            ChangedFrom,
            ChangedTo,
            Action,
            ChangedDate,
            [Column]
        )
        select * from (select 
            ChangedFrom,
            ChangedTo,
            [Action],
            GETDATE() as ChangedDate,
            REPLACE(p1col, '_After', '')  as [Column]
            from 
            (select 
            *
            from @MERGERESULTS
            UNPIVOT
            (
                   ChangedFrom
                   FOR pcol IN (ColA_Before,ColB_Before,ColC_Before,ColD_Before)
            ) AS P
            UNPIVOT
            (
                ChangedTo 
                For p1col IN (ColA_After,ColB_After,ColC_After,ColD_After )
            ) p1) as a
            where 
            a.pcol = 'ColA_Before' and a.p1col = 'ColA_After' OR
            a.pcol = 'ColB_Before' and a.p1col = 'ColB_After' OR
            a.pcol = 'ColC_Before' and a.p1col = 'ColC_After' OR
            a.pcol = 'ColD_Before' and a.p1col = 'ColD_After') as o -- this where condition is needed to get only relevant results, else we will get all the cols as it is a cross join
        where o.ChangedFrom <> o.ChangedTo;
END

执行结果

EXEC MERGE_AUDIT

PROC RESULT

© www.soinside.com 2019 - 2024. All rights reserved.