Please start any new threads on our new
site at https://forums.sqlteam.com. We've got lots of great SQL Server
experts to answer whatever question you can come up with.
| Author |
Topic |
|
egil
Starting Member
16 Posts |
Posted - 2008-06-05 : 12:37:55
|
HiI am building a pretty simple intranet application where we need to be able to track changes to tables. The tracking feature do not need to be very advanced, we just need to see who changed something and what it was. Therefore I decided just to use a trigger based solution, but need some input/advice since my SQL skills is somewhat lacking.Consider the following (mock-up) schema:-- My content tableCREATE TABLE [Content]( [ContentGuid] [uniqueidentifier] NOT NULL PRIMARY KEY DEFAULT (newid()), [Data] [nvarchar](4000) NOT NULL, [ChangedBy] [nchar](10) NOT NULL, [MaybeNull] [int] NULL)-- My history tableCREATE TABLE [History]( [ChangedTable] [nvarchar](50) NOT NULL, [ReferenceGuid] [uniqueidentifier] NOT NULL, [ChangedBy] [nchar](10) NOT NULL, [ChangedOn] [datetime] NOT NULL DEFAULT (getutcdate()), [IsDelete] [bit] NOT NULL DEFAULT ((0)), [Changes] [xml] NOT NULL) ON [PRIMARY]-- My insert/update triggerCREATE TRIGGER [RecordChangeOnInsertUpdate] ON [Content] AFTER INSERT,UPDATEAS BEGIN SET NOCOUNT ON; DECLARE @Changes xml SET @Changes = (select * from [inserted] for xml raw ('Content'), elements xsinil) INSERT INTO [History] ([ChangedTable] ,[ReferenceGuid] ,[ChangedBy] ,[ChangedOn] ,[IsDelete] ,[Changes]) SELECT 'Content', ContentGuid, ChangedBy, getutcdate(), 0, @Changes FROM [inserted]END-- My delete triggerCREATE TRIGGER [RecordChangeOnDelete] ON [Content] AFTER DELETEAS BEGIN SET NOCOUNT ON; DECLARE @Changes xml SET @Changes = (select * from [deleted] for xml raw ('Content'), elements xsinil) INSERT INTO [History] ([ChangedTable] ,[ReferenceGuid] ,[ChangedBy] ,[ChangedOn] ,[IsDelete] ,[Changes]) SELECT 'Content', ContentGuid, ChangedBy, getutcdate(), 1, @Changes FROM [deleted]ENDI have decided to use the "one history table for all table changes" method. The changes for a row is stored in a xml column which obviously limits the total size of columns in a table being tracked, but that is not a problem in my application. In general I like this set up, since I will be able to change the schema continuously without having to change the triggers, and since the application will probably evolve a lot over the coming months this is pretty important to me.EDIT: I should add that all the tables I will be tracking have a uniqueidentifier column. This makes it possible to related table rows in the different tables being tracked with their history.A few concerns with the above SQL:- Can the inserted/deleted table change between "SET @Changes = (select * ..." and the "INSERT INTO ..." statement, such that the data is not valid? If so, how to work around that?- If I were to (hypothetically) perform a "UPDATE [Content] SET [Data] = 'something'", not only is my update trigger called once for each row updated, but the XML added to the inserted row in the history table ([History].[Changes]) represent all the rows updated in the batch update. How do I get around this?Are there any other issues I should be aware of?Regards, Egil. |
|
|
Page47
Master Smack Fu Yak Hacker
2878 Posts |
Posted - 2008-06-05 : 13:24:09
|
quote: Can the inserted/deleted table change between...
Noquote: If I were to (hypothetically) perform a "UPDATE [Content] SET [Data] = 'something'", not only is my update trigger called once for each row updated....
False ... you trigger will run oncequote: ...but the XML added to the inserted row in the history table ([History].[Changes]) represent all the rows updated in the batch update.
True ... you will insert one row into [History]. If you want one row in [History] for each row in INSERTED/DELETED, you'll need to either loop through INSERTED/DELETED (weak) or write you insert into [History] in a set based manner (the true path).Jayto here knows when |
 |
|
|
egil
Starting Member
16 Posts |
Posted - 2008-06-05 : 13:40:43
|
Hi JayThanks for replying. quote: False ... you trigger will run once
Are you completely sure? I just did a quick test. Added two rows to the [Content] table and then did the mentioned updated, and two new rows was added the [History] table.I am running SQL Server 2005 in case that makes a difference.quote: True ... you will insert one row into [History]. If you want one row in [History] for each row in INSERTED/DELETED, you'll need to either loop through INSERTED/DELETED (weak) or write you insert into [History] in a set based manner (the true path).
Curious, how do I do a set based insert? |
 |
|
|
tkizer
Almighty SQL Goddess
38200 Posts |
Posted - 2008-06-05 : 14:09:43
|
| egil,It depends how you did the INSERT. If you ran two INSERT statements to add the two rows, then you ran two batches and therefore the trigger fired twice. If instead you added the two rows in one INSERT statement, then you ran one batch and therefore the trigger fired once and needs to be written to handle multiple rows.See this for an example of a trigger that handles multiple rows:http://weblogs.sqlteam.com/tarad/archive/2004/09/14/2077.aspxTara KizerMicrosoft MVP for Windows Server System - SQL Serverhttp://weblogs.sqlteam.com/tarad/Database maintenance routines:http://weblogs.sqlteam.com/tarad/archive/2004/07/02/1705.aspx |
 |
|
|
Page47
Master Smack Fu Yak Hacker
2878 Posts |
Posted - 2008-06-05 : 14:24:50
|
If you have 10 rows in [Content] and you run:update [Content] set [Data] = 'something' ...your trigger will run 1 time, not 10 times.Jayto here knows when |
 |
|
|
egil
Starting Member
16 Posts |
Posted - 2008-06-05 : 14:42:22
|
quote: Originally posted by Page47 If you have 10 rows in [Content] and you run:update [Content] set [Data] = 'something' ...your trigger will run 1 time, not 10 times.
Hmm. Why do I get two new rows in my [History] table when doing the update on [Content] table (which has a total of two rows) with update [Content] set [Data] = 'something' ?Regards, Egil. |
 |
|
|
Page47
Master Smack Fu Yak Hacker
2878 Posts |
Posted - 2008-06-05 : 15:01:02
|
| Well, it is not because the trigger fired twice. Promise.Take a look at you query in the trigger. Insert into History...select from inserted. You've got two rows in inserted, right?Jayto here knows when |
 |
|
|
egil
Starting Member
16 Posts |
Posted - 2008-06-05 : 15:29:30
|
Ahh sorry Jay, you are of cause right.I am trying to write a set-based solution that should solve that problem, if I understand you guys correct.I think I understand what I need to do - something like this:WITH ChangedContent (ContentGuid, Changes)AS (... this is where I get stuck. I need to call (select * from [inserted] for xml raw ('Content'), elements xsinil) for each row in [inserted] such that my temp ChangedContent table contains ContentGuid and associated xml representation of the row)INSERT INTO [History]([ChangedTable],[ReferenceGuid],[ChangedBy],[ChangedOn],[IsDelete],[Changes])SELECT 'Content', ContentGuid, ChangedBy, getutcdate(), 0, @ChangesFROM [inserted] JOIN ChangedContent ON [inserted].ContentGuid == ChangedContent.ContentGuidAm I on the right path or?Thanks for the help, Egil. |
 |
|
|
tkizer
Almighty SQL Goddess
38200 Posts |
Posted - 2008-06-05 : 15:43:32
|
| Why do you need a CTE for this?Tara KizerMicrosoft MVP for Windows Server System - SQL Serverhttp://weblogs.sqlteam.com/tarad/Database maintenance routines:http://weblogs.sqlteam.com/tarad/archive/2004/07/02/1705.aspx |
 |
|
|
egil
Starting Member
16 Posts |
Posted - 2008-06-05 : 15:56:56
|
tkizer: I probably don't :)I managed to get the below code to work as I want, but please tell me if there is a better way.with ChangeTable (TableName, ContentGuid, ChangedBy, ChangedOn, IsDelete, Changes)as (select 'Content', c2.ContentGuid, c2.ChangedBy, getutcdate(), 0, (select * from [inserted] c1 where c1.ContentGuid = c2.ContentGuid for xml raw ('Content'), elements xsinil) as Changes from [inserted] c2 )insert into [History] ([ChangedTable] ,[ReferenceGuid] ,[ChangedBy] ,[ChangedOn] ,[IsDelete] ,[Changes])select TableName, ContentGuid, ChangedBy, ChangedOn, Isdelete, Changesfrom ChangeTableRegards, Egil. |
 |
|
|
tkizer
Almighty SQL Goddess
38200 Posts |
Posted - 2008-06-05 : 16:00:29
|
| I would think you'd just use this without the CTE:INSERT INTO [History]([ChangedTable],[ReferenceGuid],[ChangedBy],[ChangedOn],[IsDelete],[Changes])SELECT 'Content', ContentGuid, ChangedBy, getutcdate(), 0, @ChangesFROM [inserted] JOIN ChangedContent ON [inserted].ContentGuid = ChangedContent.ContentGuidTara KizerMicrosoft MVP for Windows Server System - SQL Serverhttp://weblogs.sqlteam.com/tarad/Database maintenance routines:http://weblogs.sqlteam.com/tarad/archive/2004/07/02/1705.aspx |
 |
|
|
egil
Starting Member
16 Posts |
Posted - 2008-06-05 : 16:26:31
|
Ahh yes, that works too Tara. Thanks.Could a query like this be written better?SELECT c2.ContentGuid, (SELECT * FROM [Content] c1 WHERE c1.ContentGuid = c2.ContentGuid FOR XML RAW ('Content'), ELEMENTS XSINIL)FROM [Content] c2;It seems odd to me to have a nested select statement where I have.Regards, Egil. |
 |
|
|
tkizer
Almighty SQL Goddess
38200 Posts |
Posted - 2008-06-05 : 16:32:09
|
| I haven't a clue what that does.Tara KizerMicrosoft MVP for Windows Server System - SQL Serverhttp://weblogs.sqlteam.com/tarad/Database maintenance routines:http://weblogs.sqlteam.com/tarad/archive/2004/07/02/1705.aspx |
 |
|
|
egil
Starting Member
16 Posts |
Posted - 2008-06-05 : 16:40:28
|
| Ahh sorry. The select statement builds the ChangedContent content table in your example above. The [Content] in the statement should be replaced with [inserted]. |
 |
|
|
|
|
|
|
|