我正在尝试在我的视图触发器中设置事务时遇到问题。这是我的DDL设置:
CREATE TABLE entity1 (
id INT NOT NULL IDENTITY PRIMARY KEY,
attr1 INT NOT NULL,
attr2 INT NOT NULL
);
GO
CREATE TABLE entity2 (
entity1_id INT NOT NULL FOREIGN KEY REFERENCES entity1(id),
attr3 INT NOT NULL,
attr4 INT NOT NULL
);
GO
CREATE VIEW my_view AS
SELECT attr1, attr2, attr3, attr4
FROM entity1 AS e1
INNER JOIN entity2 AS e2
ON e1.id = e2.entity1_id;
GO
CREATE TRIGGER tg_my_view_ins ON my_view
INSTEAD OF INSERT AS
BEGIN
BEGIN TRY
SAVE TRANSACTION here; -- checkpoint
INSERT INTO entity1 (attr1, attr2)
SELECT attr1, attr2 FROM inserted;
INSERT INTO entity2 (entity1_id, attr3, attr4)
SELECT SCOPE_IDENTITY(), attr3, attr4 FROM inserted;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION here; -- rollback to checkpoint in case on an error
END CATCH
END
GO
如您所见,我在触发器中创建一个保存点,并在出现任何错误时回滚(我假设约束错误也由TRY / CATCH块处理)。问题是,当我在事务中执行错误插入时,触发器错误处理块不会回滚:
BEGIN TRY
BEGIN TRANSACTION;
-- successful insert
INSERT INTO my_view (attr1, attr2, attr3, attr4) VALUES (1,2,3,4);
SELECT * FROM entity1; -- one entity
-- i wrap the bad insert into try/catch so the error is discarded,
-- but still rolled back
BEGIN TRY
INSERT INTO my_view (attr1, attr2, attr3) VALUES (3,2,1);
END TRY
BEGIN CATCH
END CATCH;
SELECT * FROM entity1; -- should only have one entity, but has two
ROLLBACK; -- discard the whole transaction
END TRY
BEGIN CATCH
ROLLBACK; -- discard the whole transaction in case of any errors
END CATCH;
我似乎无法设置触发器,因为它不会在出现错误时创建孤立记录。我尝试在我的触发器中使用BEGIN TRANSACTION here
和COMMIT TRANSACTION here
而不是SAVE TRANSACTION here
,没有运气。在触发器中处理约束错误的正确方法是什么?
执行设置我想保持原样,如果可能的话。我创建并回滚事务以进行测试。我将错误的插入包装到try / catch块中以丢弃我知道应该发生的错误。
通过在catch
块中添加错误记录,可以清楚地看到这种看似混乱的行为。以下对测试代码的修改添加了错误日志记录(以及其他一些改进),其中显示了该过程中实际发生的情况:
begin try
begin transaction;
INSERT INTO dbo.my_view (attr1, attr2, attr3, attr4) VALUES (1,2,3,4);
SELECT * FROM dbo.entity1;
BEGIN TRY
INSERT INTO dbo.my_view (attr1, attr2, attr3) VALUES (3,2,1);
END TRY
BEGIN CATCH
-- Logging - inner CATCH
select 'Inner', @@trancount, error_number(), error_message(), error_procedure(), error_line();
END CATCH;
select * from dbo.entity1;
rollback;
end try
begin catch
-- Logging - outer CATCH
select 'Outer', @@trancount, error_number(), error_message(), error_procedure(), error_line();
-- Conditional rollback, because some errors always terminate the transaction
if @@trancount > 0
rollback;
end catch;
如果在触发器完好无损的情况下运行此代码,您将看到内部CATCH
捕获的错误:
3931
当前事务无法提交,也无法回滚到保存点。回滚整个交易。
按错误编号搜索会导致this post出现类似问题。在他的回答中,Rutzky表明这种行为的罪魁祸首是XACT_ABORT
会话选项,默认情况下显然设置为ON
触发器。如果您的目的是追求基于触发器的架构,那么在触发器内关闭此选项将有助于:
create or alter trigger dbo.tg_my_view_ins
on dbo.my_view
instead of insert as
-- Implicitly set to ON in triggers by default; makes error handling impossible
set xact_abort off;
begin try
save transaction here;
INSERT INTO dbo.entity1 (attr1, attr2)
SELECT attr1, attr2 FROM inserted;
INSERT INTO dbo.entity2 (entity1_id, attr3, attr4)
SELECT e.id, attr3, attr4
FROM inserted i
-- The actual JOIN condidions should reference a natural key in the master table.
-- This is just an example.
inner join dbo.entity1 e on e.attr1 = i.attr1 and e.attr2 = i.attr2;
end try
begin catch
if @@trancount > 0
rollback transaction here;
end catch;
return;
GO
(同样,我已经纠正了您的代码中的其他几个问题。)