Можно ли выдавать операторы CREATE, используя sp_executesql с параметрами? - PullRequest
0 голосов
/ 30 января 2019

Я пытаюсь динамически создавать триггеры, но столкнулся с запутанной проблемой, связанной с использованием 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'.

После нескольких попыток я обнаружил, что даже если я вообще не использую @tableName в динамическом SQL, я все равно получаю эту ошибку.И я также получаю эту ошибку, пытаясь создать 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 голосов
/ 03 февраля 2019

Если вы хотите использовать параметр в качестве строки, добавьте двойное 'до и после имени параметра

как это :

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
0 голосов
/ 03 февраля 2019

Можно ли выдавать операторы 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 '''''+@tableName+'''''
        END''
        )'                            -- End of EXECUTE()


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

Список настроек:

  1. Требуется дополнительная EXECUTE, комментарий ниже объясняет, почему
  2. Добавлены дополнительные квадратные скобки, чтобы сделать SQL-инъекции немного сложнее

Я ищу конкретные (в идеале, документированные) ограничения sp_executesql с параметрами, и если есть какие-то обходные пути для этих конкретных ограничений (кроме неиспользования параметров)

в этом случае этоявляется ограничением команд DDL, а не sp_executesql.Операторы DDL не могут быть параметризованы с использованием переменных.Документация Microsoft гласит:

Переменные можно использовать только в выражениях, а не вместо имен объектов или ключевых слов.Для построения динамических операторов SQL используйте EXECUTE.

source: DECLARE (Transact-SQL)

Поэтому решение с EXECUTE предоставлено мной какобходной путь

0 голосов
/ 03 февраля 2019

Можно ли выдавать операторы CREATE, используя sp_executesql с параметрами?

Простой ответ: "Нет" , вы не можете

Согласно MSDN

Обычно параметры действительны только в операторах языка манипулирования данными (DML), но не в операторах языка определения данных (DDL)

Вы можете проверить более подробную информацию об этом Параметры оператора

В чем проблема?

Параметры допускаются только вместо скалярных литералов,например, строки или даты в кавычках или числовые значения.Вы не можете параметризовать 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 
0 голосов
/ 02 февраля 2019

Поскольку 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 как строку без параметров и выполнить оператор как непараметрический запрос для создания триггера.

0 голосов
/ 01 февраля 2019

Если вы выполните заявление 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', @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
0 голосов
/ 30 января 2019

Лично я ненавижу триггеры и большую часть времени стараюсь избегать их;)

Однако, если вам действительно нужен этот динамический материал, вы должны использовать 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%'')';
0 голосов
/ 30 января 2019

Я бы настоятельно рекомендовал не использовать динамический 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, который будет использовать имя динамической таблицы.

ПРИМЕЧАНИЕ. Как отмечено ниже, я полагаю, что вы ограничены в операторах DML, которые могут выполняться с sp_executesql(), и я думаю, что параметризация также ограничена.И, основываясь на других ваших комментариях, звучит не так, как будто вам действительно нужен динамический процесс, а способ повторить определенную задачу для нескольких элементов.Если это так, я рекомендую сделать это вручную с помощью копирования / вставки, а затем выполнить операторы.

...