views:

480

answers:

8

Hi, I would like to know if there is anyway I can add a trigger on two tables that will replicate the data to the other.

For example:

  • I have a two users tables, users_V1 and users_V2, When a user is updated with one of the V1 app, it activate a trigger updating it in users_V2 as well.

  • If I want to add the same trigger on the V2 table in order to update the data in V1 when a user is updated in V2, will it go into an infinite loop? Is there any way to avoid that.

A: 

You're going to have to create some sort of loopback detection within your trigger. Perhaps using an "if exists" statement to see if the record exists before entering it into the next table. It does sound like it will go into an infinite loop the way it's currently set up.

Aaron
Yes, I'm already using if exists for the insert trigger, I was wondering if there is anything like that for the updates.
mnml
You'd use the same logic...if the user exists with the updated field, then don't update again. I'd add whatever field is updated to the where statement in my "if exists" query.
Aaron
A: 

Recursion in triggers, that is, one trigger calling another, is limited to 32 levels

In each trigger, just check if the row you wish to insert already exists.

Example

CREATE TRIGGER Table1_Synchronize_Update ON [Table1] FOR UPDATE AS
BEGIN
  UPDATE  Table2
  SET     LastName = i.LastName
          , FirstName = i.FirstName
          ,  ... -- Every relevant field that needs to stay in sync
  FROM    Table2 t2
          INNER JOIN Inserted i ON i.UserID = t2.UserID
  WHERE   i.LastName <> t2.LastName
          OR i.FirstName <> t2.FirstName
          OR ... -- Every relevant field that needs to stay in sync
END

CREATE TRIGGER Table1_Synchronize_Insert ON [Table1] FOR INSERT AS
BEGIN
  INSERT INTO Table2
  SELECT i.*
  FROM   Inserted i
         LEFT OUTER JOIN Table2 t2 ON t2.UserID = i.UserID
  WHERE  t2.UserID IS NULL
END

CREATE TRIGGER Table2_Synchronize_Update ON [Table2] FOR UPDATE AS
BEGIN
  UPDATE  Table1
  SET     LastName = i.LastName
          , FirstName = i.FirstName
          ,  ... -- Every relevant field that needs to stay in sync
  FROM    Table1 t1
          INNER JOIN Inserted i ON i.UserID = t1.UserID
  WHERE   i.LastName <> t1.LastName
          OR i.FirstName <> t1.FirstName
          OR ... -- Every relevant field that needs to stay in sync
END

CREATE TRIGGER Table2_Synchronize_Insert ON [Table2] FOR INSERT AS
BEGIN
  INSERT INTO Table1
  SELECT i.*
  FROM   Inserted i
         LEFT OUTER JOIN Table1 t1 ON t1.UserID = i.UserID
  WHERE  t1.UserID IS NULL
END
Lieven
The question specifies that this is an `UPDATE`, not `INSERT`. The question assumes that there will *always* be existing data.
Aaronaught
@Aaronaught - I known and it does not matter. The answer is valid. If you like, I'll explain it in more detail.
Lieven
Please do explain how the phrase "check if the row you wish to insert already exists" applies to an `UPDATE` operation; it certainly is not obvious and a plausible explanation would make this a much better answer.
Aaronaught
@Aaronaught - I am assuming that both tables have the same structure. If they do not, the `SELECT * FROM Inserted` needs adjustment. (Just noticed that the update is wrong, fixing it).
Lieven
That said, the solution you presented is much cleaner.
Lieven
OK - pretty cumbersome to maintain, and very similar to HLGEM's now, and the `INSERT` triggers will error out because they don't handle the cycles, but at least it's a clear and direct answer. Downvote removed.
Aaronaught
@Aaronaught - fixed the inserts. About HLGEM's answer being the same, you are right but I didn't "steal" his idea.
Lieven
A: 

Avoid triggers like the plague .... use a stored procedure to add the user. If this requires some design changes then make them. Triggers are the EVIL.

Hassan Syed
hehe I knew this would happen :D bring on the downvotes.
Hassan Syed
That's ridiculous, triggers are often necessary if you want to maintain data integrity. Stored procs just can't do that as ther is no guarantee that the person making the change will use the stored proc. If a dev has trouble understanding or dealing with triggers then he should get his skill level up not complain about them.
HLGEM
Well I could also add call the "V1 update function" and the V2 "update function" directly into the App. Code, I thought that trigger would be better for a few reasons: other devs wont forget to add both functions directly in the code and triggers can be removed very easily when the V1 isnt needed anymore.
mnml
@HLGEM Once you have a few triggers in place, changes to the schema are a disaster. Triggers area a quick fix -- developers should be on the ball about using the correct stored procedures.
Hassan Syed
Changes to the schema are not a disaster if you know what you are doing (We have lots of triggers and make lots of schema changes and haven't once hit this disaster you speak of) and not only the application hits the database and not all task can be done unig the sp (I'm not going to update 250,000 records using the one record at a time update proc for instance.
HLGEM
And triggers are not a quick fix (Sps are the quick fix in my view), triggers are the correct way to maintain complex data integrity rules. The only way to ensure that any way the data is changed, the rules will be enforced.
HLGEM
Agree with @HLGEM - this is a poor answer, stored procedures can only provide an abstraction over common functionality, they cannot actually *enforce a rule*, and they cannot operate on an entire record set (except using UDTTs). I've made dozens - no, *hundreds* of schema changes and never accidentally broken a trigger; that's what testing is for.
Aaronaught
While I agree with stringent convention it is possible to make use of triggers -- and any team that manages to juggle them correctly would certainly impress me. However, you can do anything and everything a trigger does without entrenching yourself. If you haven't encountered any problems with triggers I would hazard a guess that you haven't reached the required complexity point where triggers become a problem.
Hassan Syed
48 triggers, 6 of them recursive, 3 with possible multi-table cycles, and 12 enabled bit-flags in the context info, several of which are set by both stored procedures and a service abstraction to do special work. Is that complex enough? Database triggers should be no more difficult to manage than application events. I'd actually be very worried if I saw any production-scale database *without* triggers.
Aaronaught
Hassan, the point is no you can't enforce rules on ALL record changes with a stored proc. It is not possible. It is a pipe dream to think you can on any moderately complex system. Trying to do so will almost inevitably create data integrity issues at some point. Ity is the job of a database designer to protect the integrity of the data, you cannot guarantee a stored proc is the only way data will be changed, a trigger is far more likley to execute when needed.
HLGEM
A: 

Try something like (I didn;t bother with thecreate trigger stuff as you clearly already know how to write that part):

update t
set field1 = i.field1
field2 = i.field2
from inserted i
join table1 t on i.id  = t.id
where field1 <> i.field1 OR field2 <> i.field2
HLGEM
+4  A: 

I don't recommend explicitly disabling the trigger during processing - this can cause strange side-effects.

The most reliable way to detect (and prevent) cycles in a trigger is to use CONTEXT_INFO().

Example:

CREATE TRIGGER tr_Table1_Update
ON Table1
FOR UPDATE AS

DECLARE @ctx VARBINARY(128) 
SELECT @ctx = CONTEXT_INFO() 
IF @ctx = 0xFF
    RETURN

SET @ctx = 0xFF

-- Trigger logic goes here

See this link for a more detailed example.


Note on CONTEXT_INFO() in SQL Server 2000:

Context info is supported but apparently the CONTEXT_INFO function is not. You have to use this instead:

SELECT @ctx = context_info
FROM master.dbo.sysprocesses
WHERE spid = @@SPID
Aaronaught
Thank you for pointing that out, I'm going to have a look
mnml
Is there any alternative to CONTEXT_INFO() in SQL SERVER 2000 ?
mnml
@mnml: `CONTEXT_INFO` is available in SQL Server 2000. If you're using an even older version (I hope not), the link at the end of my answer shows an (inferior) solution using a temporary table. Temp tables are session-bound, so they have roughly the same effect, they're just not as efficient to use.
Aaronaught
It's SQL Server 8.0.194 but this is what I'm getting :Msg 195, Level 15, State 10, Procedure trigger, Line 8'CONTEXT_INFO' is not a recognized function name.
mnml
@mnml: You're right, SQL Server 2000 is different. I tracked it down and added a SQL 2000 version - context info is still there, you just have to get at it a slightly different way.
Aaronaught
thanks for the help :)
mnml
+1  A: 
  • Either use TRIGGER_NESTLEVEL() to restrict trigger recursion, or

  • check the target table whether an UPDATE is necessary at all:

    IF (SELECT COUNT(1) 
    FROM users_V1 
    INNER JOIN inserted ON users_V1.ID = inserted.ID
    WHERE users_V1.field1 <> inserted.field1
    OR users_V1.field2 <> inserted.field2) > 0 BEGIN
    
    
    UPDATE users_V1 SET ...
    
devio
IF TRIGGER_NESTLEVEL() > 1 RETURN-----works fine thanks
mnml
+1  A: 

I'm with the no triggers camp for this particular design scenario. Having said that, with the limited knowledge I have about what your app does and why it does it, here's my overall analysis:

Using a trigger on a table has an advantage of being able to act on all actions on the table. That's it, your main benefit in this case. But that would mean you have users with direct access to the table or multiple access points to the table. I tend to avoid that. Triggers have their place (I use them a lot), but it's one of the last database design tools I use because they tend to not know a lot about their context (generally, a strength) and when used in a place where they do need to know about different contexts and overall use cases, their benefits are weakened.

If both app versions need to trigger the same action, they should both call the same stored proc. The stored proc can ensure that all the appropriate work is done, and when your app no longer needs to support V1, then that part of the stored proc can be removed.

Calling two stored procs in your client code is a bad idea, because this is an abstraction layer of data services which the database can provide easily and consistently, without your application being worried about it.

I prefer to control the interface to the underlying tables more - with either views or UDFs or SPs. Users never get direct access to a table. Another point here is that you could present a single "users" VIEW or UDF coalescing the appropriate underlying tables without the user even knowing about - perhaps getting to the point where there is not even any "synchronization" necessary, since new attributes are in an EAV system if you need that kind of pathological flexibility or in some other different structure which can still be joined - say OUTER APPLY UDF etc.

Cade Roux
I agree that there are better roads to back-compat (I suspect that this could be done with just one table) - but multiple access paths to a table are inevitable when an application uses any kind of O/R mapper, which is most applications nowadays. It's good to use an SP as an abstraction, and it's better still to have both - route app calls through the SP, and then make the SP disable the trigger before doing its work. That way you can change the implementation *and* you're protected against careless Business Analysts with direct DB access who need to make large batch updates.
Aaronaught
@Aaronaught - Sure. General direct table access would be a final design change for me. I don't general enforce any database consistency through an SP - I do enforce access and limit capabilities (no one runs a query on a multi-million row fact table without restrictions). I'm not a fan of ORM, in the sense of a miracle abstraction which just works - I prefer to be the ORM. If my designs were trivial enough to work with an automatic system, they should be trivial enough to code-generate without me in the first place.
Cade Roux
If I implied that ORM was a magic bullet, then I apologize for any sort of misunderstanding. My approach to ORM is to use it for the 50-90% of the mapping work that *is* simply grunt-work, and use custom code for the rest. There's rarely any one-size-fits all solution to any problem; triggers and ORMs are just two more useful tools in the construction of a major system, in addition to stored procs and access restrictions.
Aaronaught
A: 

I had the exact same problem. I tried using CONTEXT_INFO() but that is a session variable and so it works only the first time! Then next time a trigger fires during the session, this won't work. So I ended up with using a variable that returns Nest Level in each of the affected triggers to exit.

Example:

CREATE TRIGGER tr_Table1_Update
ON Table1
FOR UPDATE AS
BEGIN
      --Prevents Second Nested Call
      IF @@NESTLEVEL>1 RETURN 

      --Trigger logic goes here
END

Note: Or use @@NESTLEVEL>0 if you want to stop all nested calls

One other note -- There seems to be much confusion in this article about nested calls and recursive calls. The original poster was referring to a nested trigger where one trigger would cause another trigger to fire, which would cause the first trigger to fire again, and so on. This is Nested, but according to SQL Server, not recursive because the trigger is not calling/triggering itself directly. Recursion is NOT where "one trigger [is] calling another". That is nested, but not necessarily recursive. You can test this by enabling/disabling recursion and nesting with some settings mentioned here: blog post on nesting

Valkyrie-MT