SQL Server“AFTER INSERT”触发器看不到刚刚插入的行
Posted
技术标签:
【中文标题】SQL Server“AFTER INSERT”触发器看不到刚刚插入的行【英文标题】:SQL Server "AFTER INSERT" trigger doesn't see the just-inserted row 【发布时间】:2010-09-29 03:36:17 【问题描述】:考虑一下这个触发器:
ALTER TRIGGER myTrigger
ON someTable
AFTER INSERT
AS BEGIN
DELETE FROM someTable
WHERE ISNUMERIC(someField) = 1
END
我有一个表 someTable,我试图阻止人们插入不良记录。就本题而言,不良记录的字段“someField”全为数字。
当然,执行此操作的正确方法不是使用触发器,但我不控制源代码...只是 SQL 数据库。所以我并不能真正阻止坏行的插入,但是我可以马上删除它,这已经足够满足我的需要了。
触发器工作,有一个问题......当它触发时,它似乎永远不会删除刚刚插入的坏记录......它会删除任何旧的坏记录,但它不会删除刚刚插入的坏记录.因此,经常会有一条坏记录在周围漂浮,直到其他人出现并进行另一次 INSERT 时才会删除。
我对触发器的理解有问题吗?触发器运行时新插入的行是否尚未提交?
【问题讨论】:
事务尚未提交(这就是您可以回滚的原因,这可能是您最好的答案),但您可以更新/删除行,因为触发器与 INSERT 语句在同一个事务中. 【参考方案1】:触发器不能修改已更改的数据(Inserted
或 Deleted
),否则您可能会在更改再次调用触发器时获得无限递归。一种选择是让触发器回滚事务。
编辑:这样做的原因是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.
【讨论】:
嗯...只要您没有在插入时或插入后进行插入,就不会触发无限递归。在这里,Joel 试图在插入时删除,这不会导致触发器重新触发。 我对该理论的回忆有点模糊,但 IIRC 实际上试图确定在一般情况下是难以处理的。您可能会遇到相互递归的触发器或其他无法静态评估的东西。 “标准”方法不支持更新不断变化的数据。 MSSQL 允许嵌套和递归触发器。 msdn.microsoft.com/en-us/library/aa258254(SQL.80).aspx MSDN 文章中讨论的递归触发器并不完全相同。如果您更新“插入”或“删除”,则在计算上难以决定其他触发器应该看到什么。想象一下,如果两个、三个或 10 个触发器进行了更新——正确的数据是什么?如果更新(甚至是否进行更新)取决于行的内容怎么办?现在正确的答案需要决策机制来解释触发器。打开***并阅读有关停止问题的不可判定性证明。 触发器肯定会修改传入的数据,这就是重点。我们相信数据库服务器不会在表中循环。危险在于在两个表之间编写一个循环,这就是你得到无限递归的时候,但即使这样也很难做到,因为两个表之间的操作从来都不是对称的。【参考方案2】:我认为您可以使用 CHECK 约束 - 这正是它的发明目的。
ALTER TABLE someTable
ADD CONSTRAINT someField_check CHECK (ISNUMERIC(someField) = 1) ;
我之前的回答(也可能有点矫枉过正):
我认为正确的方法是使用 INSTEAD OF 触发器来防止插入错误的数据(而不是事后删除)
【讨论】:
查看我的单独回答。 +1 德米特里。 原来我们使用触发器的原因是运行代码没有很好地处理插入失败。在大多数情况下,你是对的,我们不应该使用触发器。但最初的问题可能仍然与某人有关,所以我接受了完全解决这个问题的答案。【参考方案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."
我还没有测试过它,但从逻辑上讲,它看起来应该是你所追求的……而不是删除插入的数据,完全阻止插入,因此不需要你必须撤消插入。它应该性能更好,因此最终应该更轻松地处理更高的负载。
编辑:当然,如果插入发生在其他有效事务中,则整个事务可能会回滚,因此您需要考虑这种情况并确定如果插入无效的数据行会构成完全无效的事务...
【讨论】:
这条规则不能潜在地回滚一个有效的交易吗?想象一下这个场景。 1)更新一行以获得错误的数字 2)然后插入有效数据。触发器不会阻止#2 成功吗? 我想理论上你是对的。我将添加一个附录来限定它。【参考方案4】:来自CREATE TRIGGER 文档:
deleted 和 inserted 是逻辑(概念)表。他们是 结构类似于上表 其中触发器被定义,即 用户操作所在的表 尝试过,并保持旧值或 可能是行的新值 由用户操作更改。为了 例如,要检索 删除表,使用:
SELECT * FROM deleted
这样至少可以让您看到新数据。
我在文档中看不到任何指定在查询普通表时不会看到插入数据的内容...
【讨论】:
【参考方案5】:上面概述的技术很好地描述了您的选择。但是用户看到了什么?我无法想象你和负责软件的人之间的这种基本冲突怎么会导致与用户的混淆和对抗。
我会尽我所能找到摆脱僵局的其他方法 - 因为其他人可以很容易地将您所做的任何更改视为升级问题。
编辑:
我将获得我的第一个“取消删除”并承认在此问题首次出现时发布上述内容。当我看到它来自 JOEL SPOLSKY 时,我当然退缩了。但看起来它降落在附近的某个地方。不需要投票,但我会记录在案的。
对于除业务规则领域之外的细粒度完整性约束之外的任何事情,触发器很少是正确答案。
【讨论】:
用于垃圾邮件过滤器。垃圾邮件发送过程需要看到记录被成功插入,否则垃圾邮件发送者只会修改他们的垃圾邮件技术,直到它插入为止。我们不希望垃圾邮件发送者知道有一个 REGEXP 可以捕获他的垃圾邮件,否则他会解决它。 所以 - 故意恶意用户界面的合法案例。不错。【参考方案6】:你可以颠倒逻辑。不要在插入无效行后删除它,而是编写一个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
触发器不会在您插入到定义触发器的同一个表中时导致递归。
【讨论】:
谢谢,但我发现 CHECK 约束可能是更好的解决方案 是的,CHECK 约束也不错。我假设 Joel 简化了他的条件的性质,一些复杂的条件可能不是在 CHECK 约束中实现的最佳方式。 对。插入不会失败对我们来说很重要,因为这会使执行插入的进程崩溃。 Bill 您的解决方案在触发器中是一种不好的做法。它仅在插入一条记录时才有效。所有触发器都必须设计为处理多个记录插入。 @HLGEM:是的,你是对的。从那以后,我了解到 MS SQL Server 触发器会为给定操作中的所有行触发一次。我的大部分工作都是使用 InterBase/Firebird 和 mysql 完成的,其中每修改一行触发器就会触发一次。【参考方案7】:是否有可能 INSERT 有效,但之后执行的单独 UPDATE 无效但不会触发触发器?
【讨论】:
【参考方案8】:这是我修改后的比尔代码:
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 的触发器会将它们全部标记为错误。这可以处理任意数量的好记录和坏记录。
我在一个数据仓库项目中使用了这个技巧,其中插入应用程序不知道业务逻辑是否好用,而是在触发器中执行业务逻辑。性能确实很糟糕,但如果你不能让插入失败,它确实可以工作。
【讨论】:
【参考方案9】:更新:从触发器中删除适用于 MSSql 7 和 MSSql 2008。
我不是关系专家,也不是 SQL 标准专家。但是 - 与公认的答案相反 - MSSQL 可以很好地处理 recursive 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
你这里还有其他事情。
【讨论】:
【参考方案10】: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
【讨论】:
【参考方案11】:你的“触发器”正在做一些“触发器”不应该做的事情。您可以简单地让您的 Sql Server 代理运行
DELETE FROM someTable
WHERE ISNUMERIC(someField) = 1
每 1 秒左右。当你这样做的时候,写一个漂亮的小 SP 来阻止编程人员将错误插入你的表中怎么样。 SP 的一件好事是参数是类型安全的。
【讨论】:
【参考方案12】:我偶然发现了这个问题,以寻找有关插入语句和触发器期间事件序列的详细信息。我最终编写了一些简短的测试来确认 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 中打开另一个窗口并尝试: select * from table where 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 秒才能执行。在不同会话中执行的查询显示没有新数据 - 在执行插入 + 触发器期间或之后(尽管我希望任何标识都会增加,即使似乎没有插入数据)。
【讨论】:
以上是关于SQL Server“AFTER INSERT”触发器看不到刚刚插入的行的主要内容,如果未能解决你的问题,请参考以下文章
SQL Server“AFTER INSERT”触发器看不到刚刚插入的行