【问题标题】:Is it possible to issue CREATE statements using sp_executesql with parameters?是否可以使用带参数的 sp_executesql 发出 CREATE 语句?
【发布时间】:2019-06-24 02:19:11
【问题描述】:

我正在尝试动态创建触发器,但在使用 sp_executesql 并将参数传递给动态 SQL 时遇到了一个令人困惑的问题。以下简单的测试用例有效:

DECLARE @tableName sysname = 'MyTable';
DECLARE @sql nvarchar(max) = N'
    CREATE TRIGGER TR_' + @tableName + N' ON ' + @tableName + N' FOR INSERT
        AS
        BEGIN
            PRINT 1
        END';
EXEC sp_executesql @sql

但是,我希望能够在脚本中使用@tableName(和其他值)作为变量,所以我将它传递给sp_executesql 调用:

DECLARE @tableName sysname = 'ContentItems';
DECLARE @sql nvarchar(max) = N'
    CREATE TRIGGER TR_' + @tableName + N' ON ' + @tableName + N' FOR INSERT
        AS
        BEGIN
            PRINT @tableName
        END';
EXEC sp_executesql @sql, N'@tableName sysname', @tableName=@tableName

运行上面的时候,我得到一个错误:

消息 156,第 15 级,状态 1,第 2 行
关键字“TRIGGER”附近的语法不正确。

在尝试了几件事之后,我发现即使我在动态 SQL 中根本不使用@tableName,我仍然会收到此错误。而且我在尝试创建PROCEDURE 时也遇到此错误(显然,消息是关键字“PROCEDURE”附近的语法不正确。

由于 SQL 直接运行或在不向 sp_executesql 提供参数时运行良好,这似乎是我在 SQL 引擎中遇到了真正的限制,但我没有看到它在任何地方记录。有谁知道是否有办法接受动态 CREATE 脚本,或者至少了解正在遇到的潜在限制?

更新 我可以添加一个PRINT 语句,并得到下面的SQL,它是有效的,并且运行成功(直接运行时)。如果 SQL 中没有任何动态内容(它只是一个没有串联的单个字符串),我仍然会收到错误。

CREATE TRIGGER TR_ContentItems ON ContentItems FOR INSERT
    AS
    BEGIN
        PRINT @tableName
    END

无论是使用sysname 还是nvarchar(max) 作为参数,我也会遇到同样的错误。

【问题讨论】:

  • 使用打印语句并理解生成的语句。在执行 sp_executesql 之前打印 @sql
  • 另外,您需要将@tablename 转换为nvarchar
  • 可能是触发器名称已经存在。因此,在创建触发器之前尝试 DROP TRIGGER
  • @Dheerendra 从技术上讲,sysname 数据类型是 nvarchar
  • 我希望能够在脚本中使用@tableName(和其他值)作为变量...在脚本中还是在触发器中?正确答案取决于此。

标签: sql-server dynamic-sql sp-executesql


【解决方案1】:

如果您执行您说您打印的create trigger 语句...您会发现它不起作用。触发器主体中的打印语句试图输出@tablename,但从未定义过,所以会报错:

必须声明标量变量“@tableName”。

但这不是你的主要问题。至于为什么您似乎无法使用带有参数的execute_sql 执行DDL 语句,我找不到任何文档来解释原因……但是您和其他人的经验证明这很麻烦。我相信这个帖子有一个很好的理论:sp_executesql adds statements to executed dynamic script?

但是,您可以使用 EXECUTE 语句执行带有 DDL 语句的动态 sql。所以你可以做的是创建一个参数化的sp_executesql语句来验证你的表名,然后创建一个动态的sql字符串来执行EXECUTE语句。

它看起来并不漂亮,但它确实有效:

DECLARE @tableName sysname = 'MyTable';
DECLARE @sql nvarchar(max) = 
N'
set @tableName = (SELECT name FROM sys.tables WHERE OBJECT_ID = OBJECT_ID(@tableName)) --validate table
DECLARE @CreateTriggerSQL as varchar(max) =
''
CREATE TRIGGER '' + QUOTENAME(''TR_'' + @tableName) + '' ON '' + QUOTENAME( @tableName) + '' FOR INSERT
AS
BEGIN
    PRINT '''''' + @tableName + ''''''
END
''
print isnull(@CreateTriggerSQL, ''INVALID TABLE'')
exec (@CreateTriggerSQL)
';

EXEC sp_executesql @sql, N'@tableName sysname', @tableName=@tableName;

如果这样更方便,您也可以将其转换为带参数的存储过程,而不是运行sp_executesql。看起来干净了一点:

CREATE PROCEDURE sp_AddTriggerToTable (@TableName AS sysname) AS

set @tableName = (SELECT name FROM sys.tables WHERE OBJECT_ID = OBJECT_ID(@tableName)) --validate table
DECLARE @CreateTriggerSQL as varchar(max) =
'
CREATE TRIGGER ' + QUOTENAME('TR_' + @tableName) + ' ON ' + QUOTENAME( @tableName) + ' FOR INSERT
AS
BEGIN
    PRINT ''' + @tableName + '''
END
'
print isnull(@CreateTriggerSQL, 'INVALID TABLE')
exec (@CreateTriggerSQL)
GO

【讨论】:

    【解决方案2】:

    我强烈反对将动态 SQL 与表名一起使用。您正在为一些严重的 SQL 注入问题做好准备。您应该验证进入@tableName 变量的任何内容。

    也就是说,在你的例子中......

    DECLARE @tableName sysname = 'ContentItems';
    DECLARE @sql nvarchar(max) = N'
        CREATE TRIGGER TR_' + @tableName + N' ON ' + @tableName + N' FOR INSERT
            AS
            BEGIN
                PRINT @tableName
            END';
    EXEC sp_executesql @sql, N'@tableName sysname', @tableName=@tableName
    

    ...您尝试将声明的@tableName 输入到您为@sql 创建的文本中,然后您尝试通过spexecutesql 传递参数。这使您的@sql 在尝试调用它时无效。

    你可以试试:

    DECLARE @tableName sysname = 'ContentItems';
    DECLARE @sql nvarchar(max) = N'
        CREATE TRIGGER TR_'' + @tableName + N'' ON '' + @tableName + N'' FOR INSERT
            AS
            BEGIN
                PRINT @tableName
            END';
    EXEC sp_executesql @sql, N'@tableName sysname', @tableName=@tableName
    

    ...这会给你字符串...

    '
    CREATE TRIGGER TR_' + @tableName + N' ON ' + @tableName + N' FOR INSERT
        AS
        BEGIN
            PRINT @tableName
        END'
    

    ...然后可以接受您传递的参数...

    EXEC sp_executesql @sql, N'@tableName sysname', @tableName=@tableName ;
    

    同样,在将任何内容传递给将使用动态表名的动态 SQL 之前,我会使用一些繁重的验证(和白名单)。

    注意:如下所述,我相信您对可以使用sp_executesql() 执行的 DML 语句是有限的,而且我认为参数化也是有限的。根据您的其他 cmets,听起来您并不需要一个动态过程,而是一种为少数元素重复特定任务的方法。如果是这种情况,我的建议是手动复制/粘贴,然后执行语句。

    【讨论】:

    • 我的代码仍然出现同样的错误。有关详细信息,请参阅我对问题的更新。
    • 我认为您可以在 sp_executesql 中执行的 DML 语句受到限制,尤其是使用参数。这将运行更简单的语句,但我不确定创建触发器。
    【解决方案3】:

    由于 SQL 直接运行或不提供时运行良好 sp_executesql 的参数,这似乎我遇到了一个真实的 SQL 引擎中的限制,但我没有在任何地方看到它的记录。

    此行为已记录在案,尽管不直观。触发限制主题下the documentation的相关摘录:

    CREATE TRIGGER 必须是批处理中的第一条语句

    当您执行参数化查询时,参数声明被计为批处理的一部分。因此,CREATE TRIGGER 批处理(以及其他用于可编程对象(如 proc、函数等)的 CREATE 语句)不能作为参数化查询执行。

    当您尝试将CREATE TRIGGER 作为参数化查询运行时收到的无效语法错误消息并不是特别有用。下面是使用未记录和不受支持的内部参数化查询语法的代码的简化版本。

    EXECUTE(N'(@tableName sysname = N''MyTable'')CREATE TRIGGER TR_MyTable ON dbo.MyTable FOR INSERT AS');
    

    这至少会导致调用CREATE TRIGGER 限制的错误:

    Msg 1050, Level 15, State 1, Line 73 此语法仅允许用于 参数化查询。消息 111,级别 15,状态 1,第 73 行“创建 TRIGGER' 必须是查询批处理中的第一条语句。

    用这个方法同样执行另一个参数化语句运行成功:

    EXECUTE (N'(@tableName sysname = N''MyTable'')PRINT @tableName');
    

    但是如果在批处理中没有实际使用该参数,则会产生错误

    EXECUTE (N'(@tableName sysname = N''MyTable'')PRINT ''done''');
    

    Msg 1050, Level 15, State 1, Line 75 此语法仅允许用于 参数化查询。

    底线是您需要将CREATE TRIGGER 语句构建为不带参数的字符串,并将语句作为非参数化查询执行以创建触发器。

    【讨论】:

      【解决方案4】:

      是否可以使用 sp_executesql 发出 CREATE 语句 参数?

      简单的回答是“不”,你不能

      根据 MSDN

      通常,参数仅在数据操作语言中有效 (DML) 语句,而不是数据定义语言 (DDL) 语句

      您可以查看有关此Statement Parameters的更多详细信息

      有什么问题?

      只允许使用参数代替标量文字,例如带引号的字符串或日期,或数值。您不能参数化 DDL 操作。

      可以做什么?

      我相信你想使用参数化sp_executesql是为了避免任何SQL注入攻击。要为DDL 操作实现这一点,您可以执行以下操作以最大程度地减少攻击的可能性。

      1. 使用分隔符:您可以将QUOTENAME() 用于SYSNAME 参数,例如触发器名称、表名称和列名称。
      2. 限制权限:您用于运行动态DDL 的用户帐户应该只有有限的权限。就像在一个 仅具有 CREATE 权限的特定架构。
      3. 隐藏错误消息:不要将实际错误抛给用户。 SQL 注入主要通过试错法进行。如果 你隐藏实际的错误信息,破解它会变得很困难。
      4. 输入验证:你总是可以有一个函数来验证输入字符串,转义所需的字符,检查 对于特定的关键字,例如 DROP

      有什么解决方法吗?

      如果您想使用sp_executesql 参数化您的语句,在这种情况下,您可以在OUTPUT 变量中执行查询并在下一条语句中运行查询,如下所示。

      这样,第一次调用sp_executesql 将参数化您的查询,而实际执行将由第二次调用sp_executesql 执行

      例如。

      DECLARE @TableName VARCHAR(100) = 'MyTable' 
      DECLARE @returnStatement NVARCHAR(max); 
      DECLARE @sql1 NVARCHAR(max)= 
      N'SELECT @returnStatement = ''CREATE TRIGGER TR_''                                          
          +  @TableName + '' ON '' +  @TableName  +  '' FOR INSERT AS BEGIN PRINT 1 END'''
      
      EXEC Sp_executesql 
        @sql1, 
        N'@returnStatement VARCHAR(MAX) OUTPUT, @TableName VARCHAR(100)', 
        @returnStatement output, 
        @TableName 
      
      EXEC Sp_executesql @returnStatement 
      

      【讨论】:

        【解决方案5】:

        是否可以使用 sp_executesql 发出 CREATE 语句 参数?

        答案是“是”,但稍作调整:

        USE msdb
        
        DECLARE @tableName sysname = 'sysjobsteps';
        
        DECLARE @sql nvarchar(max) = N'
        EXECUTE (''                              -- Added nested EXECUTE()
            CREATE TRIGGER [TR_'' + @tableName + N''] ON ['' + @tableName + N''] FOR INSERT
                AS
                BEGIN
                    PRINT '''''+@tableName+'''''
                END''
                )'                            -- End of EXECUTE()
        
        
        EXEC sp_executesql @sql, N'@tableName sysname', @tableName=@tableName
        

        调整列表:

        1. 涉及额外的 EXECUTE,下面的评论解释了原因
        2. 添加了额外的方括号以使 SQL 注入更加困难

        我正在寻找特定的(最好是记录在案的)限制 带参数的 sp_executesql 以及是否有任何解决方法 那些特定的限制(除了不使用参数)

        在这种情况下,这是 DDL 命令的限制,而不是 sp_executesql。 DDL 语句不能使用变量进行参数化。微软文档说:

        变量只能用在表达式中,不能代替对象 名称或关键字。要构造动态 SQL 语句,请使用 EXECUTE。

        来源:DECLARE (Transact-SQL)

        因此,我提供了带有 EXECUTE 的解决方案作为解决方法

        【讨论】:

        • 只需使用QUOTENAME('TR_' + @tableName)QUOTENAME(@tableName, '''')
        • @SalmanA,很遗憾,quotename() 不能在 EXEC() 中使用
        • @AlexanderVolok 不,您在SET 命令中使用它,而不是EXEC。你这里的只是一个 SQL 注入漏洞。
        【解决方案6】:

        就我个人而言,我讨厌触发器并尽量避免它们;)

        但是,如果您真的非常需要这种动态的东西,您应该使用 sp_MSforeachtable 并不惜一切代价避免注入(正如 Shawn 所指出的):

        EXEC sys.sp_MSforeachtable
          @command1 = '
                DECLARE @sql NVARCHAR(MAX)
                SET @sql = CONCAT(''CREATE TRIGGER TR_''
                    , REPLACE(REPLACE(REPLACE(''?'', ''[dbo].'', ''''),''['',''''),'']'','''')
                    , '' ON ? FOR INSERT
            AS
            BEGIN
                PRINT ''''?'''';
            END;'');
            EXEC sp_executesql @sql;'
          , @whereand = ' AND object_id IN (SELECT object_id FROM sys.objects
        WHERE name LIKE ''%ContentItems%'')';
        

        【讨论】:

        • 总的来说,我同意,但在这种情况下,触发器是正确的答案。但是,我并不是想将它们添加到每个表中,而是添加到有限的集合中。存储过程是为了简化将正确的触发器添加到 15 个左右的表中,然后我可以删除该过程并避免任何注入的可能性。
        • 检查。无论如何,如果您添加对对象 ID 的检查,运行我的代码应该不会有任何问题。
        • 我用@whereand 参数修改了我的答案。这解决了您对有限集合的要求,因为您可以在那里进行各种检查。
        • 如果您只是想在少数几个表上执行此操作,最好自己在 SSMS 中编写 SQL 语句,然后用新名称复制/粘贴它。然后您可以运行每个批次。请记住,您可以同时运行它们,但 CREATE TRIGGER 必须是批处理中的第一个语句。
        【解决方案7】:

        如果要将参数用作字符串,请在参数名称前后添加双'

        像这样

        DECLARE @tableName sysname = 'ContentItems'; 
        
        DECLARE @sql nvarchar(max) = N'
                CREATE TRIGGER TR_' + @tableName + N' ON ' + @tableName + N' FOR INSERT
                    AS
                    BEGIN
                       print ''' + @tableName
                    +''' END';
        
        
            EXEC sp_executesql @sql
        

        如果你想使用它作为表名,使用 select 而不是 print ,

        像这样:

        DECLARE @tableName sysname = 'ContentItems';
        
        DECLARE @sql nvarchar(max) = N'
            CREATE TRIGGER TR_' + @tableName + N' ON ' + @tableName + N' FOR INSERT
                AS
                BEGIN
                    select * from ' + @tableName
                +' END';
        
        
        EXEC sp_executesql @sql
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2022-12-12
          • 2017-05-13
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多