Oracle 數據庫(十五)—— Oracle 觸發器和程序包

一、Oracle 觸發器

觸發器可以看做是一種“特殊”的存儲過程,它定義了一些在數據庫相關事件(如INSERT、UPDATE、CREATE等事件)發生時應執行的“功能代碼塊”,通常用於管理複雜的完整性約束,或監控對錶的修改,或通知其他程序,甚至可以實現對數據的審計功能。

1.1 觸發器簡介

1.1.1 觸發器介紹
在觸發器中有一個不得不提的概念——觸發事件,觸發器正是通過這個“觸發事件”來執行的(而存儲過程的調用或執行是由用戶或應用程序進行的)。

能夠引起觸發器運行的操作就被稱爲“觸發事件”,如執行DML語句(使用INSERT、UPDATE、DELETE語句對錶或視圖執行數據處理操作),執行DDL語句(使用CREATE、ALTER、DROP語句在數據庫中創建、修改、刪除模式對象),引發數據庫系統事件(如系統啓動或退出、產生異常錯誤等),引發用戶事件(如登錄或退出數據庫操作),以上這些操作都可以引起觸發器的運行。

1.1.2 觸發器語法

create [or replace] trigger tri_name
    [before | after | instead of] tri_event
    on table_name | view_name | user_name | db_name
      [for each row [when tri_condition]
begin
    plsql_sentences;
end tri_name;
  • trigger:表示創建觸發器的關鍵字,就如同創建存儲過程的關鍵字procedure一樣。
  • tri_name:觸發器的名稱,如果數據庫中已經存在了此名稱,則可以指定“or replace”關鍵字,這樣新的觸發器將覆蓋掉原來的觸發器。
  • before | after | instead of:表示“觸發時機”的關鍵字。
    • Before表示在執行DML等操作之前觸發,這種方式能夠防止某些錯誤操作發生而便於回滾或實現某些業務規則;
    • after表示在DML等操作之後發生,這種方式便於記錄該操作或某些事後處理信息;
    • instead of表示觸發器爲替代觸發器。
  • tri_event:觸發事件,比如常用的有INSERT、UPDATE、DELETE、CREATE、ALTER、DROP等。
  • on:表示操作的數據表、視圖、用戶模式和數據庫等,對它們執行某種數據操作(比如對錶執行INSERT、ALTER、DROP等操作),將引起觸發器的運行。
  • table_name | view_name | user_name | db_name:分別表示操作的數據表、視圖、用戶模式和數據庫,對它們的某些操作將引起觸發器的運行。
  • when tri_condition:這是一個觸發條件子句,其中when是關鍵字,tri_condition表示觸發條件表達式,只有當該表達式的值爲true時,遇到觸發事件纔會自動執行觸發器,使其執行觸發操作,否則即便是遇到觸發事件也不會執行觸發器。
  • for each row:指定觸發器爲行級觸發器,當DML語句對每一行數據進行操作時都會引起該觸發器的運行。如果未指定該條件,則表示創建語句級觸發器,這時無論數據操作影響多少行,觸發器都只會執行一次。
  • plsql_sentences:PL/SQL語句,它是觸發器功能實現的主體。

1.1.3 Oracle 觸發器分類
Oracle的觸發事件相對於其他數據庫而言比較複雜,比如上面提到過的DML操作、DDL操作,甚至是一些數據庫系統的自身事件等都會引起觸發器的運行。爲此,這裏根據觸發器的觸發事件和觸發器的執行情況,將Oracle所支持的觸發器分爲以下5種類型:

  • 行級觸發器:當DML語句對每一行數據進行操作時都會引起該觸發器的運行。
  • 語句級觸發器:無論DML語句影響多少行數據,其所引起的觸發器僅執行一次。
  • 替換觸發器:該觸發器是定義在視圖上的,而不是定義在表上,它是用來替換所使用實際語句的觸發器。
  • 用戶事件觸發器:是指與DDL操作或用戶登錄、退出數據庫等事件相關的觸發器。如用戶登錄到數據庫或使用ALTER語句修改表結構等事件的觸發器。
  • 系統事件觸發器:是指在Oracle數據庫系統的事件中進行觸發的觸發器,如Oracle實例的啓動與關閉。

1.2 語句級觸發器

1.2.1 語句級觸發器介紹
語句級觸發器,顧名思義就是針對一條DML語句而引起的觸發器執行。在語句級觸發器中,不使用for each row子句,也就是說無論數據操作影響多少行,觸發器都只會執行一次。

1.2.2 語句級觸發器示例
(1)本實例要實現的主要功能是使用觸發器在SCOTT模式下針對dept表的各種操作進行監控,爲此首先需要創建一個日誌表dept_log,它用於存儲對dept表的各種數據操作信息,比如操作種類(如插入、修改、刪除操作),操作時間等。

create table dept_log
(
    operate_tag varchar2(10),         --定義字段,存儲操作種類信息
    operate_time date                 --定義字段,存儲操作日期
);

創建結果
在這裏插入圖片描述

(2)然後創建一個關於emp表的語句級觸發器,將用戶對dept表的操作信息保存到dept_log表中。

create or replace trigger tri_dept
	--創建觸發器,當 dept 表發生插入,修改,刪除操作時引起該觸發器執行
	before insert or update or delete
	on dept                   
declare
	--聲明一個變量,存儲對 dept 表執行的操作類型
	var_tag varchar2(10);     
begin
	if inserting then         --當觸發事件是 INSERT 時
		var_tag := '插入';    --標識插入操作
	elsif updating then       --當觸發事件是 UPDATE 時
		var_tag := '修改';    --標識修改操作
	elsif deleting then       --當觸發事件是 DELETE 時
		var_tag := '刪除';    --標識刪除操作
	end if;
	insert into dept_log
	values(var_tag,sysdate);  --向日志表中插入對 dept 表的操作信息
end tri_dept;
/

創建結果
在這裏插入圖片描述在上面的代碼中,使用BEFORE關鍵字來指定觸發器的“觸發時機”,它指定當前的觸發器在DML語句執行之前被觸發,這使得它非常適合於強化安全性、啓用業務邏輯和進行日誌信息記錄。當然也可以使用AFTER關鍵字,它通常用於記錄該操作或者做某些事後處理工作。具體使用哪一種關鍵字,要根據實際需要而定。

另外,爲了具體判斷對dept表執行了何種操作—即具體引發了哪種“觸發事件”,代碼中還使用了條件謂詞,它由條件關鍵字(IF或ELSIF)和謂詞(inserting、updating、deleting)組成,如果條件謂詞的值爲true,那麼就是相應類型的DML語句(insert、update、delete)引起了觸發器的運行。條件謂詞通用的語法格式如下:

if inserting then              --如果執行了插入操作,即觸發了 insert 事件
    do somting about insert
elsif updating then            --如果執行了修改操作,即觸發了 update 事件
    do somting about update
elsif deleting then            --如果執行了刪除操作,即觸發了 delete 事件
    do somting about delete
end if;

另外,對於條件謂詞,用戶甚至還可以在其中判斷特定列是否被更新,例如要判斷用戶是否對dept表中dname列進行了修改,可以使用下面的語句:

if updating(dname) then       --若修改了 dept 表中的 dname 列
    ...
end if;

在上面的條件謂詞中,即使用戶修改了dept表中的數據,但卻沒有對dname列的值進行修改,那麼該條件謂詞的值仍然爲false,這樣相關的執行語句就不會得到執行。

(3)在創建完畢觸發器之後,接下來就是執行觸發器,但它的觸發執行與存儲過程截然不同,存儲過程的執行是由用戶或應用程序進行的,而它必須由一定的“觸發事件”來誘發執行。比如,對dept表執行插入(insert事件)、修改(update事件)、刪除(delete事件)等操作,都會引起tri_dept觸發器的運行。

insert into dept values(66,'業務諮詢部','長春');

update dept set loc='瀋陽' where deptno=66;

delete from dept where deptno=66;

操作結果
在這裏插入圖片描述上面的代碼對dept表執行了3次DML操作,這樣根據tri_dept觸發器自身的設計情況,其會被觸發3次,並且會向dept_log表中插入3條操作記錄。

(4)通過上面的3條DML語句,讓觸發器執行了3次,接下來就可以到dept_log表中查看日誌信息了。

select * from dept_log;

執行查看結果
在這裏插入圖片描述

1.3 行級別觸發器

1.3.1 行級觸發器介紹
行級觸發器會針對DML操作所影響的每一行數據都執行一次觸發器。創建這種觸發器時,必須在語法中使用for each for這個選項。使用行級觸發器的一個典型應用就是給數據表生成主鍵值,下面就來講解這個典型應用的實現過程。

1.3.2 行級觸發器示例
(1)爲了使用行級觸發器生成數據表中的主鍵值,首先需要創建一個帶有主鍵列的數據表,來看下面的例子。

create table goods
(
id int primary key,
good_name varchar2(50)
);

創建結果
在這裏插入圖片描述
在上面的代碼中,id列就是goods表的主鍵,因爲在創建該列時指定了“primary key”關鍵字,主鍵列的值要求不能重複,這一點很重要。

(2)爲了給goods表的id列生成不能重複的有序值,這裏需要創建一個序列

create sequence seq_id;

創建結果
在這裏插入圖片描述
上面的代碼創建了序列seq_id,用戶可以在SQL/PL程序中調用它的nextval屬性來獲取一系列有序的數值,這些數值就可以作爲goods表的主鍵值。

(3)在創建了數據表goods和序列seq_id之後,至此準備工作已經完成,下面來創建一個觸發器,用於爲goods表的id列賦值。

create or replace trigger tri_insert_good
	before insert
	on goods                --關於 goods 數據表,在向其插入新紀錄之前,引起該觸發器的運行
	for each row            --創建行級觸發器
begin
	select seq_id.nextval
	into :new.id
	from dual;              --從序列中生成一個新的數值,賦值給當前插入行的 id 列
end;
/

創建結果
在這裏插入圖片描述在上面的代碼中,爲了創建行級的觸發器,使用了for each row選項;爲了給goods表的當前插入行的id列賦值,這裏使用了“:new.id”關鍵字——也稱爲“列標識符”,這個列標識符用來指向新行的id列,給它賦值,就相當於給當前行的id列賦值,下面對這個“列標識符”相關的知識進行講解。

在行級觸發器中,可以訪問當前正在受到影響(添加、刪除、修改等操作)的數據行,這就可以通過“列標識符”來實現。列標識符可以分爲“原值標識符”和“新值標識符”,原值標識符用於標識當前行某個列的原始值,記作“:old.column_name”(如:old.id),通常在UPDATE語句中和DELETE語句使用,因爲在INSERT語句中新插入的行沒有原始值;新值標識符用於標識當前行某個列的新值,記作“:new.column_name”(如:new.id),通常在INSERT語句和UPDATE語句中被使用,因爲DELETE語句中被刪除的行無法產生新值。

(4)在觸發器創建完畢之後,用戶可以通過向goods表中插入數據來驗證觸發器是否被執行,同時也能夠驗證該行級觸發器是否能夠使用序列爲表的主鍵賦值。

insert into goods(good_name) values('蘋果');

insert into goods(id,good_name) values(9,'葡萄');

操作結果
在這裏插入圖片描述
(5)最後使用SELECT語句來檢索goods表中的數據行,從而驗證設計本實例的初衷。

select * from goods;

查看結果
在這裏插入圖片描述
從運行結果中可以看到兩條完整的數據記錄,而且還可以看到主鍵id的值是連續的自然數。雖然在第二次插入數據行時指定了id的值(即9),但這並沒有起任何作用,這是因爲在觸發器中將序列seq_id的nextval屬性值賦給了“:new.id”列標識符,這個列標識符的值就是當前插入行的id列的值,並且nextval屬性值是連續不間斷的。

1.4 替換觸發器

1.4.1 替換觸發器介紹
替換觸發器—即instead of觸發器,它的“觸發時機”關鍵字是INSTEAD OF,而不是BEFORE或AFTER。與其他類型觸發器不同的是,替換觸發器是定義在視圖(一種數據庫對象,在後面章節中會講解到)上的,而不是定義在表上。

由於視圖是由多個基表連接組成的邏輯結構,所以一般不允許用戶進行DML操作(如insert、update、delete等操作),這樣當用戶爲視圖編寫“替換觸發器”後,用戶對視圖的DML操作實際上就變成了執行觸發器中的PL/SQL語句塊,這樣就可以通過在“替換觸發器”中編寫適當的代碼對構成視圖的各個基表進行操作。

1.4.2 替換觸發器示例
(1)爲了創建並使用替換觸發器,首先需要創建一個視圖。

connect system/manager

--授權
grant create view to scott;

connect scott/tiger

create view view_emp_dept
as select empno,ename,dept.deptno,dname,job,hiredate
	 from emp,dept
	where emp.deptno= dept.deptno;

執行結果
在這裏插入圖片描述(2)接下來編寫一個關於view_emp_dept視圖在insert事件中的觸發器

create or replace trigger tri_insert_view
	--創建一個關於 view_emp_dept 視圖的替換觸發器
	instead of insert
	on view_emp_dept                           
	--行級視圖
	for each row                               
declare
	row_dept dept%rowtype;
begin
	--檢索指定部門編號的記錄行
	select * into row_dept from dept where deptno = :new.deptno;
	--未檢索到該部門編號的記錄
	if sql%notfound then                       
		--向 dept 表中插入數據
		insert into dept(deptno, dname)
		values(:new.deptno, :new.dname);      
	end if;
	--向 emp 表中插入數據
	insert into emp(empno, ename, deptno, job, hiredate)
	values(:new.empno, :new.ename, :new.deptno, :new.job, :new.hiredate);				
end tri_insert_view;
/

執行結果
在這裏插入圖片描述
在上面觸發器的主體代碼中,如果新插入行的部門編號(deptno)不在dept表中,則首先向dept表中插入關於新部門編號的數據行,然後再向emp表中插入記錄行,這是因爲emp表的外鍵值(emp.deptno)是dept表的主鍵值(dept.deptno)。

(3)當觸發器tri_insert_view成功創建之後,再向view_emp_dept視圖中插入數據時,Oracle就不會產生錯誤信息,而是引起觸發器“tri_insert_view”的運行,從而實現向emp表和dept表中插入兩行數據。

insert into view_emp_dept(empno,ename,deptno,dname,job,hiredate)
values(8888,'東方',10,'ACCOUNTING','CASHIER',sysdate);

select * from view_emp_dept where empno= 8888;

執行結果
在這裏插入圖片描述

在上面代碼的INSERT語句中,由於在dept表中已經存在部門編碼(deptno)爲10的記錄,所以觸發器中的程序只向emp表中插入一條記錄;若指定的部門編碼不存在,則首先要向dept表中插入一條記錄,然後再向emp表中插入一條記錄。

1.5 用戶事件觸發器

1.5.1 用戶事件觸發器介紹
用戶事件觸發器是因進行DDL操作或用戶登錄、退出等操作而引起運行的一種觸發器,引起該類型觸發器運行的常見用戶事件包括:CREATE、ALTER、DROP、ANALYZE、COMMENT、GRANT、REVOKE、RENAME、TRUNCATE、SUSPEND、LOGON和LOGOFF等。

1.5.2 用戶事件觸發器示例
(1)首先創建一個日誌信息表,用於保存DDL操作的信息,實例如下:

create table ddl_oper_log
(
	db_obj_name varchar2(20),      --數據對象名稱
	db_obj_type varchar2(20),      --對象類型
	oper_action varchar2(20),      --具體 ddl 行爲
	oper_user varchar2(20),        --操作用戶
	oper_date date                 --操作日期
);

執行結果
在這裏插入圖片描述

(2)創建一個用戶觸發器,用於將當前模式下的DDL操作信息保存到上面所創建的ddl_oper_log日誌信息表中。

create or replace trigger tri_ddl_oper
	--在 scott 模式下,在創建、修改、刪除數據對象之前將引發該觸發器運行
	before create or alter or drop
	on scott.schema               
begin
	insert into ddl_oper_log values(
		ora_dict_obj_name,        --操作的數據對象名稱
		ora_dict_obj_type,        --對象類型
		ora_sysevent,             --系統事件名稱
		ora_login_user,           --登錄用戶
		sysdate);
end;
/

執行結果
在這裏插入圖片描述
上面代碼中,當向日志表ddl_oper_log插入數據時,使用了若干個事件屬性,它們各自的含義如下:

  • ora_dict_obj_name:獲取DDL操作所對應的數據庫對象。
  • ora_dict_obj_type:獲取DDL操作所對應的數據庫對象的類型。
  • ora_sysevent:獲取觸發器的系統事件名。
  • ora_login_user:獲取登錄用戶名。

通過上面的4個事件屬性值和sysdate系統屬性就可以將scott用戶的DDL操作信息獲取出來,最後再把這些信息保存到ddl_oper_log日誌表中,以備隨時查看。

(3)在創建完觸發器之後,爲了引起觸發器的執行,就要在SCOTT模式下進行DDL操作。

create table tb_test(id number);

create view view_test as select empno,ename from emp;

alter table tb_test add(name varchar2(10));

drop view view_test;

select * from ddl_oper_log;

執行結果
在這裏插入圖片描述從上面的運行結果可以看出,用戶scott的DDL操作信息都被存儲到ddl_oper_log日誌表中,這些信息就是由DDL操作引起觸發器運行而保存到日誌表中的。

1.6 刪除觸發器

當一個觸發器不再使用時,要從內存中刪除它,例如:

DROP TRIGGER my_trigger;

當一個觸發器已經過時,想重新定義時,不必先刪除再創建,同樣只需在CREATE語句後面加上OR REPLACE關鍵字即可。如:

CREATE OR REPLACE TRIGGER my_trigger;

二、Oracle 程序包

程序包由PL/SQL程序元素(如變量、類型)和匿名PL/SQL塊(如遊標)、命名PL/SQL塊(如存儲過程和函數)組成。程序包可以被整體加載到內存中,這樣就可以大大加快程序包中任何一個組成部分的訪問速度。

2.1 程序包的規範

2.1.1 程序包介紹
程序包由PL/SQL程序元素(如變量、類型)和匿名PL/SQL塊(如遊標)、命名PL/SQL塊(如存儲過程和函數)組成。程序包可以被整體加載到內存中,這樣就可以大大加快程序包中任何一個組成部分的訪問速度。

實際上程序包對於用戶來說並不陌生,在PL/SQL程序中使用DBMS_OUTPUT.PUT_LINE語句就是程序包的一個具體應用,其中,DBMS_OUTPUT是程序包,而PUT_LINE就是其中的一個存儲過程,程序包通常由規範和包主體組成。

2.1.2 程序包語法格式
該“規範”用於規定在程序包中可以使用哪些變量、類型、遊標和子程序(指各種命名的PL/SQL塊),需要注意的是程序包一定要在“包主體”之前被創建,其語法格式如下:

create [or replace] package pack_name is
	[declare_variable];
	[declare_type];
    [declare_cursor];
    [declare_function];
    [declare_ procedure];
end [pack_name];
  • pack_name:程序包的名稱,如果數據庫中已經存在了此名稱,則可以指定“or replace”關鍵字,這樣新的程序包將覆蓋掉原來的程序包。
  • declare _variable:規範內聲明的變量。
  • declare _type:規範內聲明的類型。
  • declare _cursor:規範內定義的遊標。
  • declare _function:規範內聲明的函數,但僅定義參數和返回值類型,不包括函數體。
  • declare _procedure:規範內聲明的存儲過程,但僅定義參數,不包括存儲過程主體。

2.1.3 創建程序包的“規範”示例

create or replace package pack_emp is
	--獲取指定部門的平均工資
	function fun_avg_sal(num_deptno number) return number;    
	 --按照指定比例上調指定職務的工資           
	procedure pro_regulate_sal(var_job varchar2,num_proportion number); 
end pack_emp;
/

執行結果
在這裏插入圖片描述
從上面的代碼中可以看到,在“規範”中聲明的函數和存儲過程只有頭部的聲明,而沒有函數體和存儲過程主體,這正是規範的特點。

注意 只定義了“規範”的程序包還不可以使用,此時如果試圖在PL/SQL塊中通過程序包的名稱來調用其中的函數或存儲過程,Oracle將會產生錯誤提示。

2.2 程序包的主體

2.2.1 程序包的主體介紹
程序包的主體包含了在規範中聲明的遊標、過程和函數的實現代碼,另外,也可以在程序包的主體中聲明一些內部變量。程序包主體的名稱必須與規範的名稱相同,這樣通過這個相同的名稱Oracle就可以將“規範”和“主體”結合在一起組成程序包,並實現一起進行編譯代碼。

在實現函數或存儲過程主體時,可以將每一個函數或存儲過程作爲一個獨立的PL/SQL塊來處理。

2.2.2 程序包的主體語法
與創建“規範”不同的是,創建程序包主體使用CREATE PACKAGE BODY語句,而不是CREATE PACKAGE語句,這一點需要讀者注意,創建程序包主體的代碼如下:

create [or replace] package body pack_name is
        [inner_variable]
        [cursor_body]
        [function_title]
        {begin
            fun_plsql;
        [exception]
            [dowith _ sentences;]
        end [fun_name]}
        [procedure_title]
        {begin
            pro_plsql;
        [exception]
            [dowith _ sentences;]
        end [pro_name]}
    …
    end [pack_name];
  • pack_name:程序包的名稱,要求與“規範”對應的程序包名稱相同。
  • inner_variable:程序包主體的內部變量。
  • cursor_body:遊標主體。
  • function_title:從“規範”中引入的函數頭部聲明。
  • fun_plsql:PL/SQL語句,這裏是函數主要功能的實現部分。從begin到end部分就是函數的body。
  • dowith _ sentences:異常處理語句。
  • fun_name:函數的名稱。
  • procedure_title:從“規範”中引入的存儲過程頭部聲明。
  • pro_plsql:PL/SQL語句,這裏是存儲過程主要功能的實現部分。從begin到end部分就是存儲過程的body。
  • pro_name:存儲過程的名稱。

2.2.3 程序包的主體示例

create or replace package body pack_emp is
	--引入“規範”中的函數
	function fun_avg_sal(num_deptno number) return number is
	--定義內部變量
	num_avg_sal number; 
begin
	select avg(sal)
	into num_avg_sal
	from emp
	where deptno= num_deptno; 
	--計算某個部門的平均工資並返回平均工資
	return(num_avg_sal);                                      
exception
	when no_data_found then 
		dbms_output.put_line('該部門編號不存在僱員記錄');
		return 0; 
	end fun_avg_sal;
	--引入“規範”中的存儲過程
	procedure pro_regulate_sal(var_job varchar2,num_proportion number) is
	begin
		update emp
		set sal = sal*(1+num_proportion)
		--爲指定的職務調整工資
		where job = var_job; 
		end pro_regulate_sal;
end pack_emp;
/

執行結果
在這裏插入圖片描述在創建了程序包的“規範”和“主體”之後,就可以像普通的存儲過程和函數一樣實施調用了。

set serveroutput on
declare
	num_deptno emp.deptno%type;                         --定義部門編號變量
	var_job emp.job%type;                               --定義職務變量
	num_avg_sal emp.sal%type;                           --定義工資變量
	num_proportion number;                              --定義工資調整比例變量
begin
	num_deptno:=10;                                     --設置部門編號爲 10
	num_avg_sal:=pack_emp.fun_avg_sal(num_deptno);      --計算部門編號爲 10 的平均工資
	dbms_output.put_line(num_deptno||'號部門的平均工資是:'||num_avg_sal);--輸出平均工資
	var_job:='SALESMAN';                                --設置職務名稱
	num_proportion:=0.1;                                --設置調整比例
	pack_emp.pro_regulate_sal(var_job,num_proportion);  --調整指定部門的工資
end;
/

執行結果
在這裏插入圖片描述
總結一下使用一個程序包的過程就是:首先創建程序包的“規範”,然後再創建程序包的“主體”,最後在PL/SQL塊或SQL*Plus中調用程序包中的子程序——即函數或存儲過程。

2.3 刪除程序包

與函數和過程一樣,當一個包不再使用時,要從內存中刪除它,語法:

DROP PACKAGE my_package

示例

drop package pack_emp;

執行結果
在這裏插入圖片描述

當一個包已經過時,想重新定義時,不必先刪除再創建,同樣只需在CREATE語句後面加上OR REPLACE關鍵字即可,如:

CREATE OR REPLACE PACKAGE my_package

參考文獻:

  1. Oracle 11g從入門到精通 第二版,明日科技 著,清華大學出版社有限公司
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章