#sql #sql-server #tsql #triggers #transactions
#sql #sql-server #tsql #триггеры #транзакции
Вопрос:
Я столкнулся с проблемой при попытке настроить транзакцию в триггере моего представления. Вот моя настройка 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, чтобы исключить ошибку, которая, как я знаю, должна произойти.
Комментарии:
1. Кроме того: из-за того, что вы используете
Scope_Identity
, создается впечатление, что ваш триггер не будет корректно работать, когдаinserted
содержит более одной строки.2. Спасибо за совет, @HABO! Я предполагаю, что я должен использовать
OUTPUT
предложение для сохранения идентификаторов всех вновь вставленных строк.3. Я бы пересмотрел любой дизайн, использующий точки сохранения. Слишком сложно поддерживать.
Ответ №1:
Это, казалось бы, запутанное поведение можно прояснить, добавив регистрацию ошибок в ваши 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 с аналогичным вопросом. В своем ответе Руцки показывает, что виновником такого поведения является 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 defau< 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
(Опять же, я исправил несколько других проблем с вашим кодом.)
Комментарии:
1. Спасибо за подробное объяснение. Я не понимал, что это
XACT_ABORT
переключается наON
неявно в триггерах. Кроме того, спасибо, что указали и исправили проблемы в моей настройке.