存储过程中的动态查询不会返回错误,但是无法插入记录

时间:2019-03-26 09:27:33

标签: sql sql-server

我正在尝试编写一个存储过程,该过程将插入记录,其中column1和column2被“固定”,而column3,column4和column5发生了变化。有时甚至没有column3或column4或column5。

这是我第一次在存储过程中使用动态查询,但感到很困惑。请帮我看看我在做什么错。

这是我的代码:

ALTER PROCEDURE [dbo].[usp_Insert]
    @param1 nvarchar(MAX) = NULL, 
    @param2 nvarchar(MAX) = NULL, 
    @param3 nvarchar(MAX) = NULL, --optional
    @param4 nvarchar(MAX) = NULL, --optional
    @param5 nvarchar(MAX) = NULL, --optional
    @col3 nvarchar(50) = NULL, --optional
    @col4 nvarchar(50) = NULL, --optional
    @col5 nvarchar(50) = NULL  --optional

AS

SET NOCOUNT ON;
DECLARE @QRY NVARCHAR(MAX) = '';

BEGIN
    IF NOT EXISTS (Select ...)
        BEGIN TRY
            BEGIN TRANSACTION
            SET @QRY = @QRY + ' DECLARE @param1 nvarchar(MAX) = ' + CHAR(39) + @param1 + CHAR(39) + ';'
            SET @QRY = @QRY + ' DECLARE @param2 nvarchar(MAX) = ' + CHAR(39) + @param2 + CHAR(39) + ';'
            IF @param3 is not null
                SET @QRY = @QRY + ' DECLARE @param3 nvarchar(MAX) = ' + CHAR(39) + @param3 + CHAR(39) + ';'
            IF @param4 is not null
                SET @QRY = @QRY + ' DECLARE @param4 nvarchar(MAX) = ' + CHAR(39) + @param4 + CHAR(39) + ';'
            IF @param5 is not null
                SET @QRY = @QRY + ' DECLARE @param5 nvarchar(MAX) = ' + CHAR(39) + @param5 + CHAR(39) + ';'

            SET @QRY = @QRY + ' INSERT INTO tableName (column1, column2' 
            IF @col3 is not null
                SET @QRY = @QRY + ', ' + @col3
            IF @col4 is not null
                SET @QRY = @QRY + ', ' + @col4
            IF @col5 is not null
                SET @QRY = @QRY + ', ' + @col5
            SET @QRY = @QRY + ') VALUES '
            SET @QRY = @QRY + ' (@param1, @param2 '
            IF @col3 is not null
                SET @QRY = @QRY + ', @param3'
            IF @col4 is not null
                SET @QRY = @QRY + ', @param4'
            IF @col5 is not null
                SET @QRY = @QRY + ', @param5'
            SET @QRY = @QRY + ')'

            EXEC (@QRY)

            COMMIT TRANSACTION
        END TRY
        BEGIN CATCH
            IF (@@TRANCOUNT > 0) 
                BEGIN  
                    ROLLBACK TRANSACTION   
                    RAISERROR('Exception occurred. Transaction rolled back.',16,1)
                END
        END CATCH
    ELSE
        BEGIN  
            RAISERROR('Duplicate record found. Record not inserted.',16,1)
        END
END

如果有更好的方法来实现我的目标,请给我启发。谢谢!

编辑: 抱歉,我很困惑。是的,我的代码中有一个开始并提交。同样,第3、4和5列的列名也作为参数传递给存储过程。这就是为什么我选择使用动态查询的原因。我相应地编辑了上面的代码。很抱歉忽略了非常重要的部分!

5 个答案:

答案 0 :(得分:1)

我可以看到的一个问题(感谢@GuidoG)是,如果@param1@param2NULL,则您的@QRY字符串将是NULL

您可以替换这些行...

        SET @QRY = @QRY + ' DECLARE @param1 nvarchar(MAX) = ' + CHAR(39) + ISNULL(@param1, 'NULL') + CHAR(39) + ';'
        SET @QRY = @QRY + ' DECLARE @param2 nvarchar(MAX) = ' + CHAR(39) + ISNULL(@param2, 'NULL') + CHAR(39) + ';'

我个人还是会跳过参数部分。您没有逃避它们,因此容易受到SQL Injection攻击和/或意外失败的影响。

sp_executesql可以避免这些问题...

        SET @QRY = @QRY + ' INSERT INTO tableName (column1, column2' 
        IF @param3 is not null
            SET @QRY = @QRY + ', column3'
        IF @param4 is not null
            SET @QRY = @QRY + ', column4'
        IF @param5 is not null
            SET @QRY = @QRY + ', column5'
        SET @QRY = @QRY + ') VALUES '
        SET @QRY = @QRY + ' (@param1, @param2 '
        IF @param3 is not null
            SET @QRY = @QRY + ', @param3'
        IF @param4 is not null
            SET @QRY = @QRY + ', @param4'
        IF @param5 is not null
            SET @QRY = @QRY + ', @param5'
        SET @QRY = @QRY + ')'

        EXEC sp_executesql
          @QRY,  
          N'@param1 NVARCHAR(MAX),
            @param2 NVARCHAR(MAX),
            @param3 NVARCHAR(MAX),
            @param4 NVARCHAR(MAX),
            @param5 NVARCHAR(MAX)',
          @param1,
          @param2,
          @param3,
          @param4,
          @param5

(即使您传递了所有5个参数,也只有您感兴趣的参数才在INSERT中使用,并将它们作为参数传递可以防止SQL Injection攻击或转义特殊字符的需要等等,等等。哦,您再也没有串联NULL的风险。)

编辑:

我也环顾四周,看看是否有更好的方法来获取列默认值。尽管我找不到“更好”的东西,但是有一种“不同”的方法...

INSERT INTO
    tableName(
      column1,
      column2,
      column3,
      column4,
      column5
    )
SELECT
    @param1,
    @param2,
    ISNULL(@param3, MAX(CASE WHEN COLUMN_NAME = 'column3' THEN COLUMN_DEFAULT END)),
    ISNULL(@param4, MAX(CASE WHEN COLUMN_NAME = 'column4' THEN COLUMN_DEFAULT END)),
    ISNULL(@param5, MAX(CASE WHEN COLUMN_NAME = 'column5' THEN COLUMN_DEFAULT END))
FROM
    INFORMATION_SCHEMA.COLUMNS
WHERE
        TABLE_SCHEMA = 'dbo'        -- or whatever it really is in your case 
    AND TABLE_NAME   = 'tableName'
;

根本不需要动态SQL。但是我不认为它比动态SQL更好。

EDIT2:

OH !!!

您的代码在错误处理中显示ROLLBACK,这意味着您没有向我们显示的代码中有BEGIN TRANSACTION吗?

您实际上在任何地方都有COMMIT TRANSACTION吗?

答案 1 :(得分:1)

您必须检查参数是否为NULL,请看下面的示例

declare @param1 nvarchar(10) = null
declare @QRY nvarchar(100)

set @QRY = 'test ' + @param1

select @QRY

结果将为NULL,因为其中一个隐含值为空

因此,您应该检查是否存在null并替换为文本'NULL'或其他一些值
如果这样做的话,我们将不再是NULL,请再次查看此示例

declare @param1 nvarchar(10) = null
declare @QRY nvarchar(100)

set @QRY = 'test ' + isnull(@param1, 'null')

select @QRY

这将导致test null

也许这就是您的问题所在

所以我建议将其更改为

SET @QRY = @QRY + ' DECLARE @param1 nvarchar(MAX) = ' + CHAR(39) + ISNULL(@param1, 'NULL') + CHAR(39) + ';'
SET @QRY = @QRY + ' DECLARE @param2 nvarchar(MAX) = ' + CHAR(39) + ISNULL(@param2, 'NULL') + CHAR(39) + ';

答案 2 :(得分:1)

使用sp_executesql并将参数作为参数传递:

ALTER PROCEDURE [dbo].[usp_Insert]
    @param1 nvarchar(MAX) = NULL, 
    @param2 nvarchar(MAX) = NULL, 
    @param3 nvarchar(MAX) = NULL, --optional
AS

SET NOCOUNT ON;
DECLARE @QRY NVARCHAR(MAX);

BEGIN
    IF NOT EXISTS (Select ...)
            SET @QRY = N'INSERT INTO tableName (column1, column2' 
            IF @param3 is not null
                SET @QRY = @QRY + N', column3'
            SET @QRY = @QRY + N') VALUES '
            SET @QRY = @QRY + N' (@p1, @p2 '
            IF @param3 is not null
                SET @QRY = @QRY + N', @p3'
            SET @QRY = @QRY + N')'

            exec sp_executesql @QRY, N'@p1 NVARCHAR(MAX), @p2 NVARCHAR(MAX), @p3 NVARCHAR(MAX)', @param1, @param2, @param3;

    END
END

@param...是本地存储的proc参数。 @p1 ... @px是动态SQL上下文参数。确定可以传递未使用的额外参数(NULL时为@p3

您的BEGIN TRY ... CATCH事务处理不正确。有关正确的模式,请参见http://rusanu.com/2009/06/11/exception-handling-and-nested-transactions/

答案 3 :(得分:0)

我假设您正在执行此操作,以插入第3、4、5列的默认值

这种方法更加简单,可以防止注入

ALTER PROCEDURE [dbo].[usp_Insert]
    @param1 nvarchar(MAX) = NULL, 
    @param2 nvarchar(MAX) = NULL, 
    @param3 nvarchar(MAX) = NULL, --optional
    @param4 nvarchar(MAX) = NULL, --optional
    @param5 nvarchar(MAX) = NULL  --optional

AS

SET NOCOUNT ON;
DECLARE @QRY NVARCHAR(MAX) = '';

BEGIN
    IF NOT EXISTS (Select ...)
        BEGIN TRY

            SET @QRY = 'INSERT INTO TABLE_NAME (Col1, Col2, Col3, Col4, Col5) 
                VALUES (@Param1, @Param2, @Param3, @Param4, @Param5)'
            IF @Param3 IS NULL
                SET @QRY = REPLACE(@Qry, '@Param3', 'DEFAULT')
            IF @Param4 IS NULL
                SET @QRY = REPLACE(@Qry, '@Param4', 'DEFAULT')
            IF @Param5 IS NULL
                SET @QRY = REPLACE(@Qry, '@Param5', 'DEFAULT')
            sp_executesql @QRY, 
                 N'@param1 nvarchar(MAX), 
                 @param2 nvarchar(MAX),
                 @param3 nvarchar(MAX),
                 @param4 nvarchar(MAX),
                 @param5 nvarchar(MAX)',
                 @param1, @param2, @param3, @param4, @param5

        END TRY
        BEGIN CATCH
            IF (@@TRANCOUNT > 0) 
                BEGIN  
                    ROLLBACK TRANSACTION   
                    RAISERROR('Exception occurred. Transaction rolled back.',16,1)
                END
        END CATCH
    ELSE
        BEGIN  
            RAISERROR('Dulicate record found. Record not inserted.',16,1)
        END
END

如果可选参数为null,则动态sql看起来像

INSERT INTO TABLE_NAME (Col1, Col2, Col3, Col4, Col5) 
VALUES (@Param1, @Param2, DEFAULT, DEFAULT, DEFAULT)

哪个完全有效

答案 4 :(得分:0)

当我针对如此相对简单的用例看到如此复杂的动态SQL代码时,我个人会感到鸡皮

似乎您只想在其中一个表的新记录中插入2到5个字段值。

我最重要的问题是,这三个可选字段是否具有除NULL以外的任何默认值(通过使用DEFAULT约束)。如果是这样,我还将提供这些默认值作为您的存储过程参数的默认值。

然后我将从前两个必需参数中删除默认值。

然后我将更改正文以仅使用一个简单的插入语句,而不使用所有复杂的动态SQL内容。

类似这样的东西:

ALTER PROCEDURE [dbo].[usp_Insert]
    @param1 NVARCHAR(MAX), 
    @param2 NVARCHAR(MAX), 
    @param3 NVARCHAR(MAX) = NULL, --optional
    @param4 NVARCHAR(MAX) = NULL, --optional
    @param5 NVARCHAR(MAX) = N'Some default value other than NULL'  --optional
AS
    SET NOCOUNT ON;
BEGIN
    IF NOT EXISTS (SELECT ...)
        BEGIN TRY
            INSERT INTO tableName (column1, column2, column3, column4, column5)
            VALUES (@param1, @param2, @param3, @param4, @param5);
        END TRY
        BEGIN CATCH
            IF (@@TRANCOUNT > 0) 
                BEGIN  
                    ROLLBACK TRANSACTION   
                    RAISERROR('Exception occurred. Transaction rolled back.', 16, 1);
                END;
        END CATCH;
    ELSE
        BEGIN  
            RAISERROR('Dulicate record found. Record not inserted.', 16, 1);
        END;
END;
GO

但这只是我个人的喜好...

附加说明:

我真的很喜欢您的SP结构,在该结构中,您可以在第一个BEGIN语句之前设置任何准备语句。我个人总是在AS之后使用BEGIN ... END块,但是您可以在准备和执行之间添加额外的分隔。真好;)