79401860

Date: 2025-01-31 05:50:28
Score: 1.5
Natty:
Report link

I've been pondering this one myself for a few weeks on and off, having arrived at the following answer. This was specifically done to track changes within a 'logging table' for user defined tables (not every table) via stored procedures, excluding the use of external tooling requirements. I believe this is equivalent to your question. I am using TSQL for this task. Note for the below I specifically addressed INSERT actions but can be readily adjusted and expanded for UPDATE and DELETE for your own needs. I leave this exercise for the user.

I apologies in advance for the length of this response.

Given the assumption we are familiar with stored procedures we can immediately start at the guts of our work. I failed to break down my solution further than as shown, and the following and is presented with the intention of both combining each part for a complete answer (Part 1 - 5 can be copy-pasted), while explaining each part in detail for understanding.

Part 1:

BEGIN
DECLARE 
    @TABLE_NAME     NVARCHAR(MAX) = NULL,
    @PKEY           NVARCHAR(MAX) = NULL,
    @COLUMN_NAME    NVARCHAR(MAX) = NULL,
    @EventLog_I_TR  NVARCHAR(MAX) = NULL

DECLARE TableCursor CURSOR FOR
    SELECT TABLE_NAME FROM [dbo].[EVENT_LOG_TABLES]

OPEN TableCursor
FETCH NEXT FROM TableCursor INTO @TABLE_NAME

WHILE @@FETCH_STATUS = 0
BEGIN

In Part 1, we start with our DECLARE of required variables. To facilitate the selection of only "some" tables, I am storing the tables I want to track as string data within [EVENT_LOG_TABLES].[TABLE_NAME]. I have then used a cursor to iterate through these tables. I expect I will refine this in future to remove cursor reliance, however this has not occurred today.

Part 2:

        SET @PKEY = ''
    
    SELECT @PKEY = @PKEY + 'CAST(INSERTED.[' + COLUMN_NAME + '] AS NVARCHAR(MAX)) + '','' + '
    FROM INFORMATION_SCHEMA.COLUMNS 
    WHERE TABLE_NAME = @TABLE_NAME 
    AND COLUMN_NAME IN (
        SELECT KCU.COLUMN_NAME 
        FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE KCU 
        JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS TC ON KCU.CONSTRAINT_NAME = TC.CONSTRAINT_NAME
        WHERE KCU.TABLE_NAME = @TABLE_NAME
        AND TC.CONSTRAINT_TYPE = 'PRIMARY KEY'
    )
    
    SET @PKEY = LEFT(@PKEY, LEN(@PKEY) - 8)

In Part 2, I have collated the primary key values of the table into a string. As we are working with many tables, which will have many different primary keys, I have chosen to concatenate these values to provide a unique and searchable value within the logging table. If all tables have the same keys, it may be more efficient to break out this section into multiple return values instead of one. I leave this decision to the user.

Part 3:

        SET @EventLog_I_TR = ''

    DECLARE ColumnCursor CURSOR FOR
        SELECT      COLUMN_NAME 
        FROM        INFORMATION_SCHEMA.COLUMNS 
        WHERE       TABLE_NAME = @TABLE_NAME 
                    AND COLUMN_NAME NOT IN (SELECT  KCU.COLUMN_NAME 
                                            FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE KCU 
                                            JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS TC ON KCU.CONSTRAINT_NAME = TC.CONSTRAINT_NAME
                                            WHERE KCU.TABLE_NAME = @TABLE_NAME
                                            AND TC.CONSTRAINT_TYPE = 'PRIMARY KEY'
                                            )

    OPEN ColumnCursor
    FETCH NEXT FROM ColumnCursor INTO @COLUMN_NAME

    WHILE @@FETCH_STATUS = 0
    BEGIN    

I have set our trigger string (@EventLog_I_TR) to an empty string ('') so that we can concatenate this value later on (in Part 4). Meanwhile, I have also declared the cursor over the columns within the current table which are not primary keys.

Up to now, we can now assess the table (user defined), primary keys (distinct tracking), and columns (change tracking).

This leads us into Part 4:

            SET @EventLog_I_TR = @EventLog_I_TR + CHAR(13) +
            'INSERT INTO [dbo].[EVENT_LOG] ([TABLE], [PKEY], [COLUMN], [ACTION], [VAL]) ' +
            'SELECT ''' + @TABLE_NAME + ''',' + @PKEY + ',''' + @COLUMN_NAME + 
            ''',''I'',[' + @COLUMN_NAME + '] ' +
            'FROM INSERTED WHERE ' + '[' + @COLUMN_NAME + '] IS NOT NULL; ' + CHAR(13)

        FETCH NEXT FROM ColumnCursor INTO @COLUMN_NAME
    END

    CLOSE ColumnCursor
    DEALLOCATE ColumnCursor

We set our previous @EventLog_I_TR to a concatenated SQL string where we create a series of "INSERT INTO" queries. This will insert into our [EVENT_LOG] the table, primary key, column name, action taken (insert, update, delete- In this I am inserting ("I"), and the value associated with the action.

Closing the cursor, we now have a concatenated SQL string of INSERT to handle our table data.

Part 5:

    EXEC('IF EXISTS (SELECT * FROM sys.triggers WHERE object_id = OBJECT_ID(N''[TR_I_' + @TABLE_NAME + ']'')) DROP TRIGGER [TR_I_' + @TABLE_NAME + ']')
    EXEC('CREATE TRIGGER [TR_I_' + @TABLE_NAME + '] ON [' + @TABLE_NAME + '] AFTER INSERT AS SET NOCOUNT ON ' + @EventLog_I_TR)

    FETCH NEXT FROM TableCursor INTO @TABLE_NAME
END
   
CLOSE TableCursor
DEALLOCATE TableCursor
END

In our final Part 5, we first drop the trigger for each table in our [EVENT_LOG_TABLES] if it exists, before recreating it as a CREATE TRIGGER "TR_I_"+@TABLE_NAME. For this example, "I" stands for 'INSERT'. Adjust as you require (or combine methods yourself for update, insert, delete).

The output result of this is a trigger on each table you have defined within [EVENT_LOG_TABLES], which AFTER INSERT will create a record within [EVENT_LOG] for the table, primary key(s), column, action, and value.

I have expanded this for my own use with timedate and user constraints, and an event_id. Apply what is most appropriate yourself.

I hope this is helpful.

Reasons:
  • RegEx Blacklisted phrase (1): I want
  • Long answer (-1):
  • Has code block (-0.5):
  • User mentioned (1): @EventLog_I_TR
  • Low reputation (1):
Posted by: C-Lien