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

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了而不是SQL Server中的触发器丢失SCOPE_IDENTITY?相关的知识,希望对你有一定的参考价值。

我有一个表格,我创建了一个INSTEAD OF触发器来强制执行一些业务规则。

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

Insert + Scope code

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

SELECT SCOPE_IDENTITY()

Trigger:

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的方法。我该如何工作?

答案

使用@@identity而不是scope_identity()

scope_identity()返回当前范围中最后创建的id时,@@identity返回当前会话中最后创建的id。

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

另一答案

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

另一答案

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

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

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

  1. 使触发器返回行集。然后,在执行插入的包装器SP中,将INSERT Table1 EXEC sp_ExecuteSQL ...执行到另一个表。然后scope_identity()将起作用。这很麻烦,因为它需要动态SQL,这很痛苦。此外,请注意,动态SQL在调用SP的用户的权限下运行,而不是SP所有者的权限。如果原始客户端可以插入到表中,他仍然应该具有该权限,只要知道如果拒绝直接插入表的权限就可能遇到问题。
  2. 如果存在另一个候选键,则使用这些键获取插入行的标识。例如,如果Name上有唯一索引,则可以插入,然后使用Name从刚刚插入的表中选择(多行多行)ID。虽然如果另一个会话删除您刚刚插入的行,这可能会出现并发问题,但如果有人在应用程序使用它之前删除了您的行,则不会比原始情况更糟糕。

现在,即使您的SP或其他触发器在主插入后插入到身份承载表中,也可以确定如何确保触发器安全,以便@@ Identity能够返回正确的值。

另外,请在您的代码中添加关于您正在做什么的评论以及为什么未来的触发器访问者不会破坏事物或浪费时间来解决问题。

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()继续工作。唯一的缺点是标识表上的回滚插入确实使用了所使用的标识值(标识值仍然增加了插入尝试中的行数)。

我一直盯着这几分钟并且现在没有绝对的确定性,但我认为这保留了包容性开始时间和独家结束时间的含义。如果结束时间是包容性的(这对我来说很奇怪),则比较需要使用<=而不是<。

另一答案

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

解决方案是在Trigger的末尾添加以下SELECT语句:

SELECT * FROM deleted UNION ALL
SELECT * FROM inserted;

代替*你可以提到所有的列名,包括

SELECT IDENT_CURRENT(‘tablename’) AS <IdentityColumnname>
另一答案

像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
另一答案

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

在程序中:

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:是不是有相当于一般存储过程执行的触发器

相当于mySQL中的SQL Server函数SCOPE_IDENTITY()?