Создайте триггер DDL в каждой базе данных на экземпляре 2005 года. - PullRequest
1 голос
/ 02 декабря 2008

Мне нужно создать триггер в каждой базе данных на моем экземпляре SQL 2005. Я настраиваю некоторые триггеры аудита ddl.

Я создаю курсор со всеми именами баз данных и пытаюсь выполнить оператор USE. Кажется, это не меняет базу данных - оператор CREATE TRIGGER просто запускается в adventureworks несколько раз. Другим вариантом будет префикс объекта триггера с именем databasename.dbo.triggername. Это тоже не работает - какое-то ограничение в создании триггеров. Конечно, я мог бы сделать это вручную, но я бы предпочел, чтобы он был написан по сценарию для легкого применения и удаления. У меня есть другие варианты, если я не могу сделать это в сценарии 1 sql, но я хотел бы сделать это простым:)

Вот то, что у меня есть до сих пор - надеюсь, вы найдете ошибку!

--setup stuff...    
CREATE DATABASE DBA_AUDIT
GO 
USE DBA_AUDIT
GO
CREATE TABLE AuditLog
(ID        INT PRIMARY KEY IDENTITY(1,1),
Command    NVARCHAR(1000),
PostTime   DATETIME,
HostName   NVARCHAR(100),
LoginName  NVARCHAR(100)
)
GO

CREATE ROLE AUDITROLE
GO

sp_adduser 'guest','guest','AUDITROLE'
GO

GRANT INSERT ON SCHEMA::[dbo]
TO AUDITROLE

--CREATE TRIGGER IN ALL NON SYSTEM DATABASES

DECLARE @dataname varchar(255),
@dataname_header varchar(255),
@command VARCHAR(MAX),
@usecommand VARCHAR(100)
SET @command = '';

--get the list of database names

DECLARE datanames_cursor CURSOR FOR SELECT name FROM sys.databases 
WHERE name not in ('master', 'pubs', 'tempdb', 'model','msdb')

OPEN datanames_cursor

FETCH NEXT FROM datanames_cursor INTO @dataname
WHILE (@@fetch_status = 0)
BEGIN

PRINT '----------BEGIN---------'

PRINT 'DATANAME variable: ' + @dataname;

EXEC ('USE ' + @dataname);

PRINT 'CURRENT db: ' + db_name();

SELECT @command = 'CREATE TRIGGER DBA_Audit ON DATABASE
FOR DDL_DATABASE_LEVEL_EVENTS
AS
DECLARE @data XML
DECLARE @cmd NVARCHAR(1000)
DECLARE @posttime NVARCHAR(24)
DECLARE @spid NVARCHAR(6)
DECLARE @loginname NVARCHAR(100)
DECLARE @hostname NVARCHAR(100)
SET @data = EVENTDATA()
SET @cmd = @data.value(''(/EVENT_INSTANCE/TSQLCommand/CommandText)[1]'', ''NVARCHAR(1000)'')
SET @cmd = LTRIM(RTRIM(REPLACE(@cmd,'''','''')))
SET @posttime = @data.value(''(/EVENT_INSTANCE/PostTime)[1]'', ''DATETIME'')
SET @spid = @data.value(''(/EVENT_INSTANCE/SPID)[1]'', ''nvarchar(6)'')
SET @loginname = @data.value(''(/EVENT_INSTANCE/LoginName)[1]'',
    ''NVARCHAR(100)'')
SET @hostname = HOST_NAME()
INSERT INTO [DBA_AUDIT].dbo.AuditLog(Command, PostTime,HostName,LoginName)
 VALUES(@cmd, @posttime, @hostname, @loginname);'

 EXEC (@command);
 FETCH NEXT FROM datanames_cursor INTO @dataname;

PRINT '----------END---------'

END
CLOSE datanames_cursor
DEALLOCATE datanames_cursor

OUTPUT:
----------BEGIN---------
DATANAME variable: adventureworks
CURRENT db: master
Msg 2714, Level 16, State 2, Procedure DBA_Audit, Line 18
There is already an object named 'DBA_Audit' in the database.
----------END---------
----------BEGIN---------
DATANAME variable: SQL_DBA
CURRENT db: master
Msg 2714, Level 16, State 2, Procedure DBA_Audit, Line 18
There is already an object named 'DBA_Audit' in the database.
----------END---------

EDIT: Я уже попробовал подход sp_msforeachdb

Msg 111, Level 15, State 1, Line 1
'CREATE TRIGGER' must be the first statement in a query batch.

EDIT:

Вот мой окончательный код - этот точный скрипт не был протестирован, но он работает примерно на 100 базах данных. Ура! * * 1013

Одно предупреждение - ваши базы данных должны быть в режиме совместимости 90 (в опциях для каждого дБ), в противном случае вы можете начать получать ошибки. Учетная запись в части инструкции EXECUTE AS также должна иметь доступ для вставки в вашу таблицу администраторов.

USE [SQL_DBA]
GO
/****** Object:  Table [dbo].[DDL_Login_Log]    Script Date: 03/03/2009 17:28:10 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[DDL_Login_Log](
    [DDL_Id] [int] IDENTITY(1,1) NOT NULL,
    [PostTime] [datetime] NOT NULL,
    [DB_User] [nvarchar](100) NULL,
    [DBName] [nvarchar](100) NULL,
    [Event] [nvarchar](100) NULL,
    [TSQL] [nvarchar](2000) NULL,
    [Object] [nvarchar](1000) NULL,
 CONSTRAINT [PK_DDL_Login_Log] PRIMARY KEY CLUSTERED 
(
    [DDL_Id] ASC,
    [PostTime] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
--This creates the trigger on the model database so all new DBs get it
USE [model]
GO
/****** Object:  DdlTrigger [ddl_DB_User]    Script Date: 03/03/2009 17:26:13 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TRIGGER [ddl_DB_User] 
ON DATABASE
FOR DDL_DATABASE_SECURITY_EVENTS
AS 

DECLARE @data XML
declare @user nvarchar(100)

SET @data = EVENTDATA()
select @user = convert(nvarchar(100), SYSTEM_USER)

execute as login='domain\sqlagent'
INSERT sql_dba.dbo.DDL_Login_Log 
   (PostTime, DB_User, DBName, Event, TSQL,Object) 
   VALUES 
   (@data.value('(/EVENT_INSTANCE/PostTime)[1]', 'nvarchar(100)'), 
   @user,
    db_name(),
    @data.value('(/EVENT_INSTANCE/EventType)[1]', 'nvarchar(100)'), 
   @data.value('(/EVENT_INSTANCE/TSQLCommand/CommandText)[1]','nvarchar(max)'),
    @data.value('(/EVENT_INSTANCE/ObjectName)[1]', 'nvarchar(1000)')
)

GO
SET ANSI_NULLS OFF
GO
SET QUOTED_IDENTIFIER OFF
GO


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
--CREATE TRIGGER IN ALL NON SYSTEM DATABASES

DECLARE @dataname varchar(255),
@dataname_header varchar(255),
@command VARCHAR(MAX),
@usecommand VARCHAR(100)
SET @command = '';
DECLARE datanames_cursor CURSOR FOR SELECT name FROM sys.databases 
WHERE name not in ('master', 'pubs', 'tempdb', 'model','msdb')
OPEN datanames_cursor
FETCH NEXT FROM datanames_cursor INTO @dataname
WHILE (@@fetch_status = 0)
BEGIN

PRINT '----------BEGIN---------'

PRINT 'DATANAME variable: ' + @dataname;

EXEC ('USE ' + @dataname);

PRINT 'CURRENT db: ' + db_name();

SELECT @command = 'CREATE TRIGGER DBA_Audit ON DATABASE
FOR DDL_DATABASE_LEVEL_EVENTS
AS
DECLARE @data XML
DECLARE @cmd NVARCHAR(1000)
DECLARE @posttime NVARCHAR(24)
DECLARE @spid NVARCHAR(6)
DECLARE @loginname NVARCHAR(100)
DECLARE @hostname NVARCHAR(100)
SET @data = EVENTDATA()
SET @cmd = @data.value(''(/EVENT_INSTANCE/TSQLCommand/CommandText)[1]'', ''NVARCHAR(1000)'')
SET @cmd = LTRIM(RTRIM(REPLACE(@cmd,'''','''')))
SET @posttime = @data.value(''(/EVENT_INSTANCE/PostTime)[1]'', ''DATETIME'')
SET @spid = @data.value(''(/EVENT_INSTANCE/SPID)[1]'', ''nvarchar(6)'')
SET @loginname = @data.value(''(/EVENT_INSTANCE/LoginName)[1]'',
    ''NVARCHAR(100)'')
SET @hostname = HOST_NAME()
INSERT INTO [DBA_AUDIT].dbo.AuditLog(Command, PostTime,HostName,LoginName)
 VALUES(@cmd, @posttime, @hostname, @loginname);'

 EXEC (@command);
 FETCH NEXT FROM datanames_cursor INTO @dataname;
PRINT '----------END---------'
END
CLOSE datanames_cursor
DEALLOCATE datanames_cursor

--------------------------------------------------------------------------------
--------------------------------------------------------------------------------

----Disable all triggers when things go haywire
sp_msforeachdb @command1='use [?]; IF  EXISTS (SELECT * FROM sys.triggers WHERE name = N''ddl_DB_User'' AND parent_class=0)disable TRIGGER [ddl_DB_User] ON DATABASE'

Ответы [ 6 ]

2 голосов
/ 02 декабря 2008

Когда вы используете EXEC (), каждое использование имеет свой собственный контекст. Итак, когда вы делаете EXEC ('USE MyDB'), он переключается на MyDB для этого контекста, затем команда заканчивается, и вы возвращаетесь к тому, с чего начали. Есть несколько возможных решений ...

Вы можете вызвать sp_executesql с именем базы данных (например, MyDB..sp_executesql), и он будет работать в этой базе данных. Хитрость заключается в том, чтобы позволить вам делать это динамически, поэтому вы в основном оборачиваете это дважды так:

DECLARE @cmd NVARCHAR(2000), @my_db VARCHAR(255)

SET @my_db = 'MyDatabaseName'

SET @cmd = 'DECLARE @my_cmd NVARCHAR(2000); SET @my_cmd = ''CREATE TRIGGER DBA_Audit ON DATABASE
FOR DDL_DATABASE_LEVEL_EVENTS
AS
DECLARE @data XML
DECLARE @cmd NVARCHAR(1000)
DECLARE @posttime NVARCHAR(24)
DECLARE @spid NVARCHAR(6)
DECLARE @loginname NVARCHAR(100)
DECLARE @hostname NVARCHAR(100)
SET @data = EVENTDATA()
SET @cmd = @data.value(''''(/EVENT_INSTANCE/TSQLCommand/CommandText)[1]'''', ''''NVARCHAR(1000)'''')
SET @cmd = LTRIM(RTRIM(REPLACE(@cmd,'''''''','''''''')))
SET @posttime = @data.value(''''(/EVENT_INSTANCE/PostTime)[1]'''', ''''DATETIME'''')
SET @spid = @data.value(''''(/EVENT_INSTANCE/SPID)[1]'''', ''''nvarchar(6)'''')
SET @loginname = @data.value(''''(/EVENT_INSTANCE/LoginName)[1]'''',
    ''''NVARCHAR(100)'''')
SET @hostname = HOST_NAME()
INSERT INTO [DBA_AUDIT].dbo.AuditLog(Command, PostTime,HostName,LoginName)
 VALUES(@cmd, @posttime, @hostname, @loginname);''; EXEC ' + @my_db + '..sp_executesql @my_cmd'

EXEC (@cmd)

Другой вариант состоит в том, чтобы сделать это как двухэтапный процесс, когда первый шаг генерирует и распечатывает фактический код с операторами USE и всем, а затем вы запускаете этот сгенерированный код.

1 голос
/ 02 декабря 2008

Вам не нужно создавать КУРСОР ...

sp_msforeachdb 'USE ?; PRINT ''Hello ?'''

РЕДАКТИРОВАТЬ: "ИСПОЛЬЗОВАТЬ?" часть состоит в том, чтобы переключиться на указанную базу данных ... вы можете поместить оператор IF, чтобы убедиться, что имя базы данных соответствует желаемому.

0 голосов
/ 10 августа 2015

Это немного более грубая сила, но вы можете сделать базовый цикл и распечатать код, который вы хотели бы выполнить, а затем выполнить код в отдельном шаге. Нет необходимости без необходимости перепрыгивать через обручи, чтобы использовать sp_msforeachdb ... при условии, что, возможно, существует ограничение на количество адресованных баз данных, что сделает этот подход нецелесообразным, но может помочь кому-то, работающему с меньшим набором баз данных:

`- Сначала получите список имен баз данных, отфильтруйте все, что вам не нужно, в предложении WHERE:

select name 
into #d 
from sys.databases 

- Затем, используя ваш список сверху, переберите и напечатайте CREATE TRIGGER CODE для каждой базы данных:

DECLARE @dbname varchar(100)
DECLARE @Trig VARCHAR(max)
select @dbname = (select top 1 name FROM #d order by name asc)

WHILE @dbname IS NOT NULL
BEGIN 
SET @Trig = 'USE ' + @dbname +'; 
GO

CREATE TRIGGER [DBA_KillerTrigger]
ON DATABASE
AFTER DDL_DATABASE_LEVEL_EVENTS
AS
/*...Trigger magic goes here...*/
GO

ENABLE TRIGGER [DBA_KillerTrigger] ON DATABASE
GO


PRINT @Trig

SELECT @dbname = (select top 1 name FROM #d where name > @dbname order by name asc)

END
0 голосов
/ 10 апреля 2009

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

CREATE TRIGGER trgMonitorNewDB НА ВСЕХ СЕРВЕРАХ ДЛЯ OBJECT_ACTION

см. Также текст ссылки

0 голосов
/ 03 декабря 2008

Другим возможным решением может быть создание триггера в базе данных модели, таким образом, все базы данных будут наследовать it.

0 голосов
/ 02 декабря 2008

Первое, что я бы попробовал, это поместить команду 'USE' в строку @command, хотя, если он жалуется, что триггер ddl должен быть первым в пакете, это вряд ли сработает.

У вас есть доступ к Visual Studio? Это было бы довольно быстро, чтобы закодировать как консольное приложение C #, оставив вам исполняемый файл, который вы можете запустить в любое время. Не такой прозрачный, как SQL-скрипт, но вы можете хранить исходный код рядом с ним.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...