目录
1.存储过程
2 函数
3.触发器
4.程序包
本次实验
前面PL/SQL编程基础中的PL/SQL程序块都是匿名的,其中包含的代码无法保存到oracle数据库中。oracel提供了4种可以存储PL/SQL块:过程、函数、触发器、包
1.存储过程
一种命名的PL/SQL程序块,参数可有可无,通常无返回值,存储过程被保存在数据库中(已经编译好的),因此执行效率很高。
1.1创建存储过程
create [or replace] procedure 过程名 [(参数1[,参数2]...)] is|as
begin
plsql语句;
[exception]
[异常处理语句;]
end [过程名];说明:没有declare,需要关键字create [or replace] procedure
输入参数其后要指定in关键字 输出参数其后要指定out关键字
eg:创建一个存储过程,改存储过程实现向dept表中插入一条记录
set serveroutput on; create procedure pro_insertDept is begin insert into dept values(77,'市场拓展部','JILIN'); commit; dbms_output.put_line('插入记录成功!'); end pro_insertDept; /
如果已经存在名为pro_insertDept的存储过程,会报错
此时可以加上or replace 覆盖掉原来的过程,如下:
set serveroutput on; create or replace procedure pro_insertDept is begin insert into dept values(77,'市场拓展部','JILIN'); commit; dbms_output.put_line('插入记录成功!'); end pro_insertDept; /
注:
1.错误处理
若创建过程存在错误,默认的保错信息很简单,就一行:警告: 创建的过程带有编译错误。
此时可以接着执行一行show errors;输出详细的报错信息。
如上面的 dbms_output.put_line 写错了2.执行
execute 过程名;
execut 过程名;
exec 过程名;3.调用执行:PL/SQL程序块中调用某个过程 (不需要exec关键字了)
eg:在PL/SQL程序块中调用存储过程pro_insertDept,然后执行这个PL/SQL块。set serveroutput on; begin pro_insertDept; end; /
1.2存储过程的参数
1.in模式参数
输入参数,也是默认参数模式,执行存储过程时:in参数不能被修改,只能根据被传入的指定值(或者默认值)为存储过程提供数据
eg:创建一个存储过程,并定义3个in模式的变量,然后将这3个变量的值插入dept表中。
create or replace procedure insert_dept( num_deptno in number, --定义in模式的变量,它存储部门编号 var_ename in varchar2, --定义in模式的变量,它存储部门名称 var_loc in varchar2) is begin insert into dept values(num_deptno,var_ename,var_loc); --向dept表中插入记录 commit; end insert_dept; /
参数传递
1.指定名称传递
指定名称传递好处:顺序可打乱(和在表中插入数据指定字段名差不多)格式:过程名(参数1=>值1,[参数2=>值2]...);
调用1:
exec insert_dept(var_ename=>'市场部',var_loc=>'成都',num_deptno=>90);
调用2:
begin insert_dept(var_ename=>'市场部',var_loc=>'成都',num_deptno=>90); end; /
2.按位置传递(按顺序全插入)
exec insert_dept(80,'采购部','成都');
或者:
begin insert_dept(80,'采购部','成都'); end; /
3.混合方式传递
eg:在PL/SQL块中调用存储过程insert_dept,然后使用"按位置传递"方式传入第1个参数值,接下来使用"指定名称传递"方式
传入剩下两个参数值,最后执行当前PL/SQL块,代码运行结果如下:exec insert_dept(38,var_loc=>'济南',var_ename=>'测试部');
注:使用混合模式,只能是前面全部是"按位置传递",后面"指定名称传递",也即是在某个位置使用"指定名称传递"方式传递
参数值后,其后面的参数值也要使用"指定名称传递"(因为"指定名称传递"方式有可能已经破坏了参数原始的定义顺序)
2.out模式参数
参数值可以传递到存储过程以外的环境,在执行存储过程时:out参数只能等待被赋值,而不能像in参数那样为存储过程本身提供数据
eg:创建一个存储过程,要求定义两个out模式的字符类型的参数,一个输入参数deptno,然后将在dept表中根据部门号检索到的一行部门信息存储到两个输出参数中。
create or replace procedure select_dept( num_deptno in number, --输入参数 var_dname out dept.dname%type, --输出参数存储部门名称并输出 var_loc out dept.loc%type) is begin select dname,loc into var_dname,var_loc from dept where deptno=num_deptno; exception when no_data_found then --若select语句无返回记录 dbms_output.put_line('该部门编号的不存在'); end select_dept; /
1.pl/sql块中调用out模式(有out参数)的存储过程
需要在declare部分声明和存储过程中out参数类型兼容的若干变量
eg:首先要在pl/sql块中声明若干变量,然后调用select_dept存储过程,并将定义的变量传入改存储过程,以便接收out参数的返回值。set serveroutput on; declare var_dname dept.dname%type; var_loc dept.loc%type; begin select_dept(90,var_dname,var_loc); dbms_output.put_line(var_dname||'位于:'||var_loc); end; /
2.使用exec命令执行out模式的存储过程
使用exec命令需要在sql*plus环境中使用variable关键字声明两个变量,用以存储out参数的返回值。
eg:使用variable关键字声明两个变量,分别用来存储部门名称和位置信息,然后使用exec命令执行
存储过程,并传入声明的两个变量来接收out参数的返回值,代码及执行结果如下。variable var_dname varchar2(50); variable var_loc varchar2(50); exec select_dept(90,:var_dname,:var_loc);
--想要查看结果还需要主动打印变量值
print var_dname var_loc;
col :VAR_LOC for a20; col :VAR_DNAME for a20; select :var_loc,:var_dname from dual;
3. in out模式参数
既可以做输入参数,也可以做输出参数,即既可以接收外界传递的参数值,也可以做为返回值传递给外界create or replace procedure pro_square( num in out number, --计算它的平方或平方根,这是一个"in out"参数 flag in boolean) is --计算平方或平方根的标识,这是一个"in"参数 i int:=2; --存储过程没有declare 直接定义内部变量了 begin if flag then num:=power(num,i); else num:=sqrt(num); end if; end; /
执行
eg:调用存储过程pro_square,计算某个数的平方根或平方。declare var_number number; var_temp number; bool_flag boolean; begin var_temp :=3; var_number :=var_temp;--此时角色是输入参数 输出语句内的角色是输出参数 bool_flag :=false;--false表示计算平方根 true表示计算平方 pro_square(var_number,bool_flag);--调用存储过程 if bool_flag then dbms_output.put_line(var_temp||'的平方是:'||var_number); else dbms_output.put_line(var_temp||'的平方根是:'||var_number); end if; end; /
false改成true:
1.3 IN参数的默认值
实际上,oracle支持在声明in参数的同时给其初始化默认值,这样在存储过程调用时,如果没有向in参数传入值,则存储过程可以使用默认值进行操作。
eg:创建一个存储过程,定义3个IN参数,并将其中的两个参数设置初始默认值,然后将这个3个IN参数的值插入dept表中。
create or replace procedure insert_dept( num_deptno in number, --in参数 需要传入值 var_dname in varchar2 default '综合部', --in参数 可以不传入值,此时使用的就是默认值 var_loc in varchar2 default '北京') is begin insert into dept values(num_deptno,var_dname,var_loc); end; /
调用:
eg:在pl/sql块中调用insert_dept存储过程,并且只向该存储过程传入两个参数值declare row_dept dept%rowtype; begin commit; insert_dept(91,var_loc=>'太原');--有默认参数时使用"指定名称传递"的方式最安全 var_dname使用默认值'综合部' select * into row_dept from dept where deptno=91; dbms_output.put_line('部门名称:《'||row_dept.dname||'》,地点在《'||row_dept.loc||'》'); end; /
2 函数
函数经常用于计算和返回一个值,可以将经常需要使用的计算或功能写成一个函数
2.1创建函数和调用
和存储过程极其相似,唯一不同:函数有返回值,存储过程没有
格式:主要关键字为function 声明有"return 返回值类型" 调用有 "return(返回值类型的变量或值)"
create or replace function 函数名[(参数1[,参数2]...) return 数据类型 is --return 数据类型指定返回值类型
[内部变量] --没有declare语句,直接在此处声明
begin
plsql语句;
[exception]
[异常处理语句;]
end [函数名]eg:创建一个函数,用于计算emp表中某个指定部门的平均工资
create or replace function get_avg_pay(num_deptno number) return number is num_avg_pay number; begin select avg(sal) into num_avg_pay from emp where deptno=num_deptno;--某个部门的平均工资 return(round(num_avg_pay,2)); exception when no_data_found then dbms_output.put_line('该部门编号的不存在'); return(0);--返回平均工资为0 end; /
调用:和c语言一样,函数执行表达式就是返回值
set serveroutput on; declare avg_pay number; begin avg_pay:=get_avg_pay(10); dbms_output.put_line('10号部门的平均工资是:'||avg_pay); end; /
调用2:
select get_avg_pay(&no) 平均工资 from dual;
2.2删除函数
drop function 函数名;
eg:
drop function get_avg_pay;
3.触发器
触发器可以看做是一种"特殊"的存储过程,它定义了一些与数据库相关事件(如insert,update,create等事件)发生时应执行
的"功能代码块",通常用于管理复杂的完整性约束,或监控对表的修改,或通知其他程序,甚至可以实现对数据的审计功能。
3.1触发器简介
触发器是由"触发事件"触发而执行的,而不像存储过程需要编写代码主动调用才会执行
触发事件:能引起触发器执行的操作被称为"触发事件"
eg:DML(insert,update,delete) DDL(create,alter,drop) 引起数据库系统事件(如程序启动或者退出、产生异常错误等);引发用户事件(如登录或退出数据库操作)
触发器可以看做是一种"特殊"的存储过程,它定义了一些与数据库相关事件(如insert,update,create等事件)发生时应执行
的"功能代码块",通常用于管理复杂的完整性约束,或监控对表的修改,或通知其他程序,甚至可以实现对数据的审计功能。定义触发器的语法格式:
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] [declare] --触发器允许有declare声明 [var_name var_type] begin plsql语句; end tri_name;
trigger:触发器关键字 触发时机关键字: before:执行DML等操作之前触发 after:执行DML等操作之后触发 instead of:此触发器是替代触发器 on:触发器要操作的对象类型 tri_name:触发器名字 tri_event:触发事件,如常用的有:insert,update,delete,create,alter,drop等 table_name | view_name | user_name | db_name:数据表,视图、用户模式、数据库 when tri_condition:触发条件子句,触发事件事件执行了,且tri_condition表达式为true触发器才会执行 触发事件事件执行了但tri_condition表达式为false,触发器也不会执行 plsql语句:触发器功能实现的主体
触发器事件5中类型:
行级触发器:当DML语句对每一行数据进行操作时都会引起该触发器的运行
语句级触发器:无论DML语句影响多少行数据,其所引起的触发器仅执行一次。
替换触发器:该触发器是定义在视图上的,而不是定义在表上的,它是用来替换所使用实际语句的触发器。
用户事件触发器:是指与DDL操作或用户登录、退出数据库等事件相关的触发器。如用户登录到数据库或使用alter语句修改表结构
系统事件触发器:是指在oracle数据库系统的事件中进行触发的触发器,如oracle实例的启动与关闭。
下面分别介绍这5种类型
3.2语句级触发器
针对一条DML语句而引起的触发器执行。
在语句级触发器中,不使用for each row子句,也即无论数据操作多少行,触发器都只会执行一次eg:监控对dept表的操作,记录操作类型和日期
1) 准备工作,先建立一个dept_log数据表,并在其中定义两个字段,分别用来存储操作种类信息和操作日期create table dept_log( operate_tag varchar2(10), operate_time varchar2(50) );
2) 创建一个触发器tri_dept,该触发器在insert、update和delete事件下都可以被触发,并且操作的数据对象是dept表。
然后要求在触发器执行时输出对dept表所做的具体操作。create or replace trigger tri_dept before insert or update or delete on dept --创建触发器tri_dept 当dept表发生插入、修改、和删除操作时引起该触发器执行 declare var_tag varchar2(10); begin if inserting then var_tag:='插入'; elsif updating then var_tag:='修改'; elsif deleting then var_tag:='删除'; end if; insert into dept_log values(var_tag,sysdate); --向日志表中插入对dept表的操作信息 end tri_dept; /insert into dept values(94,'编码部','芜湖'); update dept set loc='安庆' where deptno=94; delete from dept where deptno=94; select * from dept_log;
inserting、updating、deleting为条件谓词
对于条件谓词,甚至可以在其中判断特定列是否被更新,例如要判断用户是否是对dept表的dname列进行了修改,可以使用下面的语句:
if updating(dname) then
...
end if;
如此,则即使用户修改了dept表中的数据,但没有对dname列的值进行修改,那么该条件谓词的值仍然为false,这样相应的逻辑语句不会执行。
3.3行级别触发器
不言而喻,行级触发器会针对DML操作所影响的每一行数据都执行一次触发器。创建这种触发器时,必须在语法中使用
for each row这个选项。使用行级触发器的一个典型应用就是给数据表生成主键值,下面就来讲解这个典型应用的实现过程。1)准备工作,先建立一个有主键的数据表。
create table goods( id int primary key, good_name varchar2(50) );
2)给goods表的id列生成不能重复的有序值,这里需要创建一个序列(一种数据库对象,后面的内容)
eg:使用create sequence 语句创建一个序列,命名为seq_id,代码及运行结果如下create sequence seq_id;
上面已经建立了序列seq_id,则可以在plsql程序中调用它的nextval属性来获取一系列有序的数值,这些数值就可以作为goods表的主键值了。
3)创建一个行级触发器,该触发器在数据表goods插入数据时被触发,并且在该触发器的主体中实现设置goods表的id列的值
注:
列标识符: 访问当前正在受到影响的数据行
原值标识符:":old.列名" 通常用于update和delete 标识当前行指定列的原始值
新值标识符:":new.列名" 通常用于update和insert 指向新行的指定列,可为其赋值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; /
触发执行:插入时不必插入主键了,由触发器自己维护
insert into goods values(null,'苹果'); insert into goods values(null,'梨子'); insert into goods values(null,'香蕉'); insert into goods values(10,'西瓜'); select * from goods;
3.4替换触发器
替换触发器(即:instead of触发器),是专门定义在视图上的触发器。由于视图是多个基表连接组成的逻辑结构,
一般不允许用户直接进行DML(insert,update,delete等)操作(会报错,下面有演示),当用户为视图编写"替换触发器"后,
用户对视图的DML操作实际上就变成了执行触发器的逻辑代码,此时便可以借助触发器的逻辑对构成视图的各个基表进行操作了,以达到对视图的DML操作。1)准备工作,创建视图
在system模式下,给scott用户授予"create view"权限,然后在scott模式下创建一个检索雇员信息的视图,该视图的基表
包括dept和emp表。--切换管理员身份 connect system/password@orcl --管理员身份下给scott用户授权"create view" grant create view to scott; --切换到scott模式 connect scott/tiger@orcl --建立视图 雇员编号、姓名、部门号、部门名、工作、雇佣日期 create view view_emp_dept as select empno,ename,dept.deptno,dname,job,hiredate from emp,dept where emp.deptno=dept.deptno; --此时若直接对视图执行DML,会报错 insert into view_emp_dept values(8888,'东方',10,'ACCOUNTING','CASHIER',sysdate); --ORA-01776: 无法通过联接视图修改多个基表
2)编写一个关于view_emp_dept视图在insert事件中的触发器(替换触发器,在触发器的plsql语句块中逐表执行insert操作)
--创建一个关于view_emp_dept视图的替换触发器,在该触发器的主体中实现向emp表和dept表中插入两行相互关联的数据create or replace trigger tri_insert_view instead of insert on view_emp_dept --创建一个关于view_emp_dept视图的替换触发器(触发器替换insert操作) for each row --是行级视图 declare row_dept dept%rowtype; begin select * into row_dept from dept where deptno=:new.deptno;--检索指定部门编号的记录行 if sql%notfound then--新插入的行的部门号为:new.deptno 若未检索到这个部门号 说明是新部门,避免外键约束冲突,插入该部门 insert into dept(deptno,dname) values(:new.deptno,:new.dname); --像dept表中插入数据 end if; insert into emp(empno,ename,deptno,job,hiredate) --接着向emp表中插入数据 values(:new.empno,:new.ename,:new.deptno,:new.job,:new.hiredate); end tri_insert_view; /
3)此时对视图view_emp_dept执行insert操作便不会报错了,而是引起触发器tri_insert_view的执行,从而实现分别向
emp表和dept表插入一条记录行insert into view_emp_dept values(8888,'东方',10,'ACCOUNTING','CASHIER',sysdate); select * from view_emp_dept;
改进:部门不存在,select有into。。就直接触发器异常,删去into也不行。。。似乎要捕获异常然后在异常逻辑里执行插入
create or replace trigger tri_insert_view instead of insert on view_emp_dept --创建一个关于view_emp_dept视图的替换触发器 针对insert操作 for each row --行级别触发器 declare row_dept dept%rowtype; --dept表行类型 begin select * into row_dept from dept where deptno=:new.deptno; --检索指定部门(刚刚插入的那个部门)编号的记录行 insert into emp(empno,ename,deptno,job,hiredate) --触发器的逻辑里执行向各个基本表插入数据的逻辑 values(:new.empno,:new.ename,:new.deptno,:new.job,:new.hiredate); --以达到向逻辑视图插入数据的目的(不可以直接insert视图的,因为视图是虚表) exception when no_data_found then insert into dept(deptno,dname) values(:new.deptno,:new.dname); insert into emp(empno,ename,deptno,job,hiredate) values(:new.empno,:new.ename,:new.deptno,:new.job,:new.hiredate); end tri_insert_view; /
3.5用户事件触发器
用户事件触发器是因进行DDL操作或用户登录、退出等操作而引起运行的一种触发器,引起该类型触发器运行的常见用户事件包括:
create、alter、drop、analyze、comment、grant、revoke、rename、truncate、suspend、logon、logoff等。
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日志信息表中
--关于scott用户的DDL操作(这里包括create,alter,drop)创建一个触发器,然后将DDL操作的相关信息插入到ddl_oper_log日志表中 create or replace trigger tri_ddl_oper before create or alter or drop on scott.schema--在scott模式下,在创建、修改、删除数据对象之前将引起该触发器运行 --用户触发器的独特之处 "on scott.schema"<==>"on 用户模式" 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, --获取登录用户名3)scott模式下执行DDL操作,观察触发器执行
--在scott模式下,创建一个数据表和一个视图,然后删除视图和修改表,最后查看ddl_oper_log日志表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;
4.程序包
程序包由PL/SQL程序元素(如变量、类型)和匿名PL/SQL块(如游标)、命名PL/SQL块(如存储过程和函数)组成。
程序包可以被整体加载到内存中,这样就可以大大加快程序包中任何一个组成部分的访问速度。
dbms_output.put_line 语句就是程序包的一个具体应用,其中,dbms_output是程序包,而put_line就是其中的
一个存储过程。程序包通常由规范和包主体组成。
4.1程序包的规范
"规范"用于规定在程序包中可以使用哪些变量、类型、游标和子程序(各种命名的PL/SQL块),需要注意的是:
程序包一定要在"包主体"之前被创建
可见规范很像c++.h文件中对成员函数的声明,也像java里的接口的声明规范格式:
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: 规范内声明的存储过程,但仅仅定义参数,不包括过程主体。eg:创建一个程序包的"规范",首先在该程序包中声明一个可以获取指定部门的平均工资的函数,然后再声明
一个可以实现按照指定比例上调指定职务的工资的存储过程。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; /
4.2程序包的主体
程序包主体包含规范中声明的具体实现,此外也可以在"程序包的主体"中声明一些内部变量。
格式:
create [or replace] package body pack_name is [inner_variable] [cursor_body] [function_title] {begin fun_plsql; [exception] [dowith_sentences;] end [function_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:存储过程的名字eg:创建程序包pack_emp的主体,在该主体中实现对应"规范"中声明的函数和存储过程。
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; /
调用:
eg:创建一个匿名的PL/SQL块,然后通过程序包pack_emp调用其中的函数fun_avg_sal和存储过程pro_regulate_sal,
并输出函数返回结果,代码如下。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); 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块或sqlplus中调用程序包中的子程序--即函数或存储过程。
本次实验