Previous | Table of Contents | Next |
ROLLBACK TRANSACTION
Last week (on Day 13), you learned how ROLLBACK TRANSACTION reverses any work since the last BEGIN TRAN statement. As you learned then, when SQL Server executes ROLLBACK TRANSACTION, it reverses all the work since the outermost BEGIN TRAN statement. If there was no explicit BEGIN TRAN statement, ROLLBACK TRANSACTION reverses all the work since the data modification statement started.
Lets look at our trigger again:
create trigger tr3 on t for insert, update as if update(c2) begin print testing c2 if exists (select * from t where c2 < 0) begin raiserror (c2 must be positive or zero, 16, 10) rollback transaction return end end return
Youve already seen how ROLLBACK TRANSACTION works within the trigger. It reversed the transaction, backing out the invalid data. What if there is a larger transaction taking place? Consider this example, where three inserts are bundled into a transaction:
/* careful - you need to code this better */ select * from t begin transaction insert t (c2, c3) values (101, B1) insert t (c2, c3) values (-10, B1) insert t (c2, c3) values (103, B1) commit transaction select * from t
c1 c2 c3 ----------- ----------- ---- 2 25 rstu 4 80 qqq 5 88 qqq 1 10 aaa 3 20 aaa testing c2 testing c2 Msg 50000, Level 16, State 10 c2 must be positive or zero testing c2 Msg 3902, Level 16, State 1 The commit transaction request has no corresponding BEGIN TRANSACTION. c1 c2 c3 ----------- ----------- ---- 2 25 rstu 4 80 qqq 5 88 qqq 1 10 aaa 3 20 aaa 8 103 B1
Your intention in grouping these three inserts into a single transaction is to guarantee that all the B1 rows will be inserted, or that none will be. Clearly, this failed to happen. One of the rows (the last row in the list) did make it into the table.
Well step through this closely. The trigger fires three times, once per INSERT statement (see the debugging output, testing c2). The second time, it finds an invalid row (c2 = -10), rolls back the transaction, and returns error message 50000.
Its the ROLLBACK that causes a problem. On Day 13, you learned about @@trancount, the global variable that tracks your transaction nesting level. ROLLBACK statements set @@trancount to zero, ending the transaction, but processing did not stop. The next statement ran in its own transaction context because it was initiated with @@trancount at 0, and thats the problem.
Note: I am running version 6.5 of Microsoft SQL Server, with no service packs installed. At the time of this writing, two service packs have been released. I dont believe the service packs address this problem.The reason I mention my version is this: Previous versions of SQL Server treated a transaction rollback differently. In version 4.21, ROLLBACK TRANSACTION in a trigger immediately halted trigger processing as well as batch processing. The problem described here simply never came up because subsequent statements in the batch were not executed.
In version 6.0, the trigger continued processing after ROLLBACK TRANSACTION until it reached a RETURN statement, but processing in the batch was aborted after that return. Again, the problem we are describing did not arise because later statements werent executed.
With the release of 6.5, ROLLBACK TRANSACTION in a trigger does not cause the trigger or the batch to stop processing. That is left to you instead. Is this a bug that is corrected in a service pack? Or a new feature? Word on the newsgroups is that Microsoft considers this a bug, but I havent heard that yet from a Microsoft representative. Until that happens, your code needs to deal with this problem straight on.
Regardless of how errors are being handled, if you roll back from a trigger, you must raise an error before you return to the batch. Triggers do not raise an error automatically. If you dont raise an error yourself, the user gets no warning that the execution failed. As far as he is concerned, the operation succeeded and the data modifications are in the table.
As with other potential errors, you need to check for trigger errors in your batch. Heres the batch, corrected to check for an error after each data modification statement:
/* careful - you need to code this better */ begin transaction insert t (c2, c3) values (101, B1) if @@error <> 0 goto error_return insert t (c2, c3) values (-10, B1) if @@error <> 0 goto error_return insert t (c2, c3) values (103, B1) if @@error <> 0 goto error_return commit transaction return error_return: if @@trancount <> 0 rollback tran return
After each statement, SQL Server checks for an error. If one is encountered, it checks to see whether a transaction is still current, rolling it back if necessary. (There are any number of errors that could occur without rolling back the transaction.)
This is just one possible way of coding the solution. (Many people avoid GOTO statements, but I consider this an appropriate use of GOTO.) Just make certain that your solution allows the transaction to handle errors that roll back the transaction as well as those that do not.
Previous | Table of Contents | Next |