45

考虑这个触发器:

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

我有一个表 someTable,我试图阻止人们插入不良记录。就本问题而言,不良记录具有一个全为数字的字段“someField”。

当然,这样做的正确方法不是使用触发器,但我不控制源代码......只是 SQL 数据库。所以我不能真正阻止坏行的插入,但我可以马上删除它,这已经足够满足我的需要了。

触发器工作,有一个问题......当它触发时,它似乎永远不会删除刚刚插入的坏记录......它会删除任何旧的坏记录,但它不会删除刚刚插入的坏记录。所以经常有一个坏记录漂浮在周围,直到其他人出现并执行另一个 INSERT 时才会删除。

这是我对触发器的理解的问题吗?触发器运行时新插入的行是否尚未提交?

4

12 回答 12

48

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

编辑:这样做的原因是 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.
于 2009-01-01T18:52:50.340 回答
40

你可以颠倒逻辑。与其在插入无效行后删除它,不如编写一个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触发器不会导致递归。

于 2009-01-01T20:04:32.020 回答
28

这是我修改后的比尔代码:

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 字段 - 而不是 ints、tinyints 等 - 因为如果它们被拒绝,那是因为数据不是您所期望的。

这也解决了多记录插入问题,会导致Bill的触发器失败。如果您同时插入 10 条记录(就像您执行 select-insert-into 一样)并且其中只有一条是伪造的,那么 Bill 的触发器会将它们全部标记为错误。这可以处理任意数量的好记录和坏记录。

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

于 2009-01-22T14:20:51.970 回答
12

我认为您可以使用 CHECK 约束——这正是它的发明目的。

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

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

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

于 2009-01-01T18:57:46.897 回答
7

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

我不是关系专家,也不是 SQL 标准专家。然而 - 与公认的答案相反 - MSSQL 处理递归和嵌套触发器评估都很好。我不知道其他 RDBMS。

相关选项是'recursive triggers' 和 '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

你这里有其他事情发生。

于 2009-01-24T06:08:53.117 回答
4

CREATE TRIGGER文档中:

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

因此,这至少为您提供了一种查看新数据的方法。

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

于 2009-01-01T19:17:31.177 回答
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 你所追求的......而不是删除插入的数据,完全阻止插入,因此不需要你必须撤消插入。它应该表现得更好,因此最终应该更轻松地处理更高的负载。

编辑:当然,如果插入发生在其他有效事务中,整个事务可能会回滚,因此您需要考虑这种情况并确定插入无效数据行是否构成完全无效的交易...

于 2009-01-01T19:08:47.997 回答
0

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

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

编辑:

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

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

于 2009-01-01T19:20:52.497 回答
0

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

于 2009-01-05T16:57:58.467 回答
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
于 2009-05-18T02:36:13.073 回答
0

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

DELETE FROM someTable
WHERE ISNUMERIC(someField) = 1

每1秒左右。当你这样做的时候,写一个漂亮的小 SP 来阻止编程人员将错误插入你的表中怎么样。SP 的一件好事是参数是类型安全的。

于 2018-06-04T13:49:41.903 回答
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 中打开另一个窗口并尝试: select * from table where 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 秒才能执行。在不同会话中执行的查询显示没有新数据 - 在执行插入 + 触发器期间或之后(尽管我希望任何身份都会增加,即使似乎没有插入数据)。

于 2018-07-03T23:19:26.470 回答