可以使用带有参数的sp_executesql发出CREATE语句吗?

时间:2019-01-30 17:56:43

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

我试图动态创建触发器,但是在使用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

运行以上命令时,出现错误:

  

第15层状态1的第2行156消息
  关键字“ 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),我都会遇到相同的错误。

7 个答案:

答案 0 :(得分:4)

如果执行您说过打印的create trigger语句,您将发现它不起作用。触发器主体中的print语句正在尝试输出@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

答案 1 :(得分: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语句上受到限制,并且我认为参数化也受到限制。而且,根据您的其他评论,这听起来并不像您真的需要一个动态过程,而是一种对少数元素重复执行特定任务的方法。如果是这种情况,我的建议是手动执行复制/粘贴操作,然后执行语句。

答案 2 :(得分:2)

  

由于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的局限性:

  

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

类似地执行这种方法的另一个参数化语句成功运行:

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

但是,如果您实际上不在批处理中使用该参数,则会导致错误

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

消息1050,级别15,状态1,第75行此语法仅适用于   参数化查询。

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

答案 3 :(得分:2)

  

是否可以使用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 

答案 4 :(得分:1)

  

是否可以使用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. 涉及了额外的执行力,下面的评论解释了原因
  2. 添加了额外的方括号,以使SQL注入更加困难

  

我正在寻找以下方面的特定(理想情况是有文件证明)限制   sp_executesql与参数,以及是否有任何变通办法   这些特定的限制(除了不使用参数外)

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

  

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

来源:DECLARE (Transact-SQL)

因此,我提供了EXECUTE解决方案

答案 5 :(得分:0)

我个人讨厌触发器,并在大多数情况下尽量避免使用它们;)

但是,如果您确实确实需要这种动态的东西,则应该使用 sp_MSforeachtable ,并避免不计成本地注入(肖恩指出):

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%'')';

答案 6 :(得分:0)

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

喜欢这样

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