Можно ли выдавать операторы CREATE, используя sp_executesql с параметрами?
Я пытаюсь динамически создавать триггеры, но столкнулся с запутанной проблемой, 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', @[email protected]
При запуске выше, я получаю ошибку:
Сообщение 156, уровень 15, состояние 1, строка 2
Неверный синтаксис рядом с ключевым словом "TRIGGER".
После нескольких попыток я обнаружил, что даже если я вообще не использую @tableName
в динамическом SQL, я все равно получаю эту ошибку. И я также получаю эту ошибку, пытаясь создать 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)
для параметра.
Ответы
Ответ 1
Если вы выполните оператор create trigger
который, как вы сказали, вы напечатали... вы обнаружите, что он не работает. Оператор print в теле триггера пытается вывести @tablename
, но никогда не определяется, поэтому вы получите сообщение об ошибке:
Необходимо объявить скалярную переменную "@tableName".
Но это не ваша главная проблема. Что касается того, почему вы не можете выполнить инструкцию DDL с execute_sql
с параметрами, я не смог найти документацию, объясняющую почему... но ваш опыт и другие доказывают, что это хлопотно. Я считаю, что этот пост имеет довольно хорошую теорию: sp_executesql добавляет операторы в исполняемый динамический скрипт?
Однако вы можете выполнить динамический sql с операторами DDL, используя инструкцию EXECUTE
. Поэтому вы можете создать параметризованный оператор 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', @[email protected];
Вы также можете преобразовать это в хранимую процедуру с параметрами вместо запуска 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
Я настоятельно рекомендую использовать Dynamic 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', @[email protected]
... вы пытаетесь ввести ваше объявленное @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', @[email protected]
... который даст вам строку...
'
CREATE TRIGGER TR_' + @tableName + N' ON ' + @tableName + N' FOR INSERT
AS
BEGIN
PRINT @tableName
END'
... который может затем принять параметр, через который вы проходите...
EXEC sp_executesql @sql, N'@tableName sysname', @[email protected] ;
Опять же, я бы использовал некоторую тщательную проверку (и белый список) перед передачей чего-либо в динамический SQL, который будет использовать имя динамической таблицы.
ПРИМЕЧАНИЕ. Как отмечено ниже, я считаю, что вы ограничены в операторах DML, которые могут выполняться с помощью sp_executesql()
, и я думаю, что параметризация также ограничена. И, судя по вашим другим комментариям, это не похоже на то, что вам действительно нужен динамический процесс, а способ повторить определенную задачу для нескольких элементов. Если это так, я рекомендую сделать это вручную с помощью копирования/вставки, а затем выполнить операторы.
Ответ 3
Так как SQL работает нормально либо напрямую, либо когда не передаются параметры для sp_executesql, похоже, что я столкнулся с настоящим ограничением в механизме SQL, но я нигде не вижу его документированного.
Такое поведение задокументировано, хотя и не интуитивно понятно. Соответствующая выдержка из документации по теме ограничений триггера:
CREATE TRIGGER должен быть первым оператором в пакете
Когда вы выполняете параметризованный запрос, объявления параметров считаются частью пакета. Следовательно, пакет CREATE TRIGGER
(и другие операторы 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 Этот синтаксис разрешен только для параметризованных запросов. Сообщение 111, уровень 15, состояние 1, строка 73 "CREATE TRIGGER" должно быть первым оператором в пакете запроса.
Аналогично, выполнение другого параметризованного оператора с помощью этого метода выполняется успешно:
EXECUTE (N'(@tableName sysname = N''MyTable'')PRINT @tableName');
Но если вы на самом деле не используете параметр в пакете, возникает ошибка
EXECUTE (N'(@tableName sysname = N''MyTable'')PRINT ''done''');
Сообщение 1050, уровень 15, состояние 1, строка 75 Этот синтаксис разрешен только для параметризованных запросов.
Суть в том, что вам нужно построить оператор CREATE TRIGGER
в виде строки без параметров и выполнить оператор как непараметрический запрос для создания триггера.
Ответ 4
Можно ли выдавать операторы CREATE, используя sp_executesql с параметрами?
Простой ответ "Нет", вы не можете
По данным MSDN
Как правило, параметры действительны только в инструкциях языка манипулирования данными (DML), но не в операторах языка определения данных (DDL)
Вы можете проверить более подробную информацию об этом утверждении Параметры
В чем проблема?
Параметры допускаются только вместо скалярных литералов, таких как строки или даты в кавычках или числовые значения. Вы не можете параметризовать операцию DDL
.
Что можно сделать?
Я считаю, что вы хотите использовать параметризованный sp_executesql
, чтобы избежать любой атаки SQL-инъекций. Чтобы добиться этого для операций DDL
вы можете сделать следующее, чтобы минимизировать вероятность атаки.
- Использовать разделители: Вы можете использовать
QUOTENAME()
для параметров SYSNAME
таких как имя триггера, имена таблиц и имена столбцов. - Ограничение разрешений. Учетная запись пользователя, которую вы используете для запуска динамического
DDL
, должна иметь только ограниченные разрешения. Как и в конкретной схеме только с разрешением CREATE
. - Скрытие сообщения об ошибке: не выдавайте фактическую ошибку пользователю. SQL-инъекции в основном выполняются методом проб и ошибок. Если вы скроете реальное сообщение об ошибке, его будет сложно взломать.
- Проверка ввода: у вас всегда может быть функция, которая проверяет введенную строку, экранирует требуемые символы, проверяет конкретные ключевые слова, такие как
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
Вы не должны объединять @tableName
для sp_executesql
а вместо этого делать его частью строки. Это будет переменная, которую вы объявляете для sp_executesql
. Я бы также изменил имя параметра, которому вы присваиваете значение, чтобы они не совпадали.
Что-то вроде:
DECLARE @tableNameVar sysname = 'ContentItems';
DECLARE @sql nvarchar(max) = N'
CREATE TRIGGER ''TR_'' + @tableName ON @tableName FOR INSERT
AS
BEGIN
PRINT @tableName
END';
EXEC sp_executesql @sql, N'@tableName sysname', @[email protected]
Возможно, вам понадобится добавить несколько кавычек вокруг @tableName
для оператора PRINT
но, вероятно, нет.
Использование QUOTENAME()
никогда не является плохой идеей.
Ответ 6
Лично я ненавижу триггеры и стараюсь избегать их большую часть времени;)
Однако, если вам действительно нужен этот динамический материал, вы должны использовать 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%'')';
Ответ 7
Если вы хотите использовать параметр в качестве строки, добавьте double 'до и после имени параметра
вот так:
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
Ответ 8
Можно ли выдавать операторы CREATE, используя sp_executesql с параметрами?
Ответ "Да", но с небольшой корректировкой:
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 '''''[email protected]+'''''
END''
)' -- End of EXECUTE()
EXEC sp_executesql @sql, N'@tableName sysname', @[email protected]
Список настроек:
- Требуется дополнительная EXECUTE, комментарий ниже объясняет почему
- Добавлены дополнительные квадратные скобки, чтобы сделать SQL-инъекции немного сложнее
Я ищу конкретные (в идеале, документированные) ограничения sp_executesql с параметрами, и если есть какие-либо обходные пути для этих конкретных ограничений (за исключением не использования параметров)
в этом случае это ограничение DDL-команд, а не sp_executesql. Операторы DDL не могут быть параметризованы с помощью переменных. Документация Microsoft гласит:
Переменные можно использовать только в выражениях, а не вместо имен объектов или ключевых слов. Чтобы создать динамические операторы SQL, используйте EXECUTE.
источник: DECLARE (Transact-SQL)
Поэтому решение с EXECUTE предоставляется мной в качестве обходного пути.