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

时间:2009-05-25 22:25:32

标签: sql-server tsql linq-to-sql sql-server-2008

我有一个表格,我在其中创建了一个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的方法。我如何使这项工作?

6 个答案:

答案 0 :(得分:17)

使用@@identity代替scope_identity()

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

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

答案 1 :(得分:12)

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

答案 2 :(得分:8)

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

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

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

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

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

  3. 现在,这里是如何明确地使你的触发器安全,@@ Identity可以返回正确的值,即使你的SP或其他触发器在主插入后插入到身份承载表。 / p>

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

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

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

答案 3 :(得分:2)

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

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

SELECT * FROM deleted UNION ALL
SELECT * FROM inserted;

代替*,您可以提及所有列名称,包括

SELECT IDENT_CURRENT(‘tablename’) AS <IdentityColumnname>

答案 4 :(得分:1)

与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

答案 5 :(得分:1)

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

在程序中:

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)。