而不是 SQL Server 中的触发器丢失 SCOPE_IDENTITY?

Posted

技术标签:

【中文标题】而不是 SQL Server 中的触发器丢失 SCOPE_IDENTITY?【英文标题】:Instead of trigger in SQL Server loses SCOPE_IDENTITY? 【发布时间】:2009-05-25 22:25:32 【问题描述】:

我有一个表,我在其中创建了一个 INSTEAD OF 触发器来执行一些业务规则。

问题是当我将数据插入此表时,SCOPE_IDENTITY() 返回一个 NULL 值,而不是实际插入的标识。

插入 + 范围代码

INSERT INTO [dbo].[Payment]([DateFrom], [DateTo], [CustomerId], [AdminId])
VALUES ('2009-01-20', '2009-01-31', 6, 1)

SELECT SCOPE_IDENTITY()

触发器:

CREATE TRIGGER [dbo].[TR_Payments_Insert]
   ON  [dbo].[Payment]
   INSTEAD OF INSERT
AS 
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;

    IF NOT EXISTS(SELECT 1 FROM dbo.Payment p
              INNER JOIN Inserted i ON p.CustomerId = i.CustomerId
              WHERE (i.DateFrom >= p.DateFrom AND i.DateFrom <= p.DateTo) OR (i.DateTo >= p.DateFrom AND i.DateTo <= p.DateTo)
              ) AND NOT EXISTS (SELECT 1 FROM Inserted p
              INNER JOIN Inserted i ON p.CustomerId = i.CustomerId
              WHERE  (i.DateFrom <> p.DateFrom AND i.DateTo <> p.DateTo) AND 
              ((i.DateFrom >= p.DateFrom AND i.DateFrom <= p.DateTo) OR (i.DateTo >= p.DateFrom AND i.DateTo <= p.DateTo))
              )

    BEGIN
        INSERT INTO dbo.Payment (DateFrom, DateTo, CustomerId, AdminId)
        SELECT DateFrom, DateTo, CustomerId, AdminId
        FROM Inserted
    END
    ELSE
    BEGIN
            ROLLBACK TRANSACTION
    END


END

代码在创建此触发器之前工作。我在 C# 中使用 LINQ to SQL。我看不到将SCOPE_IDENTITY 更改为@@IDENTITY 的方法。我该如何完成这项工作?

【问题讨论】:

我认为这个表在您插入 INSTEAD OF 语句之前就已经工作了吗?您是否检查了主键字段以确保它具有身份规范? 是的,确实如此(您现在可以看到代码 - 它确实插入了一行,并且在插入时确实获得了自动标识)。 为什么不使用在不满足规则时产生错误的 BEFORE INSERT 触发器,而不是使用 INSTEAD OF? @araqnid - 因为据我所知 SQL Server 没有这样的东西 - 虽然我必须承认我没有直接尝试过,但基于谷歌搜索,当然可能是我发现了无效或过时的资源——真的有这样的东西吗? 我们在 SQL Server 2000 中使用了这样的触发器——这似乎是默认模式,只是将触发器声明为“在 dbo.tablename 上创建触发器 TG_xxx 以进行插入,更新为 ....” .我们有逻辑来测试条件并调用“raiserror()”,然后在验证失败时执行“回滚事务”。话虽如此,我们使用 SP 来生成 ID 而不是身份——这可能就是为什么,坚持我们这样做的人对推理有点含糊。但后来他在实施 scope_identity() 之前就养成了这个习惯。 【参考方案1】:

使用@@identity 代替scope_identity()

scope_identity() 返回当前作用域中最后创建的 id,@@identity 返回当前会话中最后创建的 id。

通常建议在 @@identity 字段上使用 scope_identity() 函数,因为您通常不希望触发器干扰 id,但在这种情况下您会这样做。

【讨论】:

在我的帖子中添加了更多内容后,您可以看到我在我的 .Net 应用程序中使用 LINQ to SQL,所以据我所知,我在这里真的没有太多选择,除非可能使用 sproc 插入数据。 我对使用@@identity 有很大的犹豫,但我想在这种狭隘的情况下没关系,因为其他解决方法并没有好多少,至少你知道它会给出正确的价值,如果同时触发器不会插入到另一个表中。 @Emtucifor:在大多数情况下,您想要 scope_identity() 返回的内容,但在这种情况下不需要。这次是@@identity 是正确的选择。两者都存在的原因是有时您实际上想要不寻常的结果。 Guffa,您只需要 @@identity 因为 scope_identity() 没有执行我们都期望它执行的功能:您插入到具有标识列的表中,然后想要那个标识列值背部。 Scope_Identity() 应该保护您无需搜索然后仔细检查表上的任何触发器,以确保它们不会插入到具有标识列的另一个表中。但是,您提出的使用 @@identity 的无限制建议将在表上出现另一个后触发器或将替代触发器插入到第二个表时中断。 我意识到你不能假设,当插入一个带有 INSTEAD OF 触发器的表时,这些行甚至会被添加到表中。它们可以放在另一个表或许多其他表中,或者根本不插入任何地方。但是,调用脚本不知道这一点,也不应该知道这一点。 INSTEAD OF 触发器的目的是使底层数据操作的杂乱内容对执行插入的客户端透明。在我看来,应该有一些方法可以在 INSTEAD OF 触发器中显式设置 scope_identity。【参考方案2】:

由于您使用的是 SQL 2008,因此我强烈建议您使用 OUTPUT 子句而不是自定义标识函数之一。 SCOPE_IDENTITY 目前在并行查询方面存在一些问题,导致我完全反对它。 @@Identity 没有,但它仍然不像 OUTPUT 那样明确和灵活。加上 OUTPUT 处理多行插入。看看the BOL article 有一些很好的例子。

【讨论】:

并行查询有什么问题?它们在不同的范围内,所以我看不出它们在哪里会成为问题。 ident_current 存在并行插入问题,@@identity 存在来自触发器的插入问题,这就是为什么在发明输出之前 scope_identity 是首选方法的原因。但是使用输出的建议非常好。 对不起,我应该发布连接链接:connect.microsoft.com/SQLServer/feedback/… 关于并行查询的观点很好。但是@@identity 可以很好地处理多行插入:INSERT TheTable ... | SELECT @LastID = Scope_Identity(), @Rows = @@RowCount | SELECT FROM TheTable WHERE ID BETWEEN @LastID - @Rows + 1 AND @LastID 此外,如果表上存在 INSTEAD OF INSERT 触发器,则 OUTPUT 子句将始终为标识列返回 0。 对于从未听说过或使用过 OUTPUT 子句的人,这里有一个线程解释了如何使用它:***.com/questions/10999396/…【参考方案3】:

我对使用@@identity 有很大的保留意见,因为它可能会返回错误的答案。

但是有一种解决方法可以强制 @@identity 具有 scope_identity() 值。

为了完整起见,首先我将列出我在网上看到的这个问题的其他一些解决方法:

    使触发器返回一个行集。然后,在执行插入的包装器 SP 中,对另一个表执行 INSERT Table1 EXEC sp_ExecuteSQL ...。然后 scope_identity() 将起作用。这很麻烦,因为它需要动态 SQL,这很痛苦。另外,请注意,动态 SQL 在调用 SP 的用户的权限下运行,而不是在 SP 所有者的权限下运行。如果原始客户端可以插入到表中,他应该仍然具有该权限,只要知道如果您拒绝直接插入到表中的权限,您可能会遇到问题。

    如果有另一个候选键,则使用这些键获取插入行的标识。例如,如果 Name 上有一个唯一索引,那么您可以插入,然后使用 Name 从刚刚插入的表中选择(最多为多行)ID。虽然如果另一个会话删除了您刚刚插入的行,这可能会出现并发问题,但如果有人在应用程序可以使用它之前删除了您的行,这并不比原来的情况更糟。

现在,以下是如何明确地使您的触发器安全地让@@Identity 返回正确的值,即使您的 SP 或其他触发器在主插入之后插入到承载身份的表中

另外,请在您的代码中添加 cmets,说明您在做什么以及为什么这样做,以便触发器的未来访问者不会破坏事情或浪费时间试图弄清楚。

CREATE TRIGGER TR_MyTable_I ON MyTable INSTEAD OF INSERT
AS
SET NOCOUNT ON

DECLARE @MyTableID int
INSERT MyTable (Name, SystemUser)
SELECT I.Name, System_User
FROM Inserted

SET @MyTableID = Scope_Identity()

INSERT AuditTable (SystemUser, Notes)
SELECT SystemUser, 'Added Name ' + I.Name
FROM Inserted

-- The following statement MUST be last in this trigger. It resets @@Identity
-- to be the same as the earlier Scope_Identity() value.
SELECT MyTableID INTO #Trash FROM MyTable WHERE MyTableID = @MyTableID

通常,对审计表的额外插入会破坏所有内容,因为因为它有一个标识列,所以@@Identity 将返回该值,而不是从插入到 MyTable 中的值。但是,最终选择会根据我们之前保存的 Scope_Identity() 创建一个正确的新 @@Identity 值。这也证明了它不受 MyTable 表上任何可能的附加 AFTER 触发器的影响。

更新:

我刚刚注意到这里不需要 INSTEAD OF 触发器。这可以满足您的所有需求:

CREATE TRIGGER dbo.TR_Payments_Insert ON dbo.Payment FOR INSERT
AS 
SET NOCOUNT ON;
IF EXISTS (
   SELECT *
   FROM
      Inserted I
      INNER JOIN dbo.Payment P ON I.CustomerID = P.CustomerID
   WHERE
      I.DateFrom < P.DateTo
      AND P.DateFrom < I.DateTo
) ROLLBACK TRAN;

这当然允许 scope_identity() 继续工作。唯一的缺点是身份表上的回滚插入确实会消耗所使用的身份值(身份值仍会根据插入尝试中的行数递增)。

我已经盯着这个看了几分钟,现在还不能完全确定,但我认为这保留了包含开始时间和排除结束时间的含义。如果结束时间包含在内(这对我来说很奇怪),那么比较需要使用

【讨论】:

这真是太棒了——尽管我不再需要解决这个问题——对于其他人,也可能是未来的我自己,这简直太棒了:) 谢谢,伙计们!几年前,我在为使用 @@Identity 而不是 Scope_Identity 的访问数据项目 (ADP) 中的问题苦恼之后发现了这一点。我非常想使用 INSTEAD OF UPDATE 触发器,这样我就可以将表单绑定到视图并使其可更新。我终于让它工作了! (为了完整起见,请注意这需要 WITH VIEW_METADATA 以防止 Access 查询基础表本身。) 通过创建#Trash,然后设置identity_insert 并使用@MyTableID 进行插入,最后不再次访问原始表可能会提高性能。 仍然不影响调用者中的scope_identity() 用法(在触发器之外)。虽然@@identity 可能是正确的,scope_identity() 仍然返回 null - 这意味着 INSTEAD OF 触发器在添加时仍然是一个重大更改。 (并且需要一个 INSTEAD OF 触发器来通过具有计算列的视图进行更新..) @user2864740 很有趣。什么版本的 SQL Server?并且可以将损坏的代码更改为使用COALESCE(scope_identity(), @@identity)吗?【参考方案4】:

主要问题:触发器和实体框架都在不同的范围内工作。 问题是,如果您在触发器中生成新的 PK 值,则范围不同。因此该命令返回零行,EF 将抛出异常。

解决方案是在触发器末尾添加以下 SELECT 语句:

SELECT * FROM deleted UNION ALL
SELECT * FROM inserted;

在 * 的位置,您可以提及所有列名,包括

SELECT IDENT_CURRENT(‘tablename’) AS <IdentityColumnname>

【讨论】:

【参考方案5】:

就像 araqnid 评论的那样,触发器似乎在满足条件时回滚事务。您可以使用 AFTER INSTERT 触发器更轻松地做到这一点:

CREATE TRIGGER [dbo].[TR_Payments_Insert]
   ON  [dbo].[Payment]
   AFTER INSERT
AS 
BEGIN
    SET NOCOUNT ON;

    IF <Condition>
    BEGIN
        ROLLBACK TRANSACTION
    END
END

然后您可以再次使用 SCOPE_IDENTITY(),因为 INSERT 不再在触发器中完成。

条件本身似乎让两个相同的行过去,如果它们在同一个插入中。使用 AFTER INSERT 触发器,您可以像这样重写条件:

IF EXISTS(
    SELECT *
    FROM dbo.Payment a
    LEFT JOIN dbo.Payment b
        ON a.Id <> b.Id
        AND a.CustomerId = b.CustomerId
        AND (a.DateFrom BETWEEN b.DateFrom AND b.DateTo
        OR a.DateTo BETWEEN b.DateFrom AND b.DateTo)
    WHERE b.Id is NOT NULL)

它会捕获重复的行,因为现在它可以根据 Id 区分它们。如果您删除一行并将其替换为同一语句中的另一行,它也可以工作。

无论如何,如果您需要我的建议,请完全远离触发器。正如你所看到的,即使在这个例子中,它们也非常复杂。通过存储过程执行插入。它们比触发器更简单、更快:

create procedure dbo.InsertPayment
    @DateFrom datetime, @DateTo datetime, @CustomerId int, @AdminId int
as
BEGIN TRANSACTION

IF NOT EXISTS (
    SELECT *
    FROM dbo.Payment
    WHERE CustomerId = @CustomerId
    AND (@DateFrom BETWEEN DateFrom AND DateTo
    OR @DateTo BETWEEN DateFrom AND DateTo))
    BEGIN

    INSERT into dbo.Payment 
    (DateFrom, DateTo, CustomerId, AdminId)
    VALUES (@DateFrom, @DateTo, @CustomerId, @AdminId)

    END
COMMIT TRANSACTION

【讨论】:

感谢您的示例和建议,我已经看过了,我可能会使用您的一些想法 - 但并非所有这些想法都适用于这种情况。另外,是的,我不检查 Id 列,我让数据库为我处理(以及其他一些约束)但是我可能会将触发器移到插入后 - 当我完全看不到逻辑解决方案时创建了我的触发器:) @Andomar,使用 BETWEEN 意味着服务器必须检查四个条件。你可以得到一半的数量。有关详细信息,请参阅***.com/questions/325933/…。 另外,我们能否获得一些关于存储过程比触发器更简单和更快的参考资料?触发器通常更简单,因为它绝对可以强制执行数据完整性规则,但是仅仅拥有一个过程并不能保证它总是被使用(不要告诉我应用程序只使用 SP,总有一天有人会通过后端)。我对更快的位持怀疑态度。你能证明这一点吗? @Emtucifor:您提供的链接假定​​ EndA > StartA,如果检查约束不强制执行,我不会依赖它。强制执行完整性规则的触发器应替换为检查约束(可选地使用 UDF)。触发器是邪恶的,应该被消除。 @Andomar:这是不正确的。没有假设 EndA > StartA。请参阅silentmatt.com/intersection.html 以帮助了解这如何适用于所有范围。其次,您能否提供有关触发器如何总是邪恶并且应该始终被消除的来源或一些学术工作?我同意它们经常被过度使用、误用或写得不好,如果检查或外键约束可以完成工作,它们是不合适的。但是,触发器比 SP 更可靠。很多时候人们说“应用只使用SP”,但有一天,有人直接插入数据。【参考方案6】:

聚会有点晚了,但我自己正在研究这个问题。一种解决方法是在执行插入的调用过程中创建一个临时表,将范围标识从而不是触发器内部插入到该临时表中,然后在插入完成后从临时表中读取标识值.

在程序中:

CREATE table #temp ( id int )

... insert statement ...

select id from #temp
-- (you can add sorting and top 1 selection for extra safety)

drop table #temp

在而不是触发器:

-- this check covers you for any inserts that don't want an identity value returned (and therefore don't provide a temp table)
IF OBJECT_ID('tempdb..#temp') is not null
begin
    insert into #temp(id)
    values
    (SCOPE_IDENTITY())
end

为了安全起见,您可能希望将其称为 #temp 以外的其他名称(足够长且随机的名称,以至于其他人都不会使用它:#temp1234235234563785635)。

【讨论】:

以上是关于而不是 SQL Server 中的触发器丢失 SCOPE_IDENTITY?的主要内容,如果未能解决你的问题,请参考以下文章

SQL Server 中的触发器丢失

sql server而不是使用插入的变量和值触发插入语句

登录到 SQL Server 触发器中的表

MySQL 触发器中的“INSTEAD OF”,从 SQL Server 转换而来

SQL-Server:是不是有相当于一般存储过程执行的触发器

SQL SERVER 触发器