我试图动态创建触发器,但是在使用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)
,我都会遇到相同的错误。
答案 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
操作,您可以执行以下操作以最大程度地降低遭受攻击的可能性。
QUOTENAME()
用于SYSNAME
参数,例如触发器名称,表名称和列名称。DDL
的用户帐户应仅具有有限的权限。像上
具有CREATE
权限的特定模式。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
附件列表:
我正在寻找以下方面的特定(理想情况是有文件证明)限制 sp_executesql与参数,以及是否有任何变通办法 这些特定的限制(除了不使用参数外)
在这种情况下,它是DDL命令的限制,而不是sp_executesql。无法使用变量对DDL语句进行参数化。 Microsoft文档说:
变量只能在表达式中使用,不能代替对象 名称或关键字。要构造动态SQL语句,请使用EXECUTE。
因此,我提供了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