I need to perform several inserts in a single atomic transaction. For example:
start transaction;
insert ...
insert ...
commit;
However when MySQL encounters an error it aborts only the particular statement that caused the error. For example, if there is an error in the second insert statement the commit will still take place and the first insert statement will be recorded. Thus, when errors occur a MySQL transaction is not really a transaction. To overcome this problem I have used an error exit handler where I rollback the transaction. Now the transaction is silently aborted but I don't know what was the problem.
So here is the conundrum for you:
How can I both make MySQL abort a transaction when it encounters an error, and pass the error code on to the caller?
How can I both make MySQL abort a transaction when it encounters an error, and pass the error code on to the caller?
MySQL does pass error code to the caller and based on this error code the caller is free to decide whether it wants to commit work done up to the moment (ignoring the error with this particular INSERT statement) or to rollback the transaction.
This is unlike PostgreSQL which always aborts the transaction on error and this behavior is a source of many problems.
Update:
It's a bad practice to use an unconditional ROLLBACK inside the stored procedures.
Stored procedures are stackable and transactions are not, so a ROLLBACK within a nested stored procedure will roll back to the very beginning of the transaction, not to the state of the stored procedure execution.
If you want to use transactions to restore the database state on errors, use SAVEPOINT constructs and DECLARE HANDLER to rollback to the savepoints:
CREATE PROCEDURE prc_work()
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION ROLLBACK TO sp_prc_work;
SAVEPOINT sp_prc_work;
INSERT …;
INSERT …;
…
END;
Failure in either insert will roll back all changes made by the procedure and exit it.
Using Mr. Quassnoi's example, here's my best approach to catching specific errors:
The idea is to use a tvariable to catch a simple error message, then you can catch sql states you think may happen to save custom messages to your variable:
DELIMITER $$
DROP PROCEDURE IF EXISTS prc_work $$
CREATE PROCEDURE prc_work ()
BEGIN
DECLARE EXIT HANDLER FOR SQLSTATE '23000'
BEGIN
SET #prc_work_error = 'Repeated key';
ROLLBACK TO sp_prc_work;
END;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
SET #prc_work_error = 'Unknown error';
ROLLBACK TO sp_prc_work;
END;
START TRANSACTION;
SAVEPOINT sp_prc_work;
INSERT into test (id, name) VALUES (1, 'SomeText');
COMMIT;
END $$
DELIMITER ;
Then you just do your usual call, and do a select statement for the variable like:
call prc_work(); select #prc_work_error;
This will return either NULL if no error, or the message error in case of an error. If you need persistent error message you can optionally create a table to store it.
It's tedious and not very flexible because requires a DECLARE EXIT HANDLER segment for each status code you want to catch, it won't also show detailed error messages but hey, it works.
Related
I am trying to create an after insert trigger in MySQL that writes the message "Insert Failed" to an error table called tbl_error if the trigger fails for any reason.
To solve this problem, I have attempted to use a combination of handlers and signal statements. None have worked.
DELIMITER $$
CREATE TRIGGER trig_statesout_afterinsert AFTER INSERT ON states
FOR EACH ROW
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
SIGNAL SQLSTATE VALUE '99999'
SET MESSAGE_TEXT = 'Insert Failed';
INSERT INTO tbl_error(ErrorMessage) Values ('Insert Failed');
END;
INSERT INTO statesout (states_id, state, cases, lastupdate)
SELECT s.states_id, s.state, s.cases, current_timestamp()
FROM states as s
WHERE s.states_id = NEW.states_id;
END $$
I attempted to return information from a diagnostic statement, but that didn't work either.
Because the documentation states that
For SQLEXCEPTION conditions, the stored program terminates at the
statement that raised the condition, as if there were an EXIT handler.
If the program was called by another stored program, the calling
program handles the condition using the handler selection rules
applied to its own handlers.
I split the update logic for the trigger into a separate stored procedure in case the issue was that the SQLEXCEPTION conditions cannot be in the calling object. It didn't work.
Some SO solutions suggest using a "trick" to call a nonexistent table, but the documentation says this is not necessary due to the SIGNAL statement:
Without SIGNAL, it is necessary to resort to workarounds such as
deliberately referring to a nonexistent table to cause a routine to
return an error.
In any case, those solutions only throw errors based on a specific condition, not for any condition.
What am I doing wrong? Is it impossible for a MYSQL trigger to return an error message if the trigger fails for any reason?
It's not possible to make any change to data if the trigger activates a SQLException.
If the trigger experiences any error, whether handled or not handled, then the INSERT that spawned the trigger is undone, as well as any SQL run by the trigger, and even any SQL run by triggers spawned by actions your trigger executes.
This is related to the idea of atomicity — either the whole action must succeed, or else no part of the action is saved. It's all or nothing.
If you want to log the error, you'll have to check for the error in your application, and then log the error as a separate action. You can't do it in the trigger body.
I have this SQL in my MYSQL DB (sproc with empty body so I guess no implicit commits ?).
DROP PROCEDURE IF EXISTS doOrder;
DELIMITER $$
CREATE PROCEDURE doOrder(IN orderUUID VARCHAR(40))
BEGIN
SAVEPOINT sp_doOrder;
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION ROLLBACK TO sp_doOrder;
-- doing my updates and selects here...
END;
RELEASE SAVEPOINT sp_doOrder;
END $$
DELIMITER ;
When I
call doOrder('some-unique-id');
I get: ERROR 1305 (42000): SAVEPOINT sp_doOrder does not exist.
I might overlook something... Any idea?
Since this is the top answer on Google when searching for "savepoint does not exist", I'll add my solution here as well.
I had a TRUNCATE statement within the code executed in my transaction, which caused an implicit commit and thus ended the transaction. Creating a savepoint outside of a transaction does not cause an error, it will just not be executed. This means the first time you'll notice something is wrong is when you try to release your savepoint / rollback it back.
This is the full list of statements that cause an implicit commit: https://dev.mysql.com/doc/refman/5.7/en/implicit-commit.html
You have to use START TRANSACTION instead of BEGIN to start a transaction in a stored procedure.
Also, you may need to move the SAVEPOINT statement to be after the DECLARE (depending upon where you put the START TRANSACTION)
Note
Within all stored programs (stored procedures and functions, triggers,
and events), the parser treats BEGIN [WORK] as the beginning of a
BEGIN ... END block. Begin a transaction in this context with START
TRANSACTION instead.
Cf: http://dev.mysql.com/doc/refman/5.6/en/commit.html
This worked at the end. Thanks udog.
DROP PROCEDURE IF EXISTS doOrder;
DELIMITER $$
CREATE PROCEDURE doOrder(IN orderUUID VARCHAR(40))
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION ROLLBACK TO sp_order;
START TRANSACTION;
SAVEPOINT sp_order;
-- doing my updates and selects here...
COMMIT;
END $$
DELIMITER ;
MySQL stored procedure will throw the error out if there is no rollback command for SQLEXCEPTION, but it has changed some data before the exception.
I add rollback command for SQL exceptions:
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
END;
But I can not know the reason of the rollback now.
I know we can define every SQL exception and it's handler, but it's too complex and I just want to know why the rollback occurred.
Is there a simple method to get the reason of rollback in MySQL stored procedure?
Thanks #kordirko. I have get one solution with RESIGNAL. But it is only supported until MySQL 5.5.
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
SET #flag = 1;
IF #flag = 1 THEN RESIGNAL;END IF;
ROLLBACK;
END;
Let's say I have two stored procedures, Outer and Inner:
CREATE PROCEDURE dbo.Outer
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRAN
EXEC Inner
-- Perform additional processing (which should not occur if there is
-- a ROLLBACK in Inner)
...
COMMIT
END;
GO
The Outer stored procedure turns on XACT_ABORT and starts an explicit transaction. It then calls the Inner stored procedure inside the transaction.
CREATE PROCEDURE dbo.Inner
AS
BEGIN
DECLARE #Id INT=(SELECT Id FROM SomeTable WHERE ...);
IF (#Id IS NOT NULL)
ROLLBACK;
INSERT INTO SomeTable(...)
VALUES (...);
END;
GO
The Inner stored procedure performs a check to see if something is present and if it is wants to rollback the entire transaction started in the Outer stored procedure and abort all further processing in both Inner and Outer.
The thing that happens instead of what I expect as outlined above, is that I get the error message:
In Inner:
Transaction count after EXECUTE indicates a mismatching number of
BEGIN and COMMIT statements. Previous count = 1, current count = 0.
In Outer:
The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.
Clearly, the ROLLBACK even with XACT_ABORT turned on does not stop the flow of execution. Putting a RETURN statement after the ROLLBACK gets us out of Inner, but execution in Outer continues. What do I need to do in order to cause the ROLLBACK to stop all further processing and effectively cause an exit out of Outer?
Thanks for any help.
Issuing the rollback does not abort the batch (Regardless of the XACT_ABORT setting). The batch will automatically abort in your case if an error is thrown with the high enough severity - either system or custom generated via RAISERROR.
You have to implement Exception Handling
Begin Try
Set NOCOUNT ON
SET XACT_ABORT ON
Begin Tran
--Your Code
Commit Tran
End Try
Begin Catch
Rollback Tran
End Catch
PROBLEM SUMMARY:
i made error handling that seems to be way too complicated and still does not solve all situations (as there can be situations where transaction gets in uncommitable state). I suspect i:
have missed something important and doing it wrong (can you explain what? and how should i do it then?).
haven't missed anything- just have to accept that error handling is still huge problem in SQL Server.
Can you offer better solution (for described situation below)?
ABOUT MY SITUATION:
I have (couple of) stored procedure in SQL Server, that is called from different places. Can generalize for 2 situations:
Procedure is called from .NET code, transaction is made and handled in SQL procedure
Procedure is called in other procedure (to be more specific- in Service Broker activation procedure), so the transaction is handled by outer procedure.
I made it so, that procedure returns result (1 for success, 0 for failure) + returns message for logging purposes in case of error.
Inside the procedure:
Set XACT_ABORT ON; -- transaction not to be made uncommitable because of triggers.
Declare #PartOfTran bit = 0; -- is used, to save status: 1- if this procedure is part of other transaction or 0- should start new transaction.
If this is part of other tran, then make save point. If not- then begin transaction.
Begin try block- do everything and if there is no mistakes AND if this is not nested transaction do commit. If it is nested transaction- commit will be made in caller procedure.
In case of error: if this is nested transaction and transaction is in commitable state- can do rollback to savepoint "MyTran". if its not part of transaction, rollback transaction called "MyTran". In all other cases- just return error code and message.
Code looks like this:
Create Procedure dbo.usp_MyProcedure
(
-- params here ...
#ReturnCode int out, -- 1 Success, != 1 Error
#ReturnMsg nvarchar(2048) out
)
AS
Begin
Set NoCount ON;
Set XACT_ABORT ON;
Declare #PartOfTran bit = 0;
IF(##TRANCOUNT > 0)
Begin
SET #PartOfTran = 1;
SAVE TRAN MyTran;
END
Else
BEGIN TRAN MyTran;
Begin Try
-- insert table1
-- update table2
-- ....
IF(#PartOfTran = 0)
COMMIT TRAN MyTran;
Select #ReturnCode = 1, #ReturnMsg = Null;
End Try
Begin Catch
IF (XACT_STATE() = 1 And #PartOfTran = 1) OR #PartOfTran = 0
Rollback Tran MyTran;
Select #ReturnCode = 0, #ReturnMsg = ERROR_MESSAGE();
End Catch
End
OTHER LITERATURE:
From my favorite bloggers have seen:
sommarskog - but i don't like that "outer_sp" has line "IF ##trancount > 0 ROLLBACK TRANSACTION", because in my case- outer procedure can be called in transaction, so in that case i have "Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 1, current count = 0."
rusanu - actually almost the same as i wrote here (maybe idea comes from that blog post- i wrote my solution based on all i have read about this subject). This blog post still does not solve what should i do with uncommitable transactions. This is problem in case of Service Broker. How can i make correct logging of error message, if i have to rollback uncommitable transaction? i have ideas about this, but all of them seems like workarounds not elegant solutions.
You won't be able to achieve a solution that rolls back only the work done in usp_MyProcedure in any condition. Consider the most obvious example: deadlock. When your are notified of exception 1205 (you've been chosen as a deadlock victim) the transaction has already rolled back ( in order to allow progress). As error handling goes, the only safe option is to further raise and re-throw so that the caller has a chance to react. The 'uncommittable transaction' is just a variation on that theme: there is just no way error handling can recover from such a situation in a manner that is sensible for the caller, when the caller has started a transaction. The best thing is to raise (re-throw). This is why I used the pattern you've seen in my blog at Exception HAndling and Nested Transactions
Considering this in Service Broker context it means that there is no completely bullet proof, exception safe message handling routine. If you hit an uncommitable transaction (or a transaction that has already rolled back by the time you process the catch block, like 1205 deadlock) then your whole batch of received messages will have to rollback. Logging is usualy done in such situations after the outermost catch block (usually locate din the activated procedure). Here is pseudo code of how this would work:
usp_myActivatedProc
as
#commited = false;
#received = 0;
#errors = 0;
begin transaction
begin try
receive ... into #table;
#received = ##row_count;
foreach message in #table
save transaction
begin try
process one message: exec usp_myProcedure #msg
end try
begin catch
if xact_state()=1
rollback to savepoint
#errors += 1;
-- decide what to do with failed message, log
-- this failure may still be committed (receive won't roll back yet)
else
-- this is a lost cause, re-throw
raiserror
end catch
fetch next #table
endfor
commit
#commited = true;
end try
catch
#error_message = error_message();
if xact_state() != 0
rollback
end catch
if #commited = false
begin
insert into logging 'failed', #received, #error_message
end
-- insert table1
IF ##ERROR > 0
GOTO _FAIL
-- update table2
-- ....
IF ##ERROR > 0
GOTO _FAIL
GOTO _SUCCESS
_ERROR:
ROLLBACK TRAN
SET #ReturnCode = 1
RETURN
_FAIL:
ROLLBACK TRAN
SET #ReturnCode = 1
RETURN
_SUCCESS:
COMMIT TRAN
at the end of the tran insert the
GOTO _SUCCESS
so it will commit the tran if didnt hit any errors.
AND all of ur insert or update statement inside the tran insert the
IF ##ERROR > 0
GOTO _FAIL
so when it hit error.. it will go to the
_FAIL:
ROLLBACK TRAN
SET #ReturnCode = 1
RETURN
and u can set all ur return value at there