SQL Server“AFTER INSERT”触发器看不到刚刚插入的行

时间:2009-01-01 18:50:01

标签: sql-server sql-server-2005 triggers

考虑这个触发器:

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

我有一张桌子,有桌子,我正试图阻止人们插入不良记录。出于这个问题的目的,坏记录有一个字段“someField”,它都是数字。

当然,正确的方法是使用触发器,但我不控制源代码......只是SQL数据库。所以我无法阻止插入坏行,但我可以立即将其删除,这对我的需求来说已经足够了。

触发器有效,有一个问题......当它触发时,它似乎永远不会删除刚刚插入的坏记录...它会删除任何旧的坏记录,但它不会删除刚刚插入的坏记录。所以通常会有一个不好的记录,直到其他人出现并执行另一次INSERT时才会被删除。

这是我对触发器的理解中的问题吗?在触发器运行时是否还没有提交新插入的行?

12 个答案:

答案 0 :(得分:47)

触发器无法修改已更改的数据(InsertedDeleted),否则您可以获得无限递归,因为更改会再次调用触发器。一种选择是触发器回滚事务。

编辑:这样做的原因是SQL的标准是触发器无法修改插入和删除的行。根本原因是修改可能导致无限递归。在一般情况下,此评估可能涉及相互递归级联中的多个触发器。让系统智能地决定是否允许这样的更新在计算上是难以处理的,基本上是halting problem.的变体

可接受的解决方案是不允许触发器更改更改的数据,尽管它可以回滚事务。

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.

答案 1 :(得分:39)

你可以颠倒逻辑。如果您确认该行有效,请写入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

如果您的应用程序不想处理错误(正如Joel在他的应用程序中所说的那样),那么请不要RAISERROR。只需以静默方式触发而不是执行无效的插入操作。

我在SQL Server Express 2005上运行它并且它可以工作。请注意,如果您插入到定义了触发器的同一个表中,INSTEAD OF触发器不会导致递归。

答案 2 :(得分:28)

这是Bill的代码的修改版本:

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字段来处理所有事情 - 而不是整数,细小等等 - 因为如果它们被拒绝,那是因为数据不是你所期望的那样。

这也解决了多记录插入问题,这将导致Bill的触发器失败。如果你同时插入10个记录(就像你进行select-insert-into)而其中只有一个是伪造的,那么Bill的触发器会将所有记录标记为坏。这可以处理任意数量的好记录和坏记录。

我在数据仓库项目中使用了这个技巧,其中插入应用程序不知道业务逻辑是否有用,而我们在触发器中执行了业务逻辑。真的很讨厌性能,但如果你不能让插入失败,它确实有效。

答案 3 :(得分:12)

我认为你可以使用CHECK约束 - 它正是它的发明。

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

我之前的回答(也可能有点矫枉过正):

我认为正确的方法是使用INSTEAD OF触发器来防止插入错误的数据(而不是在事后删除它)

答案 4 :(得分:7)

更新:从触发器中删除适用于MSSql 7和MSSql 2008。

我不是关系大师,也不是SQL标准。然而 - 与接受的答案相反 - MSSQL与r ecursive and nested trigger evaluation处理得很好。我不知道其他RDBMS。

相关选项为'recursive triggers' and 'nested triggers'。嵌套触发器被限制为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

你还有其他事情要发生。

答案 5 :(得分:4)

来自CREATE TRIGGER文档:

  

已删除已插入是逻辑(概念)表格。他们是   结构上类似于表格   定义了触发器,即   用户操作所在的表   尝试,并保持旧的值或   行的新值可能是   由用户操作更改。对于   例如,检索中的所有值   删除表格,使用:SELECT * FROM deleted

这样至少可以为您提供查看新数据的方法。

我在文档中看不到任何指定在查询普通表时不会看到插入数据的内容...

答案 6 :(得分:3)

我找到了这个参考:

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

我还没有对它进行过测试,但从逻辑上看它看起来应该是dp你所追求的...而不是删除插入的数据,完全阻止插入,因此不需要你必须撤消插入。它应该表现更好,因此最终应该更容易处理更高的负载。

编辑:当然, 的潜力是,如果插入发生在其他有效的事务中,那么wole事务可以回滚,因此您需要考虑该场景并确定如果插入无效数据行将构成完全无效的交易......

答案 7 :(得分:0)

上面列出的技术很好地描述了您的选择。但是用户看到了什么?我无法想象你和负责软件的人之间的这种基本冲突如何不会与用户混淆和对抗。

我会竭尽所能找到摆脱僵局的其他方法 - 因为其他人很容易看到你所做的任何改变都会使问题升级。

编辑:

我会得到我的第一个“取消删除”,并承认在这个问题首次出现时发布上述内容。当我看到它来自JOEL SPOLSKY时,我当然感到很沮丧。但看起来它落在了附近。不需要投票,但我会将其记录在案。

除了业务规则范围之外的细粒度完整性约束之外,IME,触发器很少是正确的答案。

答案 8 :(得分:0)

INSERT是否可能有效,但是之后的单独UPDATE是无效的但不会触发触发器?

答案 9 :(得分:0)

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

答案 10 :(得分:0)

你的“触发器”正在做一些“触发器”不应该做的事情。您可以简单地运行Sql Server Agent

DELETE FROM someTable
WHERE ISNUMERIC(someField) = 1
每隔1秒钟左右。当你在它的时候,如何编写一个漂亮的小SP来阻止编程人员将错误插入到你的表中。 SP的一个好处是参数是类型安全的。

答案 11 :(得分:0)

我偶然发现了这个问题,以寻找有关插入语句和触发器期间事件序列的详细信息。我最终对一些简短的测试进行了编码,以确认SQL 2016(EXPRESS)的行为-认为共享是适当的,因为它可能会帮助其他人搜索类似信息。

根据我的测试,可以从“插入”表中选择数据,并使用该表来更新插入的数据本身。而且,令我感兴趣的是,插入的数据对其他查询不可见,直到触发器完成为止,此时最终结果是可见的(至少是我可以测试的最好结果)。我没有对递归触发器等进行测试(我希望嵌套触发器对表中插入的数据具有完全可见性,但这只是一个猜测)。

例如-假设我们有一个带有整数字段“ field”和主键字段“ pk”的表“ table”,以及我们插入触发器中的以下代码:

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秒到期,然后将显示更新的值(field = 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秒。在其他会话中执行的查询未显示任何新数据-在执行insert +触发器期间或之后(尽管我希望即使没有数据插入,任何身份也会增加)。