Как вставить / обновить, если запись не существует / существует с SQL Server 2008? - PullRequest
0 голосов
/ 27 апреля 2018

Мне интересно, как лучше всего проверить, существует ли запись, а затем запустить оператор обновления или проверить, не существует ли запись, а затем запустить оператор вставки? Причина, по которой мне нужно if not exists, заключается в том, что я вставляю информацию об учетной записи в таблицу. В ситуации, когда мы должны обновить, я просто ищу уникальный идентификатор. В другом случае для вставки я должен убедиться, что email или username не существует в таблице. Вот пример моего запроса:

<cfset var isUser = structKeyExists(FORM, "frmSaveaccount_isuser") ? true : false>
<cfset var isStaff = structKeyExists(FORM, "frmSaveaccount_isstaff") ? true : false>

<cftransaction action="begin">
    <cftry>
         <cfquery name="saveAccount" datasource="#Application.dsn#">
            DECLARE @AccountID UNIQUEIDENTIFIER = CASE WHEN LEN('#FORM.frm_accountid#') <> 0 THEN <cfqueryparam cfsqltype="cf_sql_idstamp" value="#trim(FORM.frm_accountid)#"> ELSE NEWID() END;
            DECLARE @FirstName VARCHAR(50) = <cfqueryparam cfsqltype="cf_sql_varchar" maxlength="50" value="#trim(FORM.frm_firstname)#">;
            DECLARE @LastName VARCHAR(50) = <cfqueryparam cfsqltype="cf_sql_varchar" maxlength="50" value="#trim(FORM.frm_lastname)#">;
            DECLARE @Middle CHAR(1) = <cfqueryparam cfsqltype="cf_sql_char" maxlength="1" value="#FORM.frm_middle#" null="#!len(trim(FORM.frmSaveaccount_middle))#">;
            DECLARE @Email VARCHAR(80) = <cfqueryparam cfsqltype="cf_sql_varchar" maxlength="80" value="#trim(FORM.frm_email)#">;
            <cfif isUser>
                DECLARE @IsUser BIT = <cfqueryparam cfsqltype="cf_sql_bit" maxlength="1" value="#trim(structKeyExists(FORM, 'frm_isuser')? 1:0)#">;
                DECLARE @ActiveUser BIT = <cfqueryparam cfsqltype="cf_sql_bit" maxlength="1" value="#trim(structKeyExists(FORM, 'frm_activeuser')? 1:0)#">;
                DECLARE @SystemAdmin BIT = <cfqueryparam cfsqltype="cf_sql_bit" maxlength="1" value="#trim(structKeyExists(FORM, 'frm_systemadmin')? 1:0)#">;
                DECLARE @UserName VARCHAR(50) = <cfqueryparam cfsqltype="cf_sql_varchar" maxlength="50" value="#trim(FORM.frm_username)#">;
            </cfif>
            <cfif isStaff>
                DECLARE @IsStaff BIT = <cfqueryparam cfsqltype="cf_sql_bit" maxlength="1" value="#trim(structKeyExists(FORM, 'frm_isstaff')? 1:0)#">;
                DECLARE @ActiveStaff BIT = <cfqueryparam cfsqltype="cf_sql_bit" maxlength="1" value="#trim(structKeyExists(FORM, 'frm_activestaff')? 1:0)#">;
                DECLARE @Position VARCHAR(10) = <cfqueryparam cfsqltype="cf_sql_varchar" maxlength="10" value="#trim(FORM.frm_positioncode)#" null="#!len(trim(FORM.frm_positioncode))#">;
            </cfif>
            DECLARE @ActionDate DATETIME = CURRENT_TIMESTAMP;
            DECLARE @ActionID UNIQUEIDENTIFIER = <cfqueryparam cfsqltype="cf_sql_idstamp" value="#AccountID#">;

            BEGIN TRAN
                IF EXISTS (SELECT AccountID FROM Accounts WITH (updlock,serializable) WHERE AccountID = @AccountID)
                BEGIN
                    UPDATE Accounts 
                    SET
                        FirstName = @FirstName,
                        LastName = @LastName,
                        Middle = @Middle,
                        Email = @Email,
                        <cfif isUser>
                            IsUser = @IsUser,
                            ActiveUser = @ActiveUser,
                            SystemAdmin = @SystemAdmin,
                            UserName = @UserName,
                        </cfif>
                        <cfif isStaff>
                            IsStaff = @IsStaff,
                            ActiveStaff = @ActiveStaff,
                            Position = @Position,
                        </cfif>
                        ActionDate = @ActionDate,
                        ActionID = @ActionID
                    WHERE AccountID = @AccountID
                    SELECT @AccountID AS RecID
                END
                ELSE
                BEGIN
                    IF NOT EXISTS(SELECT 1 FROM Accounts WHERE Email = @Email <cfif isUser> OR UserName = @UserName</cfif>)
                        INSERT INTO Accounts (
                            AccountID,FirstName,LastName,Middle,Email,
                        <cfif isUser>
                            IsUser,ActiveUser,SystemAdmin,UserName,
                        </cfif>
                        <cfif isStaff>
                            IsStaff,ActiveStaff,Position,
                        </cfif>
                            ActionDate,ActionID
                        ) VALUES (
                            @AccountID,@FirstName,@LastName,@Middle,@Email,
                        <cfif isUser>
                            @IsUser,@ActiveUser,@SystemAdmin,@UserName,
                        </cfif>
                        <cfif isStaff>
                            @IsStaff,@ActiveStaff,@Position,
                        </cfif>
                            @ActionDate,@ActionID
                        )
                        SELECT @AccountID AS RecID
                    END
                    COMMIT TRAN
                </cfquery>
                <cfcatch type="any">
                     <cftransaction action="rollback" />
                     <cfset var fnResults.status = "400">
                     <cfset var fnResults.message = "Error! Please contact your administrator.">
                </cfcatch>
             </cftry>
    </cftransaction>

Мне интересно, если это лучший подход, чем разделить на два отдельных запроса вставки / обновления? Также есть ли лучший способ проверить, существует ли запись / не существует?

Ответы [ 3 ]

0 голосов
/ 27 апреля 2018

MERGE может быть чем-то вроде

MERGE Accounts tgt
USING ( SELECT 
            AccountID = 42 
          , firstName = 'Ted'
          , userName = 'ted'
          , email = 'ted@logan.com'
) src (AccountID, firstName, userName, email)
  ON tgt.accountID = src.accountID
WHEN MATCHED 
THEN 
  UPDATE
  SET FirstName = src.firstName
WHEN NOT MATCHED
  /* Check if username or email is already used */
  AND (SELECT 1 FROM Accounts WHERE username = src.username OR email = src.email) IS NULL
THEN 
  INSERT ( accountID, firstName, email, userName )
  VALUES ( src.AccountID, src.firstName, src.email, src.username )
OUTPUT $action, inserted.AccountID
;

Как я сказал выше, я не уверен, может ли cfquery правильно интерпретировать оператор MERGE. Вы должны будете проверить это. Возможно, вам придется сделать это вызовом хранимой процедуры.

OUTPUT должен возвращать тип операций (INSERT или UPDATE) и связанный AccountID.

РЕДАКТИРОВАТЬ: я создал Fiddle, чтобы продемонстрировать некоторые из различных применений, которые вы пытаетесь здесь.

https://dbfiddle.uk/?rdbms=sqlserver_2012&fiddle=710ea9d801637c17c88f27cac165a8f5

Хотя, если честно, чем больше я думал об этом, тем больше я думал, что MERGE больше предназначен для массовых загрузок данных. Вышеуказанный метод работает, но это одна строка. Может оказаться более эффективным просто проверить наличие запрошенной записи, а затем INSERT или UPDATE при необходимости. A MERGE может быть излишним.

0 голосов
/ 28 апреля 2018

(слишком долго для комментариев ...)

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

Хотя использование cfquery допустимо, лично я бы вместо этого использовал хранимую процедуру. Процедуры лучше справляются со сложным SQL, а также упростят использование преимуществ sql NULL и / или значений по умолчанию для упрощения логики.

Вместо набора cfif/cfelse операторов, разбросанных по всему SQL, создайте хранимую процедуру со всеми необходимыми переменными. Назначьте любые значения по умолчанию для необязательных параметров. NULL обычно является хорошим выбором, поскольку позволяет легко обнаруживать пропущенные параметры и заменять различные значения на ISNULL() или COALESCE(), но ... все зависит от бизнес-логики вашего приложения.

Подпись хранимой процедуры

CREATE PROCEDURE [dbo].[YourProcedureNameHere]
    @AccountID UNIQUEIDENTIFIER
    , @FirstName VARCHAR(50)
    , @LastName VARCHAR(50)
    , @Middle CHAR(1)
    , @Email VARCHAR(80)
    , @IsUser BIT = NULL
    , @ActiveUser BIT = NULL
    , @SystemAdmin BIT = NULL
    , @UserName VARCHAR(50) = NULL 
    , @IsStaff BIT = NULL
    , @ActiveStaff BIT = NULL
    , @Position VARCHAR(10) = NULL 
AS 
BEGIN
      ... your sql logic ....
END
GO

Затем вызовите процедуру из CF, используя один cfif, чтобы условно передать соответствующие переменные для «пользователя» или «персонала». Какой бы набор переменных не был опущен (настройки пользователя или персонала), внутри хранимой процедуры будут назначены значения по умолчанию.

Несколько других предложений о коде CF

  • Как сказал @Shawn, большинство троичных операторов не нужны. Такие функции, как structKeyExists(), уже возвращают логическое значение. Нет необходимости делать что-либо дополнительно, например trim () и т. Д., Чтобы использовать результат со столбцом CF_SQL_BIT. Он конвертируется автоматически.

  • Транзакции предназначены для нескольких заявлений. Поскольку UPDATE и INSERT являются атомарными, оператор транзакции ничего не делает. Наименее не с уровнем транзакции по умолчанию.

  • Поскольку код, кажется, содержится внутри функции, не используйте область действия form напрямую. Какие бы значения не требовались для функции, она должна быть объявлена ​​и передана в функцию с использованием области действия arguments.

Возможно, есть дополнительные способы упорядочить вещи, но это должно дать вам хорошее начало.

Пример вызова процедуры

<cfstoredproc procedure="YourProcedureNameHere" datasource="#yourDSN#">
    <cfprocparam type="in" dbvarname="@AccountID" cfsqltype="cf_sql_char" value="#arguments.frm_accountid#" ....>
    ... more params ...
    <cfprocparam type="in" dbvarname="@Email" cfsqltype="cf_sql_varchar" value="#arguments.frm_email#">

    <!--- User --->
    <cfif structKeyExists(ARGUMENTS, "frmSaveaccount_isuser")>
        <cfprocparam type="in" dbvarname="@IsUser" cfsqltype="cf_sql_bit" value="1">
        <cfprocparam type="in" dbvarname="@ActiveUser" cfsqltype="cf_sql_bit" value="#structKeyExists(arguments, 'frm_activeuser')#">
        ... more params ... 
    </cfif>

    <!--- Staff --->
    <cfif structKeyExists(ARGUMENTS, "frmSaveaccount_isstaff")>
        <cfprocparam type="in" dbvarname="@IsStaff" cfsqltype="cf_sql_bit" value="1">
        <cfprocparam type="in" dbvarname="@ActiveStaff" cfsqltype="cf_sql_bit" value="#structKeyExists(arguments, 'frm_activeuser')#">
        ... more params ... 
    </cfif>


</cfstoredproc>
0 голосов
/ 27 апреля 2018

До 2008 года подход, который вы используете, в значительной степени присутствует. В SQL Server нет «Upsert», который позаботится о вас, поэтому вы должны проверить себя.

До 2008 г. - общий

IF EXISTS(SELECT [PrimaryKey] FROM [MyTable] WHERE [PrimaryKey] = @PassedInID)
    BEGIN
        UPDATE [MyTable]
        SET [Field1] = @PassedInValue1,
            [Field2] = @PassedInValue2
        WHERE [PrimaryKey] = @PassedInID
    END
ELSE
    BEGIN
        INSERT INTO [MyTable] ([PrimaryKey], [Field1], [Field2])
        VALUES (@PassedInID, @PassedInValue1, @PassedInValue2)
    END

Если вы делаете много обновлений и вызываете это много раз, либо передайте первичный ключ, либо индексируйте передаваемое вами значение. Это избавит SQL Server от загрузки данных таблицы, чтобы узнать, является ли обновление вставки необходимо.

Однако, если вы вызываете его много раз, было бы лучше передать таблицу всех вставок / обновлений и JOIN в существующую таблицу дважды и просто выполнить одну INSERT и одну UPDATE

2008 и позже - Sepcific С 2008 года и позже вы можете использовать MERGE (Спасибо за @Shawn за то, что он указал, что это был старый)

MERGE INTO [Accounts] AS target
USING (SELECT @AccountID) AS source ([AccountID])  
    ON (target.[Email] = source.Email AND target.[Username] = @Username)  
    WHEN MATCHED THEN  
        UPDATE SET [FirstName] = @FirstName
                , [LastName] = @LastName
                , [Middle] = @Middle
                , [Email] = @Email
                , [Username] = @Username
WHEN NOT MATCHED THEN  
    INSERT ([AccountID], [FirstName], [LastName], [Middle], [Email], [Username])  
    VALUES (@AccountID, @FirstName, @LastName, @Middle, @Email, @Username)  

Все-в-один Если вам нужно проверить электронную почту и имя пользователя одновременно, вы можете смешать IF NOT EXISTS и MERGE

IF NOT EXISTS(SELECT [PrimaryKey] FROM [MyTable] WHERE [Email] = @Email OR [Username] = @Username)
    BEGIN
        MERGE INTO [Accounts] AS target
        USING (SELECT @AccountID) AS source ([AccountID])  
            ON (target.[Email] = source.Email AND target.[Username] = @Username)  
            WHEN MATCHED THEN  
                UPDATE SET [FirstName] = @FirstName
                        , [LastName] = @LastName
                        , [Middle] = @Middle
                        , [Email] = @Email
                        , [Username] = @Username
        WHEN NOT MATCHED THEN  
            INSERT ([AccountID], [FirstName], [LastName], [Middle], [Email], [Username])  
            VALUES (@AccountID, @FirstName, @LastName, @Middle, @Email, @Username)
    END
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...