Вместо триггера в SQL Server теряет SCOPE_IDENTITY? - PullRequest
22 голосов
/ 26 мая 2009

У меня есть таблица, в которой я создал триггер 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()

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

Код работал до создания этого триггера. Я использую LINQ to SQL в C #. Я не вижу способа изменить SCOPE_IDENTITY на @@IDENTITY. Как мне сделать эту работу?

Ответы [ 6 ]

17 голосов
/ 26 мая 2009

Используйте @@identity вместо scope_identity().

В то время как scope_identity() возвращает последний созданный идентификатор в текущей области, @@identity возвращает последний созданный идентификатор в текущем сеансе.

Функция scope_identity() обычно рекомендуется над полем @@identity, так как вы обычно не хотите, чтобы триггеры мешали идентификатору, но в этом случае вы делаете.

12 голосов
/ 26 мая 2009

Поскольку вы работаете в SQL 2008, я настоятельно рекомендую использовать предложение OUTPUT вместо одной из пользовательских функций идентификации. В настоящее время у SCOPE_IDENTITY есть некоторые проблемы с параллельными запросами, которые заставляют меня полностью его рекомендовать. @@ Идентичность - нет, но она все же не такая явная и гибкая, как OUTPUT. Plus OUTPUT обрабатывает многорядные вставки. Взгляните на статью BOL , в которой есть несколько замечательных примеров.

8 голосов
/ 06 марта 2010

У меня были серьезные сомнения относительно использования @@ identity, потому что это может вернуть неправильный ответ.

Но есть обходной путь, заставляющий @@ identity иметь значение scope_identity ().

Просто для полноты, сначала я перечислю несколько других способов решения этой проблемы, которые я видел в Интернете:

  1. Заставить триггер возвращать набор строк. Затем в пакете SP, который выполняет вставку, выполните INSERT Table1 EXEC sp_ExecuteSQL ... для еще одной таблицы. Тогда scope_identity () будет работать. Это грязно, потому что требует динамического SQL, что является болью. Также следует помнить, что динамический SQL запускается с разрешениями пользователя, вызывающего SP, а не с разрешениями владельца SP. Если исходный клиент мог вставить в таблицу, он все равно должен иметь это разрешение, просто знайте, что у вас могут возникнуть проблемы, если вы откажете в разрешении на вставку непосредственно в таблицу.

  2. Если есть другой кандидатный ключ, получите идентификатор вставленной строки (строк), используя эти ключи. Например, если имя имеет уникальный индекс, вы можете вставить, а затем выбрать (максимум для нескольких строк) идентификатор из таблицы, которую вы только что добавили, используя имя. Хотя это может иметь проблемы с параллелизмом, если другой сеанс удаляет только что вставленную строку, это не хуже, чем в исходной ситуации, если кто-то удалил вашу строку до того, как приложение сможет ее использовать.

Теперь вот как окончательно сделать ваш триггер безопасным для @@ Identity, чтобы он возвращал правильное значение, , даже если ваш SP или другой триггер вставляется в таблицу, содержащую тождество, после основной вставки .

Кроме того, пожалуйста, добавьте в свой код комментарии о том, что вы делаете и почему, чтобы будущие посетители триггера не ломали вещи и не тратили время на попытки выяснить это.

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. Однако при окончательном выборе создается новое правильное значение @@ Identity, основанное на Scope_Identity (), из которого мы сохранили ранее. Это также защищает от любого возможного дополнительного триггера ПОСЛЕ таблицы MyTable.

Обновление:

Я только что заметил, что триггер 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 () продолжать работать. Единственным недостатком является то, что откатная вставка в таблице идентификаторов действительно использует использованные значения идентификаторов (значение идентификатора все еще увеличивается на число строк в попытке вставки).

Я смотрел на это несколько минут и сейчас не уверен в этом, но я думаю, это сохраняет значение инклюзивного времени начала и эксклюзивного времени окончания. Если бы время окончания было включительно (что было бы странно для меня), тогда для сравнения нужно было бы использовать <= вместо <. </p>

2 голосов
/ 27 декабря 2017

Основная проблема: Trigger и Entity Framework работают в разных областях. Проблема в том, что если вы генерируете новое значение PK в триггере, это будет другой областью действия. Таким образом, эта команда возвращает ноль строк и EF выдаст исключение.

Решение заключается в добавлении следующего оператора SELECT в конце вашего триггера:

SELECT * FROM deleted UNION ALL
SELECT * FROM inserted;

вместо * вы можете указать все имена столбцов, включая

SELECT IDENT_CURRENT(‘tablename’) AS <IdentityColumnname>
1 голос
/ 11 июля 2013

Немного опоздал на вечеринку, но сам разбирался в этом вопросе. Обходной путь - создать временную таблицу в вызывающей процедуре, в которой выполняется вставка, вставить идентификатор области в эту временную таблицу изнутри вместо триггера, а затем прочитать значение идентификатора из временной таблицы после завершения вставки .

В процедуре:

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

1 голос
/ 26 мая 2009

Как прокомментировал 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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...