Триггер SQL Server «AFTER INSERT» не видит только что вставленную строку

StackOverflow https://stackoverflow.com/questions/405288

Вопрос

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

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

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

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

Триггер работает, но есть одна проблема...когда он срабатывает, он никогда не удаляет только что вставленную плохую запись...он удаляет все СТАРЫЕ плохие записи, но не удаляет только что вставленную плохую запись.Таким образом, часто существует одна плохая запись, которая не удаляется, пока кто-то другой не придет и не выполнит еще одну INSERT.

Является ли это проблемой в моем понимании триггеров?Не зафиксированы ли вновь вставленные строки во время работы триггера?

Это было полезно?

Решение

Триггеры не могут модифицировать измененные данные (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.

Другие советы

Вы можете перевернуть логику.Вместо удаления недопустимой строки после ее вставки напишите 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 триггеры не вызвать рекурсию, если вы вставите в ту же таблицу, для которой определен триггер.

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

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 для всего, а не целые числа, крошечные числа и т. д., потому что если они отклоняются, это потому, что данные не такие, как вы ожидали.

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

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

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

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

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

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

ОБНОВЛЯТЬ:DELETE из триггера работает как в MSSql 7, так и в MSSql 2008.

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

Соответствующие варианты: «рекурсивные триггеры» и «вложенные триггеры».Вложенные триггеры ограничены 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

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

Из СОЗДАТЬ ТРИГГЕР документация:

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

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

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

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

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."

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

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

Возможно ли, что INSERT действителен, но после этого выполняется отдельное UPDATE, которое является недействительным, но не запускает триггер?

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

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

РЕДАКТИРОВАТЬ:

Я сделаю свое первое «восстановление» и признаюсь, что опубликовал вышеизложенное, когда этот вопрос впервые появился.Я, конечно, струсил, когда увидел, что это от ДЖОЭЛА СПОЛЬСКИ.Но похоже, что он приземлился где-то рядом.Голоса не нужны, но я занесу это в протокол.

IME, триггеры редко являются правильным ответом на что-либо, кроме мелкозернистых ограничений целостности, выходящих за рамки бизнес-правил.

В 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

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

DELETE FROM someTable
WHERE ISNUMERIC(someField) = 1

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

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

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

Например, предположим, что у нас есть таблица «table» с целочисленным полем «field» и полем первичного ключа «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 секунд.Запрос, выполняемый в другом сеансе, не показал новых данных - во время или после выполнения триггера вставки + (хотя я ожидаю, что любой идентификатор будет увеличиваться, даже если данные не будут вставлены).

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top