PL/SQL 块是在sql 语句中语言之上发展起来的一种应用,可以集中的处理各种复杂的SQL 操作。
语句格式:
DECLARE:
声明部分
BEGIN
编写主题
EXCEPTION
捕获异常
END
/
PL/SQL 之中也包含了:循环、分支等条件控制语句。
语句格式:
LOOP
循环的语句
EXIT WHEN 终止条件
循环条件必须更改
END LOOP ;
循环输出 1~10:
DECLARE
cou NUMBER ;
BEGIN
-- 必须给一个初始值
cou := 1;
LOOP
DBMS_OUTPUT.put_line('cou = ' || cou);
EXIT WHEN cou > 10;
cou := cou + 1;
END LOOP;
END ;
/
--此循环会先执行一次再进行循环
格式:
while(判断循环的条件) loop
循环的语句;
循环条件的改变;
END loop;
循环输出 1~10:
DECLARE
cou NUMBER ;
BEGIN
-- 必须给一个初始值
cou := 1;
WHILE(cou < 10) LOOP
DBMS_OUTPUT.put_line('cou = ' || cou);
cou := cou + 1;
END LOOP;
END;
/
--此循环会先判断再执行语句
格式:
FOR 变量名 in 变量的初始值..结束值 lOOP
循环语句;
END loop;
循环输出 1~10:
DECLARE
cou NUMBER ;
BEGIN
--此语句会自动将1到10赋值给cou
FOR cou in 1..10 loop
DBMS_OUTPUT.put_line('cou = ' || cou);
END LOOP;
END ;
/
格式:
IF 条件 THEN
满足条件时, 执行此语句
END IF;
案例:
DECLARE
cou NUMBER ;
BEGIN
cou := 11;
IF cou > 10 THEN
DBMS_OUTPUT.put_line('cou = ' || cou);
end if;
END ;
/
--输出11
格式:
IF 条件 THEN
满足条件时, 执行此语句
END IF;
案例:
DECLARE
cou NUMBER ;
BEGIN
cou := 8;
IF cou > 10 THEN
DBMS_OUTPUT.put_line('cou = ' || cou);
ELSE
DBMS_OUTPUT.put_line('cou = ' || cou);
end if;
END ;
/
--输出8
格式:
IF 条件 THEN
满足条件时, 执行此语句
ELSIF 条件 THEN
满足此条件时,执行此语句
ELSE
END IF;
案例:
DECLARE
cou NUMBER ;
BEGIN
cou := 8;
IF cou > 10 THEN
DBMS_OUTPUT.put_line('cou = ' || cou);
ELSIF cou < 5 THEN
DBMS_OUTPUT.put_line('cou = ' || cou);
ELSE
DBMS_OUTPUT.put_line('cou = ' || cou);
end if;
END ;
/
--输出8
格式:
IF 条件 THEN
满足条件时, 执行此语句
ELSIF 条件 THEN
满足此条件时,执行此语句
ELSE
END IF;
案例:
DECLARE
emo EMPLOYEE.EMP00%TYPE;
name EMPLOYEE.EMP01%TYPE;
BEGIN
emo := '70443BA4-7174-46CC-8A7D-6DA04627B770';
SELECT EMP01 INTO name FROM EMPLOYEE WHERE EMP00 = emo;
IF name='周康' THEN
GOTO po1;
ELSIF name='白安' THEN
GOTO po2;
ELSE
GOTO po3;
end if;
<<po1>>
DBMS_OUTPUT.put_line('po1');
<<po2>>
DBMS_OUTPUT.put_line('po2');
<<po3>>
DBMS_OUTPUT.put_line('po3');
END ;
/
游标是一种PL/SQL控制结构,可以对SQL 语句的处理进行显示控制,便于对表的行数据进行逐条处理。游标并不是一个数据库对象,只是存留在内存中。
操作步骤:
DECLARE
emp EMPLOYEE.EMP00%TYPE; --定义一个参数
empInfo EMPLOYEE%rowtype; --定义一个rowType, 用来保存一整行数据
BEGIN
emp := '7350F9DE-F5BC-461A-B499-F86680D2DA4E';
SELECT * INTO empInfo FROM EMPLOYEE WHERE EMP00 = emp;
DBMS_OUTPUT.PUT_LINE('姓名=' || empInfo.EMP01); --可以通过emoInfo. 的形式来访问这一行中的所有字段
end;
DECLARE
CURSOR mycur IS SELECT * FROM EMPLOYEE; --List(EMPLOYEE)
empInfo EMPLOYEE%rowtype;
BEGIN
--游标操作使用循环, 但是在操作之前必须先将游标打开
OPEN mycur;
--使游标下移一行
FETCH mycur INTO empInfo;
--判断此行是否有数据被发现
WHILE (mycur%FOUND) LOOP
DBMS_OUTPUT.put_line('姓名=' || empInfo.EMP01);
--修改游标往下
FETCH mycur INTO empInfo;
end loop;
end;
/
--表中所有人的姓名都被输出了
现要给所有部门的员工上涨工资,根据他们所在的部门涨工资,规则如下:
10%
20%
30%
DECLARE
cursor mycur is SELECT * FROM emp;
empInfo emp%rowtype;
s emp.sal%TYPE;
BEGIN
for empInfo in mycur loop
if empInfo.deptno = 10 then
s := empInfo.sal * 1.1;
elsif empInfo.deptno = 20 then
s := empInfo.sal * 1.2;
elsif empInfo.deptno = 30 then
s := empInfo.sal * 1.3;
end if;
if s > 5000 then
s = 5000;
end if;
UPDATE emp SET sal = s WHERE emp00 = empInfo.emp00;
end loop;
end;
/
函数就是一个有返回值的过程。定义一个函数,此函数可以根据传进来的员工编号查出该员工目前的岗位。
CREATE OR REPLACE FUNCTION "function"(emp IN varchar2) RETURN VARCHAR2
AS
DMCPT VARCHAR2(1024);
BEGIN
SELECT DMCPT INTO DMCPT FROM EMPLOYEE LEFT JOIN DM07 ON EMPLOYEE.EMP11 = DM07.DMCOD WHERE EMP00 = emp;
RETURN DMCPT;
end;
/
SELECT "function"('7350F9DE-F5BC-461A-B499-F86680D2DA4E') FROM DUAL;
函数可以用来处理一些复杂的逻辑业务,相当于把代码用sql 的方式给写出来,同时也可以将一些通用的转格式之类的代码抽象成一个函数。可参考:返回指定日期与当前日期的时间差函数
与过程相比,存储过程是存在数据库中的一个对象。如果编译错误,可以用show errors or show errors procedure myproc
。
定义一个简单的存储过程:
CREATE OR REPLACE PROCEDURE myproc1 --PROCEDURE n.过程, 程序
AS
i NUMBER;
BEGIN
i := 100;
DBMS_OUTPUT.PUT_LINE('i=' || i); --调用存储过程, 直接输出100
END;
/
如果是命令行窗口的话,exec myproc1
就可以调用存储过程了,如果是sql 窗口下的话需要用到PLSQL语句
DECLARE
定义参数;
BEGIN
myproc1; --如果存储过程有参, 则需要传参
END;
编写一个存储过程,可以传入人员的编号、姓名、性别(0,1代替),之后调用此存储过程就可以完成人员的增加操作。
CREATE OR REPLACE PROCEDURE myproc2(ENO EMPLOYEE.EMP00%TYPE, NAME EMPLOYEE.EMP01%TYPE, XB EMPLOYEE.EMP02%TYPE)
AS
cou NUMBER;
BEGIN
--首先判断该编号是否被使用, 如果被使用了就不能添加
SELECT COUNT(EMP00) INTO cou FROM EMPLOYEE WHERE EMP00 = ENO;
IF cou = 0 THEN
--可以添加
INSERT INTO EMPLOYEE(EMP00, EMP01, EMP02) VALUES (ENO, NAME, XB);
DBMS_OUTPUT.PUT_LINE('人员插入成功!');
ELSE
DBMS_OUTPUT.PUT_LINE('该人员编号已存在, 无法插入!');
END IF;
END;
/
begin
myproc2('7350F9DE-F5BC-461A-B499-F86680D2DA4F', '阴阳人', '1');
end;
存储过程的参数类型,在指定参数类型时添加,类似myproc2(ENO IN OUT EMPLOYEE.EMP00%TYPE)
:
查看所有存储过程:
SELECT * FROM USER_SOURCE;
删除存储过程:
DROP PROCEDURE myproc2;
存放在数据库中,并被隐含执行的存储过程。在Oracle 8i之前,只允许给予表和视图的DML操作,从 8i 之后,不仅可以支持DML触发器,也允许给予系统事件和DDL的操作。
INSERT OR UPDATE OR DELETE
操作前进行过滤:--禁止在休息日改变员工信息
CREATE OR REPLACE TRIGGER TR_SRC_EMP --TRIGGER n.触发器
BEFORE INSERT OR UPDATE OR DELETE
ON EMP
BEGIN
IF TO_CHAR(SYSDATE, 'DY', 'NLS_DATE_LANGUAGE=AMERICAN') IN ('SAT', 'SUN') THEN
RAISE_APPLICATION_ERROR(-20001, 'CAN‟T MODIFY USER INFORMATION IN WEEKEND');
END IF;
END;
/
--可以监听此处操作是什么类型
CREATE OR REPLACE TRIGGER TR_SRC_EMP
BEFORE INSERT OR UPDATE OR DELETE
ON EMP
BEGIN
IF TO_CHAR(SYSDATE, 'DY') IN ('星期六', '星期天') THEN
CASE
WHEN INSERTING THEN
RAISE_APPLICATION_ERROR(-20001, 'FAIL TO INSERT');
WHEN UPDATING THEN
RAISE_APPLICATION_ERROR(-20001, 'FAIL TO UPDATE');
WHEN DELETING THEN
RAISE_APPLICATION_ERROR(-20001, 'FAIL TO DELETE');
END CASE;
END IF;
END;
/
--为了统计在EMP表中增、删、改的次数,建立一张统计表
CREATE TABLE AUDIT_TABLE
(
NAME VARCHAR2(20),
INS INT,
UPD INT,
DEL INT,
STARTTIME DATE,
ENDTIME DATE
);
--建立触发器, 分类统计增删改的次数
CREATE OR REPLACE TRIGGER TR_AUDIT_EMP
AFTER INSERT OR UPDATE OR DELETE
ON EMP
DECLARE
V_TEMP INT;
BEGIN
SELECT COUNT(*) INTO V_TEMP FROM AUDIT_TABLE WHERE NAME ='EMP';
IF V_TEMP = 0 THEN
INSERT INTO AUDIT_TABLE VALUES ('EMP', 0, 0, 0, SYSDATE, NULL); --如果没有EMP表的记录, 那就新建一条
END IF;
CASE
WHEN INSERTING THEN
UPDATE AUDIT_TABLE SET INS=INS + 1, ENDTIME=SYSDATE WHERE NAME = 'EMP'; --如果是新增操作, 就将INS+1, 其他类型同理
WHEN UPDATING THEN
UPDATE AUDIT_TABLE SET UPD=UPD + 1, ENDTIME=SYSDATE WHERE NAME = 'EMP';
WHEN DELETING THEN
UPDATE AUDIT_TABLE SET DEL= DEL + 1, ENDTIME=SYSDATE WHERE NAME = 'EMP';
END CASE;
END;
/
--更新员工工资时, 新工资不能低于原来的工资
CREATE OR REPLACE TRIGGER TR_EMP_SAL
BEFORE UPDATE OF SAL --工资字段的更新触发器
ON EMP
FOR EACH ROW --为每行建立行触发器
BEGIN
IF :NEW.SAL < :OLD.SAL THEN --如果新工资小于旧工资
RAISE_APPLICATION_ERROR(-20010, 'SAL SHOULD NOT BE LESS');
END IF;
END;
/
--新建一张过程表, 统计员工的工资变化
CREATE TABLE AUDIT_EMP_CHANGE
(
NAME VARCHAR2(10),
OLDSAL NUMBER(6, 2),
NEWSAL NUMBER(6, 2),
TIME DATE
);
--建立触发器, 统计员工工资变化
CREATE OR REPLACE TRIGGER TR_SAL_SAL
AFTER UPDATE OF SAL --工资更新之后触发
ON EMP
FOR EACH ROW
DECLARE
V_TEMP INT;
BEGIN
SELECT COUNT(*) INTO V_TEMP FROM AUDIT_EMP_CHANGE WHERE NAME = :OLD.ENAME;
IF V_TEMP = 0 THEN
INSERT INTO AUDIT_EMP_CHANGE VALUES (:OLD.ENAME, :OLD.SAL, :NEW.SAL, SYSDATE);
ELSE
UPDATE AUDIT_EMP_CHANGE
SET OLDSAL=:OLD.SAL,
NEWSAL=:NEW.SAL,
TIME=SYSDATE
WHERE NAME = :OLD.ENAME;
END IF;
END;
/
触发器添加限制行:
CREATE OR REPLACE TRIGGER TR_SAL_SAL
AFTER UPDATE OF SAL
ON EMP
FOR EACH ROW
WHEN (OLD.JOB ='SALESMAN') --只有当员工的JOB='SALESMAN' 时, 更新工资才会触发这个触发器
DECLARE
V_TEMP INT;
BEGIN
SELECT COUNT(*) INTO V_TEMP FROM AUDIT_EMP_CHANGE WHERE NAME = :OLD.ENAME;
IF V_TEMP = 0 THEN
INSERT INTO AUDIT_EMP_CHANGE VALUES (:OLD.ENAME, :OLD.SAL, :NEW.SAL, SYSDATE);
ELSE
UPDATE AUDIT_EMP_CHANGE
SET OLDSAL=:OLD.SAL,
NEWSAL=:NEW.SAL,
TIME=SYSDATE
WHERE NAME = :OLD.ENAME;
END IF;
END;
/
编写DML触发器时,触发器代码不能从触发器所对应的基表中读取数据。
如果基于EMP 表建立触发器,那么该触发器的执行代码不能包含对EMP 表的查询操作。
CREATE OR REPLACE TRIGGER TR_EMP_SAL
BEFORE UPDATE OF EMP01
ON EMPLOYEE
FOR EACH ROW
DECLARE
NAME VARCHAR2(8);
BEGIN
SELECT EMP01 INTO NAME FROM EMPLOYEE;
IF :NEW.EMP01 = NAME THEN
RAISE_APPLICATION_ERROR(-21000, 'ERROR');
END IF;
END;
/
--创建的时候不会报错, 但是更新表数据就会报错
UPDATE EMPLOYEE SET EMP01='周康' WHERE EMP00 = '7350F9DE-F5BC-461A-B499-F86680D2DA4E';
触发器可以帮我们做一些数据校验、数据记录等工作,在Oracle 中有一些系统事件触发器是内置的,基于Oracle 的系统事件(LOGON
,STARTUP
)所建立的触发器。通过使用系统触发器,提供了跟踪系统或数据库变化的机制,一些内置的系统触发器如下:
触发器名 | 用途 |
---|---|
ORA_CLIENT_IP_ADDRESS | 用于返回客户端的IP地址 |
ORA_DATABASE_NAME | 用于返回当前数据库名 |
ORA_DES_ENCRYPTED_PASSWORD | 用于返回 DES 加密后的用户口令 |
ORA_DICT_OBJ_NAME | 用于返回 DDL 操作所对应的数据库对象名 |
ORA_DICT_OBJ_NAME_LIST(NAME_LIST OUT ORA_NAME_LIST_T) | 用于返回在事件中被修改的对象名列表 |
ORA_DICT_OBJ_OWNER | 用于返回DDL操作所对应的对象的所有者名 |
ORA_DICT_OBJ_OWNER_LIST(OWNER_LIST OUT ORA_NAME_LIST_T) | 用于返回在事件中被修改对象的所有者列表 |
ORA_DICT_OBJ_TYPE | 用于返回 DDL 操作所对应的数据对象的类型 |
ORA_GRANTEE(USER_LIST OUT ORA_NAME_LIST_T) | 用于返回授权事件的授权者 |
ORA_INSTANCE_NUM | 用于返回例程号 |
ORA_IS_ALTER_COLUMN(COLUMN_NAME IN VARCHAR2) | 用于检测特定列是否被修改 |
ORA_IS_CREATING_NESTED_TABLE | 用于检测是否正在建立嵌套表 |
ORA_IS_DROP_COLUMN(COLUMN_NAME IN VARCHAR2) | 用于检测特定列是否被删除 |
ORA_IS_SERVERERROR(ERROR_NUMBER) | 用于检测是否返回了特定 Oracle 错误 |
ORA_LOGIN_USER | 用于返回登录用户名 |
ORA_SYSEVENT | 用于返回触发触发器的系统事件名 |
有了上述的系统事件触发器,就可以建立一些特殊的触发器了。
--建立登录退出信息表
CREATE TABLE log_table
(
USERNAME VARCHAR2(20),
LOGON_TIME DATE,
LOGOFF_TIME DATE,
ADDRESS VARCHAR2(20)
);
建立了LOG_TABLE 表之后,就可以建立触发器对这张表进行操作了,登录触发器和退出触发一定要使用特权用户身份建立,并且登录触发器只能使用 AFTER 关键字,而退出触发器只能使用 BEFORE 关键字:
CREATE OR REPLACE TRIGGER tr_logon
AFTER LOGON ON DATABASE
BEGIN
INSERT INTO LOG_TABLE(USERNAME, LOGON_TIME, ADDRESS)
VALUES(ORA_LOGIN_USER, SYSDATE, ORA_CLIENT_IP_ADDRESS);
END;
/
CREATE OR REPLACE TRIGGER tr_logoff
BEFORE LOGOFF ON DATABASE
BEGIN
INSERT INTO LOG_TABLE(USERNAME, LOGOFF_TIME, ADDRESS)
VALUES(ORA_LOGIN_USER, SYSDATE, ORA_CLIENT_IP_ADDRESS);
END;
/
CREATE TABLE EVENT_DDL
(
EVENT VARCHAR2(20),
USERNAME VARCHAR2(20),
OWNER VARCHAR2(10),
OBJNAME VARCHAR2(20),
OBJTYPE VARCHAR2(10),
TIME DATE
);
建立触发器,使用AFTER关键字,记录表中发生的DDL 操作:
CREATE OR REPLACE TRIGGER tr_ddl
AFTER DDL ON SCHEMA
BEGIN
INSERT INTO event_ddl1 VALUES (ORA_SYSEVENT, ORA_LOGIN_USER, ORA_DICT_OBJ_OWNER, ORA_DICT_OBJ_NAME, ORA_DICT_OBJ_TYPE, SYSDATE);
END;
/
DROP TABLE TEST;
SELECT TRIGGER_NAME, STATUS FROM USER_TRIGGERS;
ALTER TRIGGER TR_CHECK_SAL DISABLE;
ALTER TRIGGER TR_CHECK_SAL ENABLE;
ALTER TABLE EMPLOYEE DISABLE ALL TRIGGERS; --批量操作一张表所有的触发器
ALTER TABLE EMPLOYEE ENABLE ALL TRIGGERS;
ALTER TABLE
命令修改表结构(添加删除列)时,会使其触发器转变为INVALID状态,这种情况下,为了使触发器继续生效,需要重新编译触发器:ALTER TRIGGER TR_DDL COMPILE;
DROP TRIGGER TR_EMP_SAL;