Триггер SQL Server «AFTER INSERT» не видит только что вставленную строку - PullRequest
42 голосов
/ 01 января 2009

Рассмотрим этот триггер:

ALTER TRIGGER myTrigger 
   ON someTable 
   AFTER INSERT
AS BEGIN
  DELETE FROM someTable
         WHERE ISNUMERIC(someField) = 1
END

У меня есть таблица, someTable, и я пытаюсь помешать людям вставлять плохие записи. Для целей этого вопроса у плохой записи есть поле "someField", которое полностью числовое.

Конечно, правильный способ сделать это НЕ с триггером, но я не контролирую исходный код ... только базу данных SQL. Поэтому я не могу предотвратить вставку плохой строки, но я могу сразу же удалить ее, что достаточно для моих нужд.

Триггер работает с одной проблемой ... когда он срабатывает, он, кажется, никогда не удаляет только что вставленную плохую запись ... он удаляет все старые плохие записи, но не удаляет только что вставленную плохую запись , Поэтому часто появляется одна плохая запись, которая не удаляется, пока кто-то другой не придет и не сделает еще одну ВСТАВКУ.

Это проблема в моем понимании триггеров? Вновь вставленные строки еще не зафиксированы во время работы триггера?

Ответы [ 12 ]

47 голосов
/ 01 января 2009

Триггеры не могут изменять измененные данные (Inserted или Deleted), в противном случае вы можете получить бесконечную рекурсию, поскольку изменения снова вызывают триггер. Один из вариантов - откат транзакции.

Редактировать: Причина этого в том, что стандарт для SQL состоит в том, что вставленные и удаленные строки не могут быть изменены триггером. Основная причина заключается в том, что изменения могут вызвать бесконечную рекурсию. В общем случае эта оценка может включать несколько триггеров во взаимно рекурсивном каскаде. Наличие разумной системы, решающей, разрешать ли такие обновления, вычислительно трудноразрешимо, по сути, является вариацией проблемы остановки .

Принято решение не разрешать триггеру изменять изменяющиеся данные, хотя это может откатить транзакцию.

create table Foo (
       FooID int
      ,SomeField varchar (10)
)
go

create trigger FooInsert
    on Foo after insert as
    begin
        delete inserted
         where isnumeric (SomeField) = 1
    end
go


Msg 286, Level 16, State 1, Procedure FooInsert, Line 5
The logical tables INSERTED and DELETED cannot be updated.

Нечто подобное откатит транзакцию.

create table Foo (
       FooID int
      ,SomeField varchar (10)
)
go

create trigger FooInsert
    on Foo for insert as
    if exists (
       select 1
         from inserted 
        where isnumeric (SomeField) = 1) begin
              rollback transaction
    end
go

insert Foo values (1, '1')

Msg 3609, Level 16, State 1, Line 1
The transaction ended in the trigger. The batch has been aborted.
39 голосов
/ 01 января 2009

Вы можете изменить логику. Вместо того, чтобы удалять недопустимую строку после ее вставки, запишите триггер INSTEAD OF, чтобы вставить только , если вы убедитесь, что строка верна.

CREATE TRIGGER mytrigger ON sometable
INSTEAD OF INSERT
AS BEGIN
  DECLARE @isnum TINYINT;

  SELECT @isnum = ISNUMERIC(somefield) FROM inserted;

  IF (@isnum = 1)
    INSERT INTO sometable SELECT * FROM inserted;
  ELSE
    RAISERROR('somefield must be numeric', 16, 1)
      WITH SETERROR;
END

Если ваше приложение не хочет обрабатывать ошибки (как, по словам Джоэля, имеет место в его приложении), то не RAISERROR. Просто заставьте триггер молча , а не , вставьте недопустимую вставку.

Я запустил это на SQL Server Express 2005, и он работает. Обратите внимание, что INSTEAD OF триггеры не вызывают рекурсию, если вы вставляете в ту же таблицу, для которой определен триггер.

28 голосов
/ 22 января 2009

Вот моя модифицированная версия кода Билла:

CREATE TRIGGER mytrigger ON sometable
INSTEAD OF INSERT
AS BEGIN
  INSERT INTO sometable SELECT * FROM inserted WHERE ISNUMERIC(somefield) = 1 FROM inserted;
  INSERT INTO sometableRejects SELECT * FROM inserted WHERE ISNUMERIC(somefield) = 0 FROM inserted;
END

Это позволяет всегда успешно выполнять вставку, и любые поддельные записи добавляются в ваши sometableRejects, где вы можете обработать их позже. Важно, чтобы ваша таблица отклонений использовала поля nvarchar для всего - не для целых, крошечных и т. Д. - потому что если они отклоняются, это потому, что данные не соответствуют вашим ожиданиям.

Это также решает проблему вставки нескольких записей, которая вызывает сбой триггера Билла. Если вы вставите десять записей одновременно (например, если вы делаете select-insert-into), и только одна из них является фиктивной, триггер Билла пометит все из них как плохие. Это обрабатывает любое количество хороших и плохих записей.

Я использовал этот трюк в проекте хранилища данных, в котором вставляющее приложение понятия не имело, хороша ли бизнес-логика, и вместо этого мы делали бизнес-логику в триггерах. По-настоящему неприятно для производительности, но если вы не можете позволить вставке не работать, она работает.

12 голосов
/ 01 января 2009

Я думаю, что вы можете использовать ограничение CHECK - это именно то, для чего оно было изобретено.

ALTER TABLE someTable 
ADD CONSTRAINT someField_check CHECK (ISNUMERIC(someField) = 1) ;

Мой предыдущий ответ (также может быть немного излишним):

Я думаю, что правильным способом является использование триггера INSTEAD OF для предотвращения вставки неправильных данных (а не удаления их постфактум)

7 голосов
/ 24 января 2009

ОБНОВЛЕНИЕ: УДАЛЕНИЕ из триггера работает как на MSSql 7, так и на MSSql 2008.

Я не гуру отношений и не сомневаюсь в стандартах SQL. Однако - вопреки принятому ответу - MSSQL прекрасно справляется как с оценкой r eursive, так и с вложенным триггером . Я не знаю о других РСУБД.

Соответствующими параметрами являются 'рекурсивные триггеры' и 'вложенные триггеры' . Вложенные триггеры ограничены 32 уровнями и по умолчанию равны 1. Рекурсивные триггеры по умолчанию отключены, и о разговоре об ограничении речи не идет - но, честно говоря, я никогда не включал их, поэтому не знаю, что происходит с неизбежным переполнение стека. Я подозреваю, что MSSQL просто убил бы ваш spid (или существует рекурсивный предел).

Конечно, это просто показывает, что принятый ответ имеет неправильную причину , а не то, что он неправильный. Однако, до триггеров INSTEAD OF, я вспоминаю, что писал триггеры ON INSERT, которые бы весело ОБНОВЛЯЛИ только что вставленные строки. Все это работало нормально, и, как и ожидалось.

Быстрый тест на УДАЛЕНИЕ только что вставленной строки также работает:

 CREATE TABLE Test ( Id int IDENTITY(1,1), Column1 varchar(10) )
 GO

 CREATE TRIGGER trTest ON Test 
 FOR INSERT 
 AS
    SET NOCOUNT ON
    DELETE FROM Test WHERE Column1 = 'ABCDEF'
 GO

 INSERT INTO Test (Column1) VALUES ('ABCDEF')
 --SCOPE_IDENTITY() should be the same, but doesn't exist in SQL 7
 PRINT @@IDENTITY --Will print 1. Run it again, and it'll print 2, 3, etc.
 GO

 SELECT * FROM Test --No rows
 GO

У вас здесь что-то еще происходит.

4 голосов
/ 01 января 2009

Из документации CREATE TRIGGER :

удалено и вставлено являются логическими (концептуальными) таблицами. Они есть структурно похож на таблицу на какой триггер определен, то есть таблица, на которой действие пользователя попытаться и удерживать старые значения или новые значения строк, которые могут быть изменено действием пользователя. За Например, чтобы получить все значения в удаленная таблица, используйте: SELECT * FROM deleted

Так что, по крайней мере, вы сможете увидеть новые данные.

Я не вижу в документах ничего, что указывает на то, что вы не увидите вставленные данные при запросе к обычной таблице ...

3 голосов
/ 01 января 2009

Я нашел эту ссылку:

create trigger myTrigger
on SomeTable
for insert 
as 
if (select count(*) 
    from SomeTable, inserted 
    where IsNumeric(SomeField) = 1) <> 0
/* Cancel the insert and print a message.*/
  begin
    rollback transaction 
    print "You can't do that!"  
  end  
/* Otherwise, allow it. */
else
  print "Added successfully."

Я не проверял его, но логически он выглядит так, как будто он должен отображать то, что вам нужно ... вместо того, чтобы удалять вставленные данные, полностью предотвратить вставку, таким образом, не требуя отменить вставку. Он должен работать лучше и, следовательно, в конечном итоге должен легче справляться с более высокой нагрузкой.

Изменить: Конечно, есть вероятность того, что если вставка произошла внутри другой действительной транзакции, то транзакция может быть откатана, поэтому вам нужно будет принять во внимание этот сценарий и определить вставка неверной строки данных будет представлять собой совершенно недействительную транзакцию ...

0 голосов
/ 04 июля 2018

Я наткнулся на этот вопрос, ища подробности о последовательности событий во время оператора вставки и триггера. В итоге я написал несколько кратких тестов, чтобы подтвердить поведение SQL 2016 (EXPRESS), и подумал, что было бы целесообразно поделиться им, так как это может помочь другим в поиске подобной информации.

На основании моего теста можно выбрать данные из "вставленной" таблицы и использовать их для обновления самих вставленных данных. И, что меня интересует, вставленные данные не видны другим запросам, пока триггер не завершится, и в этот момент окончательный результат будет виден (по крайней мере, лучше, чем я мог бы проверить). Я не проверял это для рекурсивных триггеров и т. Д. (Я ожидаю, что вложенный триггер будет иметь полную видимость вставленных данных в таблицу, но это только предположение).

Например - при условии, что у нас есть таблица «таблица» с целочисленным полем «поле» и полем первичного ключа «pk» и следующий код в нашем триггере вставки:

select @value=field,@pk=pk from inserted
update table set field=@value+1 where pk=@pk
waitfor delay '00:00:15'

Мы вставляем строку со значением 1 для «поля», тогда строка заканчивается значением 2. Более того - если я открою другое окно в SSMS и попробую: выберите * из таблицы, где pk = @ pk

где @pk - первичный ключ, который я изначально вставил, запрос будет пустым до истечения 15 секунд, а затем отобразит обновленное значение (поле = 2).

Меня интересовало, какие данные видны другим запросам во время выполнения триггера (очевидно, нет новых данных). Я также протестировал с добавленным удалением:

select @value=field,@pk=pk from inserted
update table set field=@value+1 where pk=@pk
delete from table where pk=@pk
waitfor delay '00:00:15'

Снова вставка заняла 15 секунд для выполнения. Запрос, выполняемый в другом сеансе, не показал новых данных - во время или после выполнения триггера вставки + (хотя я ожидал, что любой идентификатор будет увеличиваться, даже если кажется, что никакие данные не будут вставлены).

0 голосов
/ 04 июня 2018

Ваш «триггер» делает то, что «триггер» не должен делать. Вы можете просто запустить свой Sql Server Agent

DELETE FROM someTable
WHERE ISNUMERIC(someField) = 1

каждую 1 секунду или около того. Пока вы занимаетесь этим, как насчет того, чтобы написать симпатичный маленький SP, чтобы программисты не вставляли ошибки в вашу таблицу. Хорошая особенность SP заключается в том, что параметры являются типобезопасными.

0 голосов
/ 18 мая 2009

MS-SQL имеет настройку для предотвращения рекурсивного срабатывания триггера. Это подтверждается с помощью хранимой процедуры sp_configure, где вы можете включать или выключать рекурсивные или вложенные триггеры.

В этом случае будет возможно, если вы отключите рекурсивные триггеры, чтобы связать запись из вставленной таблицы через первичный ключ и внести изменения в запись.

В конкретном случае, о котором идет речь, это на самом деле не проблема, поскольку в результате удаляется запись, которая не будет обновлять этот конкретный триггер, но в целом это может быть правильным подходом. Таким образом, мы реализовали оптимистичный параллелизм.

Код вашего триггера, который можно использовать таким образом, будет:

ALTER TRIGGER myTrigger
    ON someTable
    AFTER INSERT
AS BEGIN
DELETE FROM someTable
    INNER JOIN inserted on inserted.primarykey = someTable.primarykey
    WHERE ISNUMERIC(inserted.someField) = 1
END
...