2

We are building a protected INSTEAD OF UPDATE trigger to monitor and control the updates of several tables (most of MasterData tables of the system, about 150 of them). So, as much as possible (installation, updates) we try to do the code as reusable as possible (no hard-coding field names or table names).

To control the "actual" version of a row, an _ACTIVE field exist, and this goes decreasing for each new version (ACTIVE row gets ACTIVE = 1). Sorry we can not use the temporal tables feature due to backwards compatibility (plenty of business logic is built based on this feature)

Update logic includes treating the OLD and NEW lines before they affect the table, and once everything is treated, update the table; not only the affected rows, but all with same uniqueness key fields (the identification of the uniqueness fields aimed to be done dynamically too; on the following example, the where clause gets dynamically constructed on the variable @toWhereOnClause)

The "real" table suffers two actions, first a bunch of new lines are inserted with _ACTIVE = 2, second, all rows that need to be update get the _ACTIVE -= 1, leaving the newest version of the row set to 1

The problem arise as this second action, the update, needs to be created dynamically, to avoid entering the table name, and set the @toWhereOnClause manually. And this triggers once more the TRIGGER, and because it is dynamicSQL (we believe) is not captured by the TRIGGER_NESTLEVEL() = 1

Code structure is as follow:

CREATE OR ALTER TRIGGER  [schema].[triggerName]  ON [schema].table
INSTEAD OF UPDATE
AS
BEGIN


SET @tableName          = '[schema].[table]'  // only line to modify for diferent tables


//TRIGGER PREPARATION

SET @schema             = (SELECT SUBSTRING(@tableName, 1, (CHARINDEX('].', @tableName))))
SET @table              = (SELECT SUBSTRING(@tableName, (CHARINDEX('[', @tableName, 3)), LEN(@tableName)))
SET @fieldNameS         = (SELECT + ',' + QUOTENAME(COLUMN_NAME)
                            FROM INFORMATION_SCHEMA.COLUMNS
                            WHERE TABLE_SCHEMA = @schema            
                            AND TABLE_NAME = @table                     
                            ORDER BY ORDINAL_POSITION
                            FOR XML path(''));

SET @uniqueFieldSelector = (SELECT +' AND LeftTable.'+ COLUMN_NAME + ' = #INSERTED.' + COLUMN_NAME
                            FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE  
                            WHERE TABLE_NAME = @tableName 
                            AND COLUMN_NAME NOT LIKE '%Active%' 
                            FOR XML PATH(''))

SET @toWhereOnClause = (SELECT (SUBSTRING(@uniqueFieldSelector, 5, LEN(@uniqueFieldSelector))))


// DUPLICATE TRIGGER TABLES INTO TEMP TABLE TO WORK ON NEW AND OLD LINES

SELECT * INTO #INSERTED FROM INSERTED -- Can't modify logic table values (INSERTED), we put it in a temp table
SELECT * INTO #DELETED FROM DELETED

// SEVERAL INSTRUCTIONS TO TREAT THE OLD AND NEW LINES (not shown here) AND CALCULATE IF THE UPDATE IS LEGAL (@CONTINUE_TRIGGER)
...

// REAL UPDATE
IF TRIGGER_NESTLEVEL() = 1 AND @CONTINUE_TRIGGER = TRUE
        --https://stackoverflow.com/questions/1529412/how-do-i-prevent-a-database-trigger-from-recursing  

    BEGIN
        SET @statementINSERT = N'INSERT INTO' +  @tableName + '( ... ) 
                            SELECT   ...  FROM #INSERTED ';                        

        EXECUTE sp_executesql @statementINSERT



        SET @statementUPDATE = N'UPDATE TheRealTable
                SET TheRealTable._ACTIVE -= 1
                FROM ' + @tableName + ' AS TheRealTable
                INNER JOIN #INSERTED ON ' + @toWhereOnClause;

        EXECUTE sp_executesql @statementUPDATE   


    END


END 

yes, we know it is complex, but legacy doesn't give many options.

SO:

Is there any way to avoid the dynamicSQL trigger again the TRIGGER ??

(system is running on WindowsServer, and Azure instances, all with 120 compatibility at least)

pGrnd2
  • 514
  • 1
  • 5
  • 14
  • This just looks like a complete nightmare to maintain. What happens when there is a bug found? You have to go back and update the trigger on all 150 tables? I would rather have 150 triggers that I can control accurately instead of 150 copies of a generic one. But that seems like total overkill. Have you looked into change data capture? In my 20+ years of working with databases I have used maybe 5-6 triggers. 150 in a single database sounds like there is likely a better solution. – Sean Lange Jun 04 '18 at 15:33
  • @SeanLange thanks for response, yes, maybe there will be better solutions, but in this case, either a custom made trigger (exact same code except the dinamic update where there will be set the table name and field names manually) or 150 sp ... // in case of a bug, there will be always better to update the same trigger 150 times, that 150 different ones – pGrnd2 Jun 05 '18 at 08:35
  • @SeanLange, so far, two "best solution" have been found (I do not have so much experience as you, but about 9 years on BI and SQL): one, the previos shown, second, a function or script that makes grammatically the triggers, replacing table names instead of calculating them by variables – pGrnd2 Jun 05 '18 at 08:37
  • Per Sean's comments, this approach is going to give you severe headaches in the long run, as with anything involving triggers. That said, one generic way of keeping track of whether you're in a special block or not is to use [`SESSION_CONTEXT`](https://learn.microsoft.com/sql/t-sql/functions/session-context-transact-sql) with a descriptive key. Set it on entering the trigger, clear it when you're done. On versions lacking `SESSION_CONTEXT`, `CONTEXT_INFO` is a less flexible alternative (but can only be used by one application). – Jeroen Mostert Jun 05 '18 at 10:14
  • Adding on Session Context - You can't avoid the trigger firing. Try moving all of the dynamic code execution to a procedure. Within that procedure set a Session Context param. In the trigger, add a condition to look for that Session param. If it is set, then don't do anything. If it is not, then run the procedure to run the dynamic code. You will also need to make sure security-wise who is allowed to run teh procedure. So that it won't be abused to bypass the triggers. Then at the end of the procedure turn the session param off. – Amir Pelled Jun 06 '18 at 12:09

0 Answers0