динамическая ошибка sql: «CREATE TRIGGER» должен быть первым оператором в пакете запросов

В рамках некоторых административных задач у нас есть много таблиц, для каждой из которых необходимо создать триггер. Триггер устанавливает флаг и дату в базе данных аудита, когда объект был изменен. Для простоты у меня есть таблица со всеми созданными объектами, для которых нужны триггеры.

Я пытаюсь сгенерировать динамический sql, чтобы сделать это для каждого объекта, но получаю следующую ошибку:
'CREATE TRIGGER' must be the first statement in a query batch.

Вот код для генерации sql.

CREATE PROCEDURE [spCreateTableTriggers]
AS

BEGIN

DECLARE @dbname     varchar(50),
        @schemaname varchar(50),
        @objname    varchar(150),
        @objtype    varchar(150),
        @sql        nvarchar(max),
        @CRLF       varchar(2)

SET     @CRLF = CHAR(13) + CHAR(10);

DECLARE ObjectCursor CURSOR FOR
SELECT  DatabaseName,SchemaName,ObjectName
FROM    Audit.dbo.ObjectUpdates;

SET NOCOUNT ON;

OPEN    ObjectCursor ;

FETCH NEXT FROM ObjectCursor
INTO    @dbname,@schemaname,@objname;

WHILE @@FETCH_STATUS=0
BEGIN

    SET @sql = N'USE '+QUOTENAME(@dbname)+'; '
    SET @sql = @sql + N'IF EXISTS (SELECT * FROM sys.triggers WHERE object_id = OBJECT_ID(N'''+QUOTENAME(@schemaname)+'.[Tiud_'+@objname+'_AuditObjectUpdates]'')) '
    SET @sql = @sql + N'BEGIN DROP TRIGGER '+QUOTENAME(@schemaname)+'.[Tiud_'+@objname+'_AuditObjectUpdates]; END; '+@CRLF
    SET @sql = @sql + N'CREATE TRIGGER '+QUOTENAME(@schemaname)+'.[Tiud_'+@objname+'_AuditObjectUpdates] '+@CRLF
    SET @sql = @sql + N'   ON '+QUOTENAME(@schemaname)+'.['+@objname+'] '+@CRLF
    SET @sql = @sql + N'   AFTER INSERT,DELETE,UPDATE'+@CRLF
    SET @sql = @sql + N'AS '+@CRLF
    SET @sql = @sql + N'IF EXISTS(SELECT * FROM Audit.dbo.ObjectUpdates WHERE DatabaseName = '''+@dbname+''' AND ObjectName = '''+@objname+''' AND RequiresUpdate=0'+@CRLF
    SET @sql = @sql + N'BEGIN'+@CRLF
    SET @sql = @sql + N'    SET NOCOUNT ON;'+@CRLF
    SET @sql = @sql + N'    UPDATE  Audit.dbo.ObjectUpdates'+@CRLF
    SET @sql = @sql + N'    SET RequiresUpdate = 1'+@CRLF
    SET @sql = @sql + N'    WHERE   DatabaseName = '''+@dbname+''' '+@CRLF
    SET @sql = @sql + N'        AND ObjectName = '''+@objname+''' '+@CRLF

    SET @sql = @sql + N'END' +@CRLF
    SET @sql = @sql + N'ELSE' +@CRLF
    SET @sql = @sql + N'BEGIN' +@CRLF
    SET @sql = @sql + N'    SET NOCOUNT ON;' +@CRLF
    SET @sql = @sql + @CRLF
    SET @sql = @sql + N'    -- Update ''SourceLastUpdated'' date.'+@CRLF
    SET @sql = @sql + N'    UPDATE  Audit.dbo.ObjectUpdates'+@CRLF
    SET @sql = @sql + N'    SET SourceLastUpdated = GETDATE() '+@CRLF
    SET @sql = @sql + N'    WHERE   DatabaseName = '''+@dbname+''' '+@CRLF
    SET @sql = @sql + N'        AND ObjectName = '''+@objname+''' '+@CRLF
    SET @sql = @sql + N'END; '+@CRLF

    --PRINT(@sql);
    EXEC sp_executesql @sql;

    FETCH NEXT FROM ObjectCursor
    INTO    @dbname,@schemaname,@objname;

END

CLOSE ObjectCursor ;
DEALLOCATE ObjectCursor ;

END

Если я использую PRINT и вставляю код в новое окно запроса, код выполняется без проблем.

Я удалил операторы GO, так как это также приводило к ошибкам.

Что я упускаю?
Почему я получаю сообщение об ошибке при использовании EXEC(@sql); или даже EXEC sp_executesql @sql;?
Это как-то связано с контекстом внутри EXEC()?
Большое спасибо за любую помощь.


person MarkusBee    schedule 26.04.2012    source источник


Ответы (2)


Если вы используете SSMS (или другой подобный инструмент) для запуска кода, созданного этим скриптом, вы получите точно такую ​​же ошибку. Он мог бы работать нормально, если бы вы вставили разделители пакетов (GO), но теперь, когда вы этого не сделали, вы столкнетесь с той же проблемой и в SSMS.

С другой стороны, причина, по которой вы не можете поместить GO в свои динамические сценарии, заключается в том, что GO не является оператором SQL, это просто разделитель, распознаваемый SSMS и некоторыми другими инструментами. Вероятно, вы уже знаете об этом.

В любом случае, смысл GO заключается в том, чтобы инструмент знал, что код должен быть разделен и его части должны выполняться отдельно. И это, отдельно, вы также должны делать в своем коде.

Итак, у вас есть такие варианты:

  • вставьте EXEC sp_execute @sql сразу после части, которая сбрасывает триггер, затем сбросьте значение @sql, чтобы затем сохранить и запустить часть определения, в свою очередь;

  • используйте две переменные, @sql1 и @sql2, сохраните часть IF EXISTS/DROP в @sql1, CREATE TRIGGER в @sql2, затем запустите оба сценария (опять же, по отдельности).

Но тогда, как вы уже поняли, вы столкнетесь с другой проблемой: вы не сможете создать триггер в другой базе данных, не выполнив оператор в контексте этой базы данных.

Теперь есть 2 способа предоставления необходимого контекста:

1) использовать оператор USE;

2) запустите оператор(ы) как динамический запрос, используя EXEC targetdatabase..sp_executesql N'…'.

Очевидно, что первый вариант здесь не сработает: мы не можем добавить USE … перед CREATE TRIGGER, потому что последний должен быть единственным оператором в пакете.

Второй вариант можно использовать, но он потребует дополнительного уровня динамики (не уверен, что это слово). Это связано с тем, что имя базы данных является здесь параметром, поэтому нам нужно запускать EXEC targetdatabase..sp_executesql N'…' как динамический скрипт, а поскольку сам скрипт для запуска должен быть динамическим скриптом, он, следовательно, будет дважды вложенный.

Итак, перед (второй) строкой EXEC sp_executesql @sql; добавьте следующее:

SET @sql = N'EXEC ' + @dbname + '..sp_executesql N'''
           + REPLACE(@sql, '''', '''''') + '''';

Как видите, для правильной интеграции содержимого @sql в качестве вложенного динамического скрипта оно должно быть заключено в одинарные кавычки. По той же причине каждая одиночная кавычка в @sql должна быть удвоена (например, с помощью REPLACE()функция, как в операторе выше).

person Andriy M    schedule 27.04.2012
comment
Большое спасибо за это. Теперь я разделил код на две «части», как вы предлагаете в своем первом варианте выше, следующим образом: - person MarkusBee; 27.04.2012
comment
[Время РЕДАКТИРОВАТЬ предыдущий комментарий истекло.] Большое спасибо. Я разделил код на две части, как вы предлагаете в первом варианте. Первая часть выполнена идеально. Уточню, что процедура выполняется из базы «Аудит», а объекты, для которых требуются триггеры, находятся в других базах. Выполнение оператора CREATE TRIGGER теперь вызывает следующую ошибку, даже при использовании полного имени таблицы: Невозможно создать триггер [...], поскольку цель не находится в текущей базе данных. Есть ли способ обойти это? Как я могу заставить его выполняться в контексте другой базы данных? Спасибо. - person MarkusBee; 27.04.2012
comment
@markb: Пожалуйста, посмотрите мое обновление. Я не уверен, что все так ясно, как хотелось бы, поэтому, пожалуйста, не стесняйтесь спрашивать. - person Andriy M; 27.04.2012
comment
Это превосходно, совершенно ясно и именно то, что мне нужно. Единственная проблема — я проверил в BOL — была с QUOTENAME, где строковый параметр ограничен 128 символами. Ввод более 128 символов возвращает NULL, поэтому я заключил его в кавычки вместо использования функции. Теперь я вижу, как 'EXEC'+@dbname+'..sp_executesql' может быть чрезвычайно мощным и позволять выполнять динамический sql в другом контексте/базе данных. Еще раз большое спасибо за вашу помощь. - person MarkusBee; 27.04.2012
comment
@markb: Ах, действительно, мой плохой. Вы можете заменить функциональность QUOTENAME чем-то вроде '''' + REPLACE(@sql, '''', '''''') + ''''. (Т.е. я думаю, что недостаточно просто заключить @sql в кавычки, но поскольку у вас есть строковые выражения внутри определения триггера, вам также нужно удвоить каждую кавычку.) - person Andriy M; 27.04.2012
comment
+1 Ах, да, я забыл про строки внутри строки! REPLACE() отлично справляется со своей задачей. Все выполняется без заминок! Вы просто сделали мои выходные :) - person MarkusBee; 27.04.2012

создание триггера должно выполняться в его собственном пакете выполнения. Вы находитесь внутри процедуры, поэтому вы не сможете ее создать.

Я предлагаю добавить @sql во временную таблицу, а затем, когда процесс завершит генерацию всех операторов, зациклить эту временную таблицу, чтобы выполнить их и создать триггеры.

person Diego    schedule 26.04.2012