zoukankan      html  css  js  c++  java
  • 15. DML, DDL, LOGON 触发器

    触发器可以理解为由特定事件触发的存储过程, 和存储过程、函数一样,触发器也支持CLR,目前SQL Server共支持以下几种触发器:

    1. DML触发器, 表/视图级有效,可由DML语句 (INSERT, UPDATE, DELETE) 触发;

    2. DDL 触发器,数据库级有效,可由DDL语句 (CREATE, ALTER, DROP 等) 触发;

    3. LOGON 触发器, 实例级有效,可由用户账号登录(LOGON)数据库实例时触发;

    . DML触发器

    1. 语句级触发器/行级触发器

    在SQL Server中,从定义来说只有语句级触发器,但如果有行级的逻辑要处理,有两个仅在触发器内有效的表 (inserted, deleted), 存放着受影响的行,可以从这两个表里取出特定的行并自行定义脚本处理;

    在ORACLE中, 对表做一次DML操作产生一次触发,叫语句级触发器,另外还可以通过指定[FOR EACH ROW]子句,对于表中受影响的每行数据均触发,叫行级触发器,原有行用:OLD表示,新行用:NEW表示;

    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

  • 相关阅读:
    c语言中float、double、long double在内存中存储方式
    linux路由表配置
    linux下ip命令用法
    Ajax与Comet
    JavaScript之JSON
    Mustache学习
    微信小程序环境搭建
    JavaScript之模仿块级作用域
    JavaScript之call()和apply()方法详解
    JavaScript引用类型之Array数组之强大的splice()方法
  • 原文地址:https://www.cnblogs.com/seusoftware/p/9120632.html
Copyright © 2011-2022 走看看