Как реализовать условную хранимую процедуру Upsert?

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

По сути, я пытаюсь синхронизировать некоторые данные между разными репозиториями, и функция Upsert мне показалась подходящей. Таким образом, во многом основано на ответе Сэма Шафрана на этот вопрос , а также некоторые другие исследования и чтения, я придумал эту хранимую процедуру:

(примечание: я использую MS SQL Server 2005, поэтому оператор MERGE не подходит)

CREATE PROCEDURE [dbo].[usp_UpsertItem] 
    -- Add the parameters for the stored procedure here
    @pContentID varchar(30) = null, 
    @pTitle varchar(255) = null,
    @pTeaser varchar(255) = null 
AS
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;

    BEGIN TRANSACTION

        UPDATE dbo.Item WITH (SERIALIZABLE)
        SET Title = @pTitle,
            Teaser = @pTeaser
        WHERE ContentID = @pContentID

        IF @@rowcount = 0
            INSERT INTO dbo.Item (ContentID, Title, Teaser)
            VALUES (@pContentID, @pTitle, @pTeaser)

    COMMIT TRANSACTION
END

Мне это удобно для базового Upsert, но я бы хотел, чтобы фактическое обновление зависело от значения другого столбца. Думайте об этом как о «блокировке» строки, чтобы процедура Upsert не могла производить дальнейшие обновления. Я мог бы изменить оператор UPDATE так:

UPDATE dbo.Item WITH (SERIALIZABLE)
SET Title = @pTitle,
    Teaser = @pTeaser
WHERE ContentID = @pContentID
AND RowLocked = false

Но тогда последующая вставка завершится ошибкой с нарушением уникального ограничения (для поля ContentID), когда она попытается вставить строку, которая уже существует, но не была обновлена, потому что она была «заблокирована».

Значит ли это, что у меня больше нет классического Upsert, т.е. что мне придется каждый раз выбирать строку, чтобы определять, можно ли ее обновить или вставить? Бьюсь об заклад, что это так, поэтому я предполагаю, что на самом деле я прошу помочь установить правильный уровень изоляции транзакции, чтобы процедура выполнялась безопасно.


person Matt    schedule 09.07.2009    source источник
comment
Что такое RowLocked (И RowLocked = false)? Это столбец в вашей таблице?   -  person A-K    schedule 10.07.2009
comment
@AlexKuznetsov - Да, RowLocked должен быть столбцом таблицы; на самом деле есть пара столбцов, которые определяют, следует ли блокировать строку (т.е. не обновлять ее с помощью этой процедуры), но я упростил свой SQL, чтобы попытаться прояснить свой вопрос; немного небрежно с синтаксисом - конечно, он должен быть AND RowLocked = 0, и я должен был упомянуть, что это битовый столбец.   -  person Matt    schedule 10.07.2009


Ответы (5)


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

/*
CREATE TABLE Item
 (
   Title      varchar(255)  not null
  ,Teaser     varchar(255)  not null
  ,ContentId  varchar(30)  not null
  ,RowLocked  bit  not null
)


UPDATE item
 set RowLocked = 1
 where ContentId = 'Test01'

*/


DECLARE
  @Check varchar(30)
 ,@pContentID varchar(30)
 ,@pTitle varchar(255)
 ,@pTeaser varchar(255)

set @pContentID = 'Test01'
set @pTitle     = 'TestingTitle'
set @pTeaser    = 'TestingTeasier'

set @check = null

UPDATE dbo.Item
 set
   @Check = ContentId
  ,Title  = @pTitle
  ,Teaser = @pTeaser
 where ContentID = @pContentID
  and RowLocked = 0

print isnull(@check, '<check is null>')

IF @Check is null
    INSERT dbo.Item (ContentID, Title, Teaser, RowLocked)
     values (@pContentID, @pTitle, @pTeaser, 0)

select * from Item

Хитрость здесь в том, что вы можете устанавливать значения в локальных переменных в операторе Update. Выше значение «флага» устанавливается только в том случае, если обновление работает (т. Е. Выполняются критерии обновления); в противном случае он не будет изменен (здесь, оставлен равным нулю), вы можете проверить это и обработать соответствующим образом.

Что касается транзакции и ее сериализации, я хотел бы узнать больше о том, что должно быть инкапсулировано в транзакции, прежде чем предлагать, как действовать.

- Дополнение, продолжение второго комментария ниже -----------

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

Я провел еще несколько тестов (добавил ограничение первичного ключа для столбца ContentId, заключил UPDATE и INSERT в транзакцию, добавил сериализуемую подсказку к обновлению) и да, это должно сделать все, что вы хотите. Неудачное обновление приводит к блокировке диапазона в этой части индекса, и это блокирует любые одновременные попытки вставить это новое значение в столбец. Конечно, если N запросов отправлены одновременно, «первый» создаст строку, и она будет немедленно обновлена ​​вторым, третьим и т. Д. - если вы не установите «блокировку» где-нибудь вдоль строки. Хорошая уловка!

(Обратите внимание, что без индекса в ключевом столбце вы заблокируете всю таблицу. Кроме того, блокировка диапазона может заблокировать строки «по обе стороны» от нового значения - или, может быть, они этого не сделают, я не проверить это. Не имеет значения, поскольку продолжительность операции должна [?] быть в миллисекундах с однозначным числом.)

person Philip Kelley    schedule 09.07.2009
comment
Следует отметить, что в исходном примере кода вы обновляете элемент таблицы, но вставляете ее в таблицу MailItem; Разве апсерты не должны применяться к одной и той же таблице? - person Philip Kelley; 10.07.2009
comment
Несоответствующие имена таблиц - это опечатка (теперь исправлено). Я знал, что вы можете установить локальную переменную с помощью SELECT, но я никогда не пробовал это с помощью UPDATE, так что это могло просто помочь. Что касается сериализуемой транзакции, мое (по общему признанию несовершенное) понимание состоит в том, что если вы не используете какую-то блокировку, вы можете получить нарушения ограничений уникального ключа, и что UPDATE с (сериализуемым) соответствующим образом делает это без взаимоблокировок. Я работаю с примером в связанном вопросе (выше) и все еще читаю / пытаюсь убедиться, что я точно понимаю, что это делает. - person Matt; 10.07.2009
comment
Обновил свой ответ с учетом отзыва о приведенном выше комментарии. - person Philip Kelley; 10.07.2009
comment
@Philip, спасибо за обновление и простой язык о подсказках и блокировках. Однако, когда я тестировал, я понял, что ваш подход с использованием IF @Check is null функционально эквивалентен исходному IF @@ rowcount = 0, потому что @Check будет иметь значение только в случае обновления строки. Когда строка существует, но не обновляется, @check имеет значение null, и я получаю уникальное нарушение ограничения от последующей вставки. Итак, ваш код работает так, как описано, это просто не решает мою проблему;) Тем не менее, это помогло, так что +1. - person Matt; 10.07.2009

Вы можете изменить порядок обновления / вставки. Таким образом, вы выполняете вставку в try / catch, и если вы получаете нарушение ограничения, выполняйте обновление. Хотя он кажется немного грязным.

person Steve Weet    schedule 09.07.2009
comment
Я всегда думал, что вы не должны полагаться на обработчики ошибок для нормальной обработки, т.е. если я знаю, что типичный вариант использования вызовет исключение, тогда я должен проверить это условие и обработать его, прежде чем оно вызовет исключение. Так что я согласен, это действительно кажется немного грязным;) Если я могу правильно определить уровень изоляции (я все еще читаю), тогда логика довольно проста - но я теряю исходное преимущество upsert (то есть без дополнительного чтения БД) . - person Matt; 10.07.2009

СОЗДАТЬ ПРОЦЕДУРУ [dbo]. [Usp_UpsertItem] - добавьте сюда параметры для хранимой процедуры @pContentID varchar (30) = null, @pTitle varchar (255) = null, @pTeaser varchar (255) = null КАК НАЧАТЬ - УСТАНОВИТЬ NOCOUNT ON добавлен, чтобы дополнительные наборы результатов не мешали операторам SELECT. УСТАНОВИТЬ NOCOUNT ON;

BEGIN TRANSACTION
    IF EXISTS (SELECT 1 FROM dbo.Item WHERE ContentID = @pContentID
             AND RowLocked = false)
       UPDATE dbo.Item 
       SET Title = @pTitle, Teaser = @pTeaser
       WHERE ContentID = @pContentID
             AND RowLocked = false
    ELSE IF NOT EXISTS (SELECT 1 FROM dbo.Item WHERE ContentID = @pContentID)
            INSERT INTO dbo.Item (ContentID, Title, Teaser)
            VALUES (@pContentID, @pTitle, @pTeaser)

COMMIT TRANSACTION

КОНЕЦ

person JNappi    schedule 10.07.2009
comment
Ужасный ужасный код! Вы не только дважды выполняете запрос для одной и той же условной операции, но и используете else, если не существует, когда подойдет простое else. См. Ответ CptSkippy для лучшего примера. - person Chris; 10.07.2009
comment
Я согласен, что другое решение чище, но ужасно, ужасно ... Я указывал в правильном направлении, не так ли? - person JNappi; 10.07.2009

Я бы отказался от сделки.

Кроме того, @@ rowcount, вероятно, будет работать, но использование глобальных переменных в качестве условной проверки приведет к ошибкам.

Просто сделайте проверку Exists (). В любом случае вы должны пройти через стол, поэтому проблема не в скорости.

Насколько я понимаю, в транзакции нет необходимости.

person Andrew    schedule 16.09.2013
comment
Шаблон обновления / вставки с rowcount безопасен только потому, что он использует сериализуемый для блокировки до вставки. В противном случае вставка может конфликтовать с одновременной попыткой обновления, которая также не соответствует ни одной строке, а затем двойная вставка вызовет повторяющиеся строки или ошибку повторяющегося ключа, если у вас есть уникальные ключи. - person Jeremiah Gowdy; 21.03.2014

person    schedule
comment
Что такое RowLocked (И RowLocked = false)? Это столбец в вашей таблице? - person A-K; 10.07.2009
comment
Я провел стресс-тестирование того, что я понял как ваш подход, и он не выдерживает высокого уровня параллелизма. - person A-K; 10.07.2009