Вместо триггера в SQL Server теряет SCOPE_IDENTITY?

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

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


person kastermester    schedule 25.05.2009    source источник
comment
Я так понимаю, эта таблица работала до того, как вы вставили оператор INSTEAD OF? Вы проверили поле первичного ключа, чтобы убедиться, что оно имеет спецификацию удостоверения?   -  person Robert Harvey    schedule 26.05.2009
comment
Да, и да, это действительно так (теперь вы можете увидеть код — он вставляет строку и получает автоматическую идентификацию при вставке).   -  person kastermester    schedule 26.05.2009
comment
Почему бы не использовать триггер BEFORE INSERT, который выдает ошибку, если правила не выполняются, а не INSTEAD OF?   -  person araqnid    schedule 26.05.2009
comment
@araqnid - потому что, насколько я мог видеть, SQL Server не имеет такой вещи - хотя я должен признать, что я не пробовал ее напрямую, но, основываясь на поиске в Google, может быть, конечно, что я нашли недействительные или устаревшие ресурсы - есть ли такое на самом деле?   -  person kastermester    schedule 26.05.2009
comment
Мы использовали такие триггеры в SQL Server 2000 - это, кажется, режим по умолчанию, просто объявляя триггер как триггер создания TG_xxx на dbo.tablename для вставки, обновления как ..... У нас есть логика для проверки условий и вызова raiserror (), затем выполните транзакцию отката, если проверка не удалась. Сказав это, мы используем SP для создания идентификаторов, а не идентификаторов - возможно, поэтому парень, который настаивал на том, чтобы мы делали это таким образом, был немного расплывчатым в отношении рассуждений. Но затем он выработал привычку до того, как была реализована функция scope_identity().   -  person araqnid    schedule 26.05.2009
comment
Пожалуйста, рассмотрите предложение Аарона Альтона использовать вывод для получения значений indetity.   -  person HLGEM    schedule 26.05.2009
comment
araqnid, ваш метод гораздо опаснее для целостности данных, чем использование идентификационных полей. Вы можете создать настоящие проблемы с одновременными вставками, если не будете очень осторожны.   -  person HLGEM    schedule 26.05.2009
comment
Да, SP генерации идентификатора выполняет дополнительную работу, чтобы монопольно заблокировать таблицу, содержащую следующий идентификатор. Что само по себе вызывает проблемы с конкуренцией. Я тоже не фанат этого (и я определенно этого не писал и не рекомендую).   -  person araqnid    schedule 27.05.2009


Ответы (6)


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

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

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

person Guffa    schedule 25.05.2009
comment
После того, как я добавил еще немного контента в свой пост, вы можете видеть, что я использую LINQ to SQL в своем приложении .Net, поэтому, насколько я вижу, у меня действительно нет особого выбора, за исключением, возможно, использования sproc для вставки данные. - person kastermester; 26.05.2009
comment
У меня есть серьезные сомнения по поводу использования @@identity, но я думаю, что в этом узком случае это нормально, поскольку другие обходные пути на самом деле не намного лучше, и, по крайней мере, вы знаете, что это даст правильное значение, если триггер не вставляется в другой стол тем временем. - person ErikE; 06.03.2010
comment
@Emtucifor: в большинстве случаев вам нужно, что возвращает scope_identity(), но не в этом случае. На этот раз правильным выбором будет @@identity. Причина существования обоих заключается в том, что иногда вам действительно нужен необычный результат. - person Guffa; 06.03.2010
comment
Гуффа, вам нужен @@identity только потому, что scope_identity() не выполняет функцию, которую мы все ожидаем: вы вставляете в таблицу, в которой есть столбец идентификаторов, а затем хотите вернуть значение ЭТОГО столбца идентификаторов. Предполагается, что Scope_Identity() защищает вас от необходимости искать, а затем тщательно проверять любые триггеры в таблице, чтобы убедиться, что они не вставляются в другую таблицу со столбцом идентификаторов. Но неограниченная рекомендация, которую вы сделали для использования @@identity, сломается в тот момент, когда в таблице появится еще один афтер-триггер или вставка вместо триггера во вторую таблицу. - person ErikE; 07.03.2010
comment
Смотрите ответ, который я предоставил для получения дополнительной информации об этом. - person ErikE; 07.03.2010
comment
Я понимаю, что вы не можете предположить, что при вставке в таблицу с триггером INSTEAD OF строки вообще будут добавлены в таблицу. Их можно было поместить в другую таблицу или во многие другие таблицы или вообще никуда не вставлять. Однако вызывающий скрипт этого не знает и не должен этого знать. Смысл триггера INSTEAD OF состоит в том, чтобы сделать запутанные кишки базовой операции с данными прозрачными для клиента, выполняющего вставку. На мой взгляд, должен быть какой-то способ явно установить scope_identity в триггере INSTEAD OF. - person ErikE; 07.03.2010

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

person Aaron Alton    schedule 26.05.2009
comment
какие проблемы с параллельными запросами? они находятся в другой сфере, поэтому я не вижу, где они могут быть проблемой. ident_current имеет проблемы с параллельными вставками, а @@identity имеет проблемы со вставками из триггеров, поэтому scope_identity был предпочтительным методом до изобретения вывода. Однако совет использовать Output очень хорош. - person HLGEM; 26.05.2009
comment
Извините, мне нужно было опубликовать ссылку для подключения: connect. microsoft.com/SQLServer/feedback/ - person Aaron Alton; 27.05.2009
comment
Вопрос о параллельных запросах хорош. Но @@identity отлично справляется с многострочными вставками: INSERT TheTable ... | SELECT @LastID = Scope_Identity(), @Rows = @@RowCount | SELECT FROM TheTable WHERE ID BETWEEN @LastID - @Rows + 1 AND @LastID - person ErikE; 07.03.2010
comment
Кроме того, предложение OUTPUT всегда будет возвращать 0 для столбцов идентификаторов, если в таблице существует триггер INSTEAD OF INSERT. - person Nathan; 20.12.2012
comment
Для тех, кто никогда не слышал или не использовал предложение OUTPUT, вот тема, в которой объясняется, как его использовать: stackoverflow.com/questions/10999396/ - person Parth Shah; 31.07.2014

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

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

person ErikE    schedule 06.03.2010
comment
Это действительно гениально - хотя мне больше не нужно решение для этого - для других и, возможно, для меня в будущем это просто потрясающе :) - person kastermester; 07.03.2010
comment
Спасибо, парни! Я понял это несколько лет назад после мучительных проблем в проекте доступа к данным (ADP), который использует @@Identity вместо Scope_Identity. Я отчаянно хотел использовать триггер INSTEAD OF UPDATE, чтобы привязать формы к представлению и сделать его обновляемым. У меня наконец-то заработало! (Для полноты обратите внимание, что для этого требуется WITH VIEW_METADATA, чтобы Access не запрашивал сами базовые таблицы.) - person ErikE; 09.03.2010
comment
Возможно, можно улучшить производительность, если в конце снова не обращаться к исходной таблице, создав #Trash, затем установив identity_insert и выполнив вставку с @MyTableID. - person ErikE; 09.03.2010
comment
По-прежнему не влияет на использование scope_identity() в вызывающем объекте (вне триггера). Хотя @@identity может быть правильным, scope_identity() по-прежнему возвращает значение NULL. Это означает, что триггер INSTEAD OF по-прежнему является критическим изменением при добавлении. (И триггер INSTEAD OF требуется для обновления через представление с вычисляемыми столбцами..) - person user2864740; 02.01.2021
comment
@ user2864740 Интересно. Какая версия SQL Server? И можно ли изменить код, который сломался, чтобы использовать COALESCE(scope_identity(), @@identity)? - person ErikE; 04.01.2021
comment
SQL Server 2014. К сожалению, в моем конкретном случае также используется репликация слиянием, которая, как известно, негативно влияет на @@identity. Текущее ожидаемое изменение заключается в переключении на использование базовой таблицы для вставки. В основном я немного сварлив. TSQL не предоставляет метод для «сброса» scope_identity вызывающего абонента, когда действует триггер вместо, и нельзя использовать OUTPUT для получения идентификатора, когда есть триггер вместо. лучшим изменением в коде, с которым я имею дело, было бы, вероятно, перейти к вставке SP и не беспокоиться о триггере представления. Время и время. Всегда проблемы со старым грубым кодом. - person user2864740; 04.01.2021

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

Решение состоит в том, чтобы добавить следующий оператор SELECT в конец триггера:

SELECT * FROM deleted UNION ALL
SELECT * FROM inserted;

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

SELECT IDENT_CURRENT(‘tablename’) AS <IdentityColumnname>
person Ashish Mishra    schedule 27.12.2017

Как прокомментировал 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)

И он будет ловить повторяющиеся строки, потому что теперь он может различать их на основе идентификатора. Это также работает, если вы удаляете строку и заменяете ее другой строкой в ​​том же операторе.

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

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
person Andomar    schedule 26.05.2009
comment
Спасибо за примеры и советы, я ознакомился и, возможно, воспользуюсь некоторыми вашими идеями, но не все из них применимы в данном случае. Кроме того, да, я не проверяю столбец Id, я позволяю БД обрабатывать это для меня (вместе с парой других ограничений), однако я, вероятно, перенесу свой триггер после вставки - я совершенно не видел логического решения, когда я создал свой триггер :) - person kastermester; 26.05.2009
comment
@Andomar, использование BETWEEN означает, что сервер должен проверить четыре условия. Вы можете обойтись половиной этого количества. См. stackoverflow.com/questions /325933/ для более подробной информации. - person ErikE; 09.03.2010
comment
Кроме того, можем ли мы получить некоторые ссылки на то, что хранимые процедуры ПРОЩЕ и БЫСТРЕЕ, чем триггеры? Триггер часто проще, потому что он может абсолютно обеспечить соблюдение правил целостности данных, но само наличие процедуры не гарантирует, что она всегда будет использоваться (не говорите мне, что приложение использует только SP, когда-нибудь кто-нибудь вставит несколько строк куда-нибудь через задний конец). И я скептически отношусь к более быстрому биту. Вы можете это доказать? - person ErikE; 09.03.2010
comment
@Emtucifor: ссылка, которую вы даете, предполагает EndA › StartA, на которую я бы не стал полагаться, если бы она не применялась ограничениями проверки. Триггер, обеспечивающий соблюдение правил целостности, должен быть заменен проверочным ограничением (необязательно с UDF). Триггеры — это зло, и их следует устранить. - person Andomar; 09.03.2010
comment
@Андомар: это неправильно. Нет предположения, что EndA › StartA. См. silentmatt.com/intersection.html, чтобы наглядно представить, как это работает для ВСЕХ диапазонов. Во-вторых, не могли бы вы предоставить источник или какую-нибудь научную работу о том, что триггеры всегда вредны и их всегда следует устранять? Я согласен, что ими часто злоупотребляют, применяют неправильно или плохо пишут, и если проверка или ограничение внешнего ключа могут выполнять свою работу, они неуместны. Однако триггеры более надежны, чем SP. Очень часто люди говорят, что приложение использует только SP, но однажды кто-то вставляет данные напрямую. - person ErikE; 10.03.2010
comment
@Emtucifor: например, StartA = 2006, EndA = 2004, StartB = 2005, EndB = 2007 не совпадают, но перекрываются. Научная работа слишком далека от коммерческой реальности, чтобы дать осмысленные идеи. Проблема триггеров не в надежности, а в сложности. Ни одно приложение не сможет вставлять данные непосредственно в таблицы, которые я обслуживаю. - person Andomar; 11.03.2010
comment
@Андомар: мои извинения. Я пропустил, что оба были A в ваших сравнениях. Однако теперь я вижу еще одну проблему с вашим запросом: если DateFrom и DateTo полностью находятся внутри @DateFrom и @DateTo, ваш запрос завершится ошибкой! Что касается @DateFrom и DateTo в обратном порядке, сначала исправьте это с помощью некоторых операторов SET (поэтому я все еще думаю, что ваш запрос нуждается в некотором обновлении). Что касается другого момента, я дважды сказал, что это не приложение будет вставлять напрямую в таблицы, а человек. Что касается заявления о том, что научная работа не может дать осмысленного понимания... хм, хорошо. Теперь я перестану метать бисер. :) - person ErikE; 11.03.2010
comment
Просто для ясности: сделайте несколько операторов присваивания, чтобы сделать @DateFrom ‹ @DateTo, а затем вы можете использовать два условия вместо 4 в своем запросе, что должно значительно повысить производительность, особенно с индексом на одну из дат (и ТАКЖЕ получить случай, когда один диапазон полностью охватывает другой, которого сейчас нет в вашем запросе). - person ErikE; 11.03.2010
comment
@Emtucifor: запросы на обновление — это просто быстрое решение; они не будут препятствовать новым записям. Что касается вашего комментария о том, что диапазон полностью находится в пределах диапазона, я предлагаю вам прочитать вопрос, прежде чем комментировать ответы;) - person Andomar; 11.03.2010
comment
Обновить запросы? Я про них ничего не говорил. Я имел в виду операторы SET исключительно для переменных @DateFrom и @DateTo. Если вы имеете в виду, что столбцы DateFrom и DateTo могут быть в обратном хронологическом порядке, тогда ваш запрос также не будет выполнен. Наконец, вы имеете в виду, что, поскольку исходный постер не проверял полностью закрытые диапазоны, это истинное требование? Интересно, это ошибка? - person ErikE; 11.03.2010

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

В процедуре:

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

person Ryan    schedule 11.07.2013