(4.30)全面瞭解觸發器:DML、DDL、LOGON觸發器

DML、DDL、LOGON觸發器html

轉自:http://www.javashuo.com/article/p-yedvcmxy-du.htmlsql

 

觸發器能夠理解爲由特定事件觸發的存儲過程, 和存儲過程、函數同樣,觸發器也支持CLR,目前SQL Server共支持如下幾種觸發器:數據庫

1. DML觸發器, 表/視圖級有效,可由DML語句 (INSERT, UPDATE, DELETE) 觸發;服務器

2. DDL 觸發器,數據庫級有效,可由DDL語句 (CREATE, ALTER, DROP 等) 觸發;session

3. LOGON 觸發器, 實例級有效,可由用戶帳號登陸(LOGON)數據庫實例時觸發;oracle

 

. DML觸發器app

1. 語句級觸發器/行級觸發器ide

在SQL Server中,從定義來講只有語句級觸發器,但若是有行級的邏輯要處理,有兩個僅在觸發器內有效的表 (inserted, deleted), 存放着受影響的行,能夠從這兩個表裏取出特定的行並自行定義腳本處理;函數

在ORACLE中, 對錶作一次DML操做產生一次觸發,叫語句級觸發器,另外還能夠經過指定[FOR EACH ROW]子句,對於表中受影響的每行數據均觸發,叫行級觸發器,原有行用:OLD表示,新行用:NEW表示;spa

 

2. BEFORE/AFTER/INSTEAD OF

在SQL Server中,從定義來講只有AFTER/INSTEAD OF觸發器,在表上支持AFTER觸發器,在表/視圖上支持INSTEAD OF觸發器,對於BEFORE觸發器的需求能夠嘗試經過INSEAD OF觸發器來實現;

SQL Server DML Trigger

BEFORE

AFTER

INSTEAD OF

TABLE

N/A

VIEW

N/A

N/A

在ORACLE中,在表上支持BEFORE/AFTER觸發器,在視圖上支持INSTEAD OF觸發器,好比ORACLE中沒法直接對視圖作DML操做,能夠經過INSTEAD OF觸發器來變樣完成;

ORACLE DML Trigger

BEFORE

AFTER

INSTEAD OF

TABLE

N/A

VIEW

N/A

N/A

 

3. 觸發條件

(1) 不能觸發的狀況

對於UPDATE,DELETE操做而言,均會觸發觸發器;而對於INSERT或者說IMPORT的狀況,是能夠控制不去觸發的。

  • 大批量導入操做,如:BULK INSERT, bcp/INSERT... SELECT * FROM OPENROWSET,都有FIRE_TRIGGERS/IGNORE_TRIGGERS選項,能夠設置是否觸發觸發器;
  • 導入導出嚮導/SSIS,若是目標是表,也有FIRE_TRIGGERS的設置選項;
  • 另外truncate操做也不會觸發;

(2) 嵌套觸發器 (Nested Triggers), 循環/遞歸觸發器 (Recursive Triggers)

嵌套觸發器,就是一次操做觸發了一個觸發器,而後觸發器裏的語句繼續觸發其餘觸發器,若是繼續回頭觸發了本身,那麼就是遞歸觸發器。

對於AFTER觸發器有個兩個開關分別控制嵌套觸發和遞歸觸發:

exec sp_configure 'nested triggers'

這個參數默認值爲1, 也就是說容許AFTER觸發器嵌套,最多嵌套32層,設爲0就是不容許AFTER觸發器嵌套,以下:

exec sp_configure 'nested triggers',0
RECONFIGURE

但這個參數有兩個另外:

  • INSTEAD OF觸發器,能夠嵌套,不受這個參數開關與否影響;
  • AFTER觸發器,即便打開該選項,也不會本身嵌套本身(即遞歸),除非打開了RECURSIVE_TRIGGERS選項,也就是循環/遞歸觸發器;
複製代碼
--create table, sql server 2016 & higher
drop table if exists A
GO
create table A(id int)
GO

--create DML trigger
drop trigger if exists tri_01
GO
create TRIGGER tri_01
ON A
AFTER INSERT, UPDATE, DELETE 
as
begin
    if @@NESTLEVEL = 32
    begin
        return
    end 
    insert A values(0)
end
GO

--check nested triggers server option
exec sp_configure 'nested triggers'
--name    minimum    maximum    config_value    run_value
--nested triggers    0    1    1    1

--test with RECURSIVE_TRIGGERS off
ALTER DATABASE dba set RECURSIVE_TRIGGERS off
select is_recursive_triggers_on, * from sys.databases 
GO
insert A values(1)
select * from A
--id
--1
--0

--test with RECURSIVE_TRIGGERS on
ALTER DATABASE dba set RECURSIVE_TRIGGERS on
select is_recursive_triggers_on, * from sys.databases 
GO

truncate table A
insert A values(1)
select * from A --32 rows

--若是沒有加@@NESTLEVEL判斷並退出,會出現32層限制的報錯,而且表裏不會插入任何數據
/*
Msg 217, Level 16, State 1, Procedure tri_01, Line 10
Maximum stored procedure, function, trigger, or view nesting level exceeded (limit 32).

select * from A --0 rows
*/

--刪表會級聯刪除觸發器,就像索引
drop table A
複製代碼

 

循環/遞歸觸發器的前提就是嵌套觸發器,只有容許嵌套了才能夠遞歸(遞歸也就是嵌套並觸發本身),遞歸有直接和間接兩種狀況:

  • 直接遞歸:就是A表的DML觸發器再回來對A表進行DML操做,如上例;
  • 間接遞歸:就是A表DML觸發器去操做B表,而後B表上觸發器回來操做A表,以下例;
複製代碼
--create table, sql server 2016 & higher
drop table if exists A
drop table if exists B
GO
create table A(id int)
create table B(id int)
GO

--create DML trigger
drop trigger if exists tri_01
drop trigger if exists tri_02
GO
create TRIGGER tri_01
ON A
AFTER INSERT, UPDATE, DELETE 
as
begin
    if @@NESTLEVEL = 32
    begin
        return
    end 
    insert B values(0)
end
GO

create TRIGGER tri_02
ON B
AFTER INSERT, UPDATE, DELETE 
as
begin
    if @@NESTLEVEL = 32
    begin
        return
    end 
    insert A values(0)
end
GO

--test with nested triggers server option ON
exec sp_configure 'nested triggers',1
RECONFIGURE

--test with RECURSIVE_TRIGGERS off
ALTER DATABASE dba set RECURSIVE_TRIGGERS off
select is_recursive_triggers_on, * from sys.databases 
GO

truncate table A
truncate table B
insert A values(1)
select * from A --16 rows
select * from B --16 rows

--test with RECURSIVE_TRIGGERS on
ALTER DATABASE dba set RECURSIVE_TRIGGERS on
select is_recursive_triggers_on, * from sys.databases 
GO

truncate table A
truncate table B
insert A values(1)
select * from A --16 rows
select * from B --16 rows

--test with nested triggers server option OFF
exec sp_configure 'nested triggers',0
RECONFIGURE

--test with RECURSIVE_TRIGGERS off
ALTER DATABASE dba set RECURSIVE_TRIGGERS off
select is_recursive_triggers_on, * from sys.databases 
GO

truncate table A
truncate table B
insert A values(1)
select * from A --1
select * from B --0

--test with RECURSIVE_TRIGGERS on
ALTER DATABASE dba set RECURSIVE_TRIGGERS on
select is_recursive_triggers_on, * from sys.databases 
GO

truncate table A
truncate table B
insert A values(1)
select * from A --1
select * from B --0

--刪表會級聯刪除觸發器,就像索引
drop table A, B
複製代碼
  • 能夠看出數據庫選項RECURSIVE_TRIGGERS,僅對直接遞歸有效,對間接遞歸無效;能夠經過Nest Triggers的開關來控制是否容許嵌套,從而控制是否容許間接遞歸;
  • 不論直接遞歸,仍是間接遞歸,遞歸次數都有32次嵌套的上限;

總結下來:

1. AFTER觸發器,默認Nest Triggers值爲1,即容許觸發器嵌套,上限32層,間接遞歸也是能夠的,直接遞歸須要開啓數據庫選項RECURSIVE_TRIGGERS;

2. INSTEAD OF觸發器,不受Nest Triggers選項影響,都可以嵌套,上限32層,間接遞歸也是能夠的,直接遞歸不管是否開啓數據庫選項RECUSIVE_TRIGGERS,都無效;把上面兩個腳本示例中的AFTER改成INSTEAD OF便可演示。

 

4. 觸發器中沒法commit/rollback事務

複製代碼
--create table, sql server 2016 & higher
drop table if exists A
GO
create table A(id int)
GO

--create DML trigger
drop trigger if exists tri_01
GO
create TRIGGER tri_01
ON A
AFTER INSERT, UPDATE, DELETE 
as
begin
    if @@NESTLEVEL = 32
    begin
        return
    end 
    insert A values(0)
    commit
end
GO

begin tran
insert A values(1)
/*
Msg 3609, Level 16, State 1, Procedure tri_01, Line 10
The transaction ended in the trigger. The batch has been aborted.
*/
複製代碼

在SQL Server和Oracle中都是這樣,觸發器做爲整個事務的一部分存在,可是並不控制整個事務的提交/回滾,爲保證數據一致性,事務邏輯由觸發器外層的語句來控制。

 

. DDL觸發器

SQL Server 2005開始支持DDL觸發器,它不僅限於對CREATE/ALTER/DROP操做有效,支持的DDL事件還有好比:權限的GRANT/DENY/REVOEK, 對象的RENAME, 更新統計信息等等,可經過DMV查看更多支持的事件類型以下:

select * from sys.trigger_event_types
where type_name not like '%CREATE%'
  and type_name not like '%ALTER%'
  and type_name not like '%DROP%'

注意:

1. TRUNCATE不在DDL觸發器的事件類型中,SQL Server中將Truncate 歸爲DML操做語句,雖然它也並不觸發DML觸發器,就像開啓開關的大批量導入操做 (Bulk Import Operations) 同樣;

2. DDL觸發器中捕獲的信息都由EVENTDATA()函數返回,返回類型爲XML格式,須要用XQuery來讀取;

 

代碼示例1:記錄全部table上的某些DDL操做

複製代碼
--記錄全部create table操做
if OBJECT_ID('ddl_log','U') is not null
    drop table ddl_log
GO

create table ddl_log
(
LogID        int identity(1,1),
EventType    varchar(50), 
ObjectName   varchar(256),
ObjectType   varchar(25),
TSQLCommand  varchar(max),
LoginName    varchar(256)
)
GO

if exists(select * from sys.triggers where name = 'TABLE_DDL_LOG' and parent_class_desc = 'DATABASE')
    drop trigger TABLE_DDL_LOG on database;
GO

create trigger TABLE_DDL_LOG
on database
for create_table
as
begin
    set nocount on 

    declare @data xml
    set @data = EVENTDATA()

    insert into ddl_log
    values
    (@data.value('(/EVENT_INSTANCE/EventType)[1]', 'varchar(50)'), 
    @data.value('(/EVENT_INSTANCE/ObjectName)[1]', 'varchar(256)'), 
    @data.value('(/EVENT_INSTANCE/ObjectType)[1]', 'varchar(25)'), 
    @data.value('(/EVENT_INSTANCE/TSQLCommand)[1]', 'varchar(max)'), 
    @data.value('(/EVENT_INSTANCE/LoginName)[1]', 'varchar(256)')
    )
end
GO

drop table if exists test_dll_trigger;
create table test_dll_trigger (id int)
select * from ddl_log
複製代碼

 

代碼示例2:禁止特定角色的用戶對特定的表作DROP操做

複製代碼
IF exists(select * from sys.triggers where name = 'NO_DROP_TABLE' and parent_class_desc = 'DATABASE')
    DROP TRIGGER [NO_DROP_TABLE] ON DATABASE;
GO

CREATE TRIGGER NO_DROP_TABLE
ON DATABASE
FOR DROP_TABLE
AS
BEGIN
    DECLARE @x                XML,
            @user_name        varchar(100),
            @db_name          varchar(100),  
            @schema_name      varchar(100),
            @object_name      varchar(200)

    --select eventdata()
    SET @x = EVENTDATA();
    SET @user_name = @x.value('(/EVENT_INSTANCE/UserName)[1]','varchar(100)');
    SET @db_name = @x.value('(/EVENT_INSTANCE/DatabaseName)[1]','varchar(100)');
    SET @schema_name = @x.value('(/EVENT_INSTANCE/SchemaName)[1]','varchar(100)');
    SET @object_name = @x.value('(/EVENT_INSTANCE/ObjectName)[1]','varchar(100)');

    --PRINT 'Current User: '     + @user_name
    --PRINT 'Current Database: ' + @db_name
    --PRINT 'Schema Name: '      + @schema_name
    --PRINT 'Table Name: '       + @object_name

    IF is_rolemember('disallow_modify_tables',@user_name) = 1
       AND @db_name = 'YOUR_DB_NAME'
       AND @schema_name = 'YOUR_SCHEMA_NAME'
       AND @object_name like 'YOUR_TABLE_NAME%'
    BEGIN 
        PRINT 'Dropping tables is not allowed'
        ROLLBACK
    END
END
GO
複製代碼

 

. LOGON 觸發器

SQL Server 2005在SP2中悄悄引入了LOGON觸發器,做爲一個實例級的對象,它的系統視圖,定義語句和DDL/DML觸發器都是分開的。

select * from sys.server_triggers where name = 'login_history_trigger'
select * from sys.server_trigger_events
select OBJECT_ID('login_history_trigger') --沒法獲取

在SQL Server中,顧名思義,LOGON觸發器,只支持LOGON事件;

在ORACLE中,實例級觸發器可支持更多事件 (SERVERERROR, LOGON, LOGOFF, STARTUP, or SHUTDOWN)。

 

代碼示例1: 記錄全部login登陸歷史 (其實也能夠經過修改login auditing選項,來記錄成功和失敗的登陸在errorlog裏)

複製代碼
IF OBJECT_ID('login_history','U') is not null
    DROP TABLE login_history
GO

CREATE TABLE login_history
(
FACT_ID         bigint IDENTITY(1,1) primary key,
LOGIN_NAME      nvarchar(1024),
LOGIN_TIME      datetime
)
GO

IF EXISTS(select 1 from sys.server_triggers where name = 'login_history_trigger')
    DROP TRIGGER login_history_trigger ON ALL SERVER
GO

CREATE TRIGGER login_history_trigger
ON ALL SERVER
FOR LOGON
AS
BEGIN
    --IF SUSER_NAME() NOT LIKE 'NT AUTHORITY\%' AND 
    --   SUSER_NAME() NOT LIKE 'NT SERVICE\%'
    IF ORIGINAL_LOGIN() NOT LIKE 'NT AUTHORITY\%' AND
       ORIGINAL_LOGIN() NOT LIKE 'NT SERVICE\%'
    BEGIN
        INSERT INTO DBA..login_history
        VALUES(ORIGINAL_LOGIN(),GETDATE());
    END;
END;
GO

--view login history after logon
SELECT * FROM login_history
複製代碼

 

代碼示例2: 限制特定用戶在特定時間範圍登陸、限制鏈接數

複製代碼
--限制下班時間不能登陸
DROP TRIGGER IF EXISTS limit_user_login_time ON ALL SERVER
GO
CREATE TRIGGER limit_user_login_time
ON ALL SERVER FOR LOGON 
AS
BEGIN
    IF ORIGINAL_LOGIN() = 'TestUser' 
       AND (DATEPART(HOUR, GETDATE()) < 9 OR DATEPART (HOUR, GETDATE()) > 18)
    BEGIN
        PRINT 'TestUser can only login during working hours!'
        ROLLBACK
    END
END
GO

--限制鏈接數
DROP TRIGGER IF EXISTS limit_user_connections ON ALL SERVER
GO
CREATE TRIGGER limit_user_connections
ON ALL SERVER 
WITH EXECUTE AS 'sa'
FOR LOGON
AS
BEGIN
    IF ORIGINAL_LOGIN() = 'TestUser' 
       AND (SELECT COUNT(*) FROM   sys.dm_exec_sessions
            WHERE  Is_User_Process = 1 
            AND Original_Login_Name = 'TestUser') > 2
    BEGIN
        PRINT 'TestUser can only have 1 active session!'
        ROLLBACK
    END
END
複製代碼

 

注意:若是LOGON觸發器把全部人都鎖在外面了怎麼辦?

Logon failed for login 'TestUser' due to trigger execution.

這時,只能經過DAC登陸SQL Server去禁用LOGON觸發器/修改邏輯以容許登陸,DAC登陸方式有遠程和本地兩種,遠程登陸須要經過sp_configure 開啓remote admin connections ,若是沒有事先開啓,那就只能選擇本地登陸方式:

服務器本地,在SSMS中經過DAC登陸

 

服務器本地,在cmd中經過DAC登陸

--禁用/啓用LOGON觸發器
DISABLE TRIGGER limit_user_connections ON ALL SERVER
ENABLE TRIGGER limit_user_connections ON ALL SERVER

 

參考:

CREATE TRIGGER (Transact-SQL)

https://docs.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql?view=sql-server-2017

Create Nested Triggers

https://docs.microsoft.com/en-us/sql/relational-databases/triggers/create-nested-triggers?view=sql-server-2017

Transact-SQL statements

https://docs.microsoft.com/en-us/sql/t-sql/statements/statements?view=sql-server-2017

Why we can‘t use commit in trigger, can anyone give proper explanation

https://community.oracle.com/thread/1082134

Database PL/SQL Language Reference, Using Triggers

https://docs.oracle.com/cd/B28359_01/appdev.111/b28370/triggers.htm#LNPLS020

相關文章
相關標籤/搜索