个人总结 高阶PLSQL 数据库编程

个人总结 高阶PLSQL 数据库编程

第一章:PL/SQL基础

PL/SQL是Oracle在关系数据库结构化查询语言SQL(strutured query language)基础上扩展得到的一种过程化查询语言。
SQL既没有变量,也没有流程控制(分支、循环)。PL/SQL是结构化和过程化的结合体,用户在执行多条SQL语句时,是
逐一发送给数据库,而PL/SQL可以一次性将多条SQL语句一起发送给数据库,减少网络流量。

1.1 、语法:

DECLARE
    --声明部分,如定义变量、常量、游标
BEGIN
    --程序编写、SQL语句
EXCEPTION
    --处理异常
END;

1.1.1、输出

SET serveroutput ON;
DECLARE
  v_eno   NUMBER;
  v_ename VARCHAR2(10);
BEGIN
  v_eno := &empno; --由用户输入
  SELECT ename INTO v_ename FROM emp WHERE empno = v_eno;
  dbms_output.put_line('编号为:' || v_eno || '雇员的名字为:' || v_ename);
END;

1.1.2、PL/SQL中变量不区分大小写

DECLARE
  v_resulta NUMBER NOT NULL := 100;  --定义非空变量同时赋值
  v_resultb NUMBER;
  v_resultc CONSTANT NUMBER NOT NULL := 100;    --定义一个常量,同时赋值
BEGIN
  v_resultb := 30;    --没有区分大小写
  dbms_output.put_line('计算结果是:' || (v_resulta + v_resultb));
END;

1.1.3、使用 %type和 %rowtype声明变量类型

DECLARE
  v_eno     emp.empno%TYPE; --与empno类型相同
  v_ename   emp.ename%TYPE; --与ename类型相同
  v_deptrow dept%ROWTYPE; --装载一行dept记录
BEGIN
  SELECT * FROM INTO v_deptrow FROM dept WHERE deptno = 10;
  dbms_output.put_line('部门编号:' || v_deptrow.deptno || ',名称:' ||
                       v_deptrow.dname);
  dbms_output.put_line('雇员编号:' || v_eno || ',雇员名:' || v_ename);
END;

1.1.4、关系运算

DECLARE
  v_url  VARCHAR2(50) := 'www.liuhuan.org';
  v_num1 NUMBER := 80;
  v_num2 NUMBER := 30;
BEGIN
  IF v_num1 > v_num2 THEN
    dbms_output.put_line('第一个数比第二个数大');
  END IF;
  IF v_url LIKE '%huan%' THEN
    dbms_output.put_line('网址之中包含huan单词');
  END IF;
END;

1.1.5、字符型:

Oracle中varchar2就是其他数据库中的varchar,两者形式上一样

DECLARE
  v_info_char  CHAR(10);
  v_info_vchar VARCHAR2(10);
BEGIN
  v_info_char  := 'HUAN'; --长度不足10个,会自动补齐空格
  v_info_vchar := 'HUAN'; --长度不足10个,不会补充
  dbms_output.put_line('v_info_char内容长度:' || length(v_info_char));
  dbms_output.put_line('v_info_vchar内容长度:' || length(v_info_vchar));
END;

1.1.6、日期型

SET serveroutput ON;
DECLARE
  v_date1 DATE := SYSDATE;    --date数据类型
  v_date2 DATE := systimestamp;
  v_timestamp1 TIMESTAMP := SYSDATE;    --timestamp数据类型
  v_timestamp2 TIMESTAMP := '19-9月-1981';
BEGIN
  dbms_output.put_line('日期数据' || v_date1);
  dbms_output.put_line('日期数据' || v_date2);
  dbms_output.put_line('日期数据' || v_timestamp1);
  dbms_output.put_line('日期数据' || v_timestamp2);
END;

1.1.7、子类型:

在标量类型基础上定义更多的约束,适应用户需求,这种新的类型成为子类型
语法:
SUBTYPE 子类型名称 IS 父数据类型[(约束)] [NOT NULL];

DECLARE
  SUBTYPE score_subtype IS NUMBER(5, 2) NOT NULL;
  v_score score_subtype := 99.35;
  SUBTYPE string_subtype IS VARCHAR2(100);
  v_company string_subtype;
BEGIN
  v_company := 'XXXXX公司';
  dbms_output.put_line(v_score);
  dbms_output.put_line(v_company);
END;

1.1.8、IF_ELSE结构

DECLARE
v_empsal emp.sal%TYPE;
v_dno emp.deptno%TYPE;
v_eno emp.empno%TYPE;
BEGIN
  v_eno := &inputEmpno;    --用户输入
  SELECT deptno, sal INTO v_dno, v_sal FROM emp WHERE empno = v_eno;
  IF v_dno = 10 THEN
    IF v_empsal * 1.1 > 5000 THEN
      UPDATE emp SET sal = 5000 WHERE empno = v_eno;
    ELSE
      UPDATE emp SET sal = sal*1.1 WHERE empno = v_eno;
    END IF;
  ELSE if v_dno = 20 THEN
    IF v_empsal * 1.2 > 5000
      UPDATE emp SET sal = 5000 WHERE empno = v_eno;
    ELSE
      UPDATE emp SET sal = sal*1.2 WHERE empno = v_eno;
    END IF;
  ELSE IF v_dno = 30 THEN
    IF v_empsal * 1.3 > 5000 THEN
      UPDATE emp SET sal = 5000 WHERE empno = v_eno;
    ELSE IF
      UPDATE emp SET sal = sal*1.3 WHERE empno = v_eno;
    END IF;
  ELSE
    NULL;
 END IF;
END;

1.1.9、正则验证

DECLARE
  v_str VARCHAR2(50) := '123';
BEGIN
  IF regexp_like(v_str, '^\d+$') THEN
    dbms_output.put_line('正则验证通过');
  END IF;
END;

1.1.10、CASE语句

DECLARE
  v_job emp.job%TYPE;
  v_eno emp.empno%TYPE;
BEGIN
  v_eno := &inputempno;
  SELECT job INTO v_job FROM emp WHERE empno = v_eno;
  CASE v_job
    WHEN 'CLERK' THEN
      UPDATE emp SET sal = sal * 1.05 WHERE empno = v_eno;
    WHEN 'SALESMAN' THEN
      UPDATE emp SET sal = sal * 1.08 WHERE empno = v_eno;
    WHEN 'MANAGER' THEN
      UPDATE emp SET sal = sal * 1.1 WHERE empno = v_eno;
    WHEN 'ANALYST' THEN
      UPDATE emp SET sal = sal * 1.2 WHERE empno = v_eno;
    ELSE
      dbms_output.put_line('雇员:' || v_eno || '工资不具备上涨资格!');
  END CASE;
END;

1.1.11、循环结构

1.1.11.1、LOOP循环

DECLARE
  v_i NUMBER := 1;
BEGIN
  LOOP
    dbms_output.put_line('v_i = ' || v_i);
    EXIT WHEN v_i >= 3;
    v_i := v_i + 1;
  END LOOP;
END;

1.1.11.2、WHILE…LOOP循环

DECLARE
  v_i NUMBER := 1;
BEGIN
  WHILE (v_i <= 3) LOOP
    dbms_output.put_line('v_i = ' || v_i);
    v_i := v_i + 1;
  END LOOP;
END;

1.1.11.3、FOR循环

DECLARE
  v_i NUMBER := 1;
BEGIN
  FOR v_i IN 1 .. 3 LOOP
    dbms_output.put_line('v_i = ' || v_i);
  END LOOP;
END;

1.1.12、循环控制:EXIT CONTINUE

1.1.12.1、EXIT 退出循环

DECLARE
  v_i NUMBER := 1;
BEGIN
  FOR v_i IN 1 .. 10 LOOP
    IF v_i = 3 THEN
      --当v_i变量增长到3时结束循环
      EXIT;
    END IF;
    dbms_output.put_line('v_i = ' || v_i);
  END LOOP;
END;

1.1.12.2、CONTINUE 跳出本次循环

DECLARE
  v_i NUMBER := 1;
BEGIN
  FOR v_i IN 1 .. 10 LOOP
    IF v_i = 3 THEN
      --当v_i变量增长到3时跳过本次循环
      CONTINUE;
    END IF;
    dbms_output.put_line('v_i = ' || v_i);
  END LOOP;
END;

1.1.13、GOTO语句

分支结构代码中进行代码跳转操作主要使用GOTO完成,直接转到指定标号处,和一般高级语言一样,GOTO不能转入
IF语句、循环体和子块,但可以从IF语句、循环体和子块转出。

DECLARE
  V_I NUMBER := 1;
BEGIN
  FOR V_I IN 1 .. 10 LOOP
    IF V_I = 2 THEN
      GOTO ENDPOINT;
    END IF;
    DBMS_OUTPUT.PUT_LINE('v_i = ' || V_I);
  END LOOP;
  <<ENDPOINT>>
  DBMS_OUTPUT.PUT_LINE('FOR循环提前结束。');
END;

1.1.14、内部程序块

对于一个完整的PL/SQL程序语法,可以在内部编写declare begin end语句块

DECLARE
  V_X NUMBER := 30;
BEGIN
  DECLARE
    V_X VARCHAR2(50) := 'oraclejava';
    V_Y NUMBER := 10;
  BEGIN
    DBMS_OUTPUT.PUT_LINE('v_x = ' || V_X);
    DBMS_OUTPUT.PUT_LINE('v_y = ' || V_Y);
  END;
  DBMS_OUTPUT.PUT_LINE('v_x = ' || V_X);
END;

1.2 异常处理

PL/SQL中一共分为编译型异常运行时异常两类。
编译型异常:程序的语法出现了错误所导致的异常。
运行时异常:程序没有语法问题,但在运行时会因为程序运算或返回结果而出现错误。
对于编译时出现的错误,用户没有办法进行处理,只能进行代码修改,而在运行时出现的异常,用户可以使用
EXCEPTION语句块处理。

1.2.1、 处理被除数为0的异常

DECLARE
  V_RESULT NUMBER;
BEGIN
  V_RESULT := 10 / 0; --被除数为0
  DBMS_OUTPUT.PUT_LINE('异常之后的代码将不再执行!');
EXCEPTION
  WHEN ZERO_DIVIDE THEN   --zero_divide是一种预定义异常(类似还有一些)
    DBMS_OUTPUT.PUT_LINE('被除数不能为0!');
    DBMS_OUTPUT.PUT_LINE('SQLCODE = ' || SQLCODE);
END;

1.2.2、 处理赋值异常

DECLARE
  V_VARA VARCHAR2(1);
  V_VARB VARCHAR2(10);
BEGIN
  V_VARB := 'java';
  V_VARA := V_VARB;
  DBMS_OUTPUT.PUT_LINE('异常之后的代码将不再执行!');
EXCEPTION
  WHEN value_error THEN    --value_error是一种预定义异常
    DBMS_OUTPUT.PUT_LINE('数据赋值错误!');
    DBMS_OUTPUT.PUT_LINE('SQLCODE = ' || SQLCODE);
END;

1.2.3、 处理SQL异常-找不到数据

DECLARE
  V_ENO   EMP.EMPNO%TYPE;
  V_ENAME EMP.ENAME%TYPE;
BEGIN
  V_ENO := &INPUTEMPNO;
  SELECT ENAME INTO V_ENAME FROM EMP WHERE EMPNO = V_ENO;
  DBMS_OUTPUT.PUT_LINE('编号为:' || V_ENO || '雇员的名字是' || V_ENAME);
EXCEPTION
  WHEN NO_DATA_FOUND THEN    --no_data_found是一种预定义异常
    DBMS_OUTPUT.PUT_LINE('未找到该雇员信息!');
END;

1.2.4、处理SQL异常-返回多条结果

DECLARE
  V_DNO   EMP.DEPTNO%TYPE;
  V_ENAME EMP.ENAME%TYPE;
BEGIN 
  V_DNO := &INPUTDEPTNO;
  SELECT ENAME INTO V_ENAME FROM EMP WHERE EMPNO = V_DNO;
EXCEPTION
  WHEN TOO_MANY_ROWS THEN
    DBMS_OUTPUT.PUT_LINE('返回的数据过多!');
    DBMS_OUTPUT.PUT_LINE('SQLCODE = ' || SQLCODE);
END;

1.2.5、使用others捕获所有异常,使用SQLERRM输出异常信息

DECLARE
  V_RESULT NUMBER;
  V_TITLE  VARCHAR2(50) := 'www.liuhuan.org';
BEGIN
  V_RESULT := V_TITLE; --此处出现异常
EXCEPTION
  WHEN OTHERS THEN
    DBMS_OUTPUT.PUT_LINE('出现了异常!');
    DBMS_OUTPUT.PUT_LINE('SQLCODE = ' || SQLCODE);
    DBMS_OUTPUT.PUT_LINE('SQLERRM = ' || SQLERRM);
END;

1.2.6、用户自定义异常

方式一:在声明块中声明EXCEPTION对象,此方式有两种选择:
选择一:声明异常对象并用名称引用他,此方式使用普通的others异常捕获用户定义的异常。
选择二:声明异常对象并将它与有效的Oracle错误代码映射,需要编写独立的when语句块捕获。
方式二:在执行块中构建动态异常。通过raise_application_error函数可以构建动态异常。
在触发动态异常时,可使用-20000~20999范围的数字,如果使用动态异常,可以在运行时指派错误信息。

1.2.6.1、声明块中声明EXCEPTION对象

1.2.6.1.1、 使用用户定义异常:采用声明异常对象的方式抛出用户定义异常,直接使用others接收。
DECLARE
  V_DATA NUMBER;
  V_MYEXP EXCEPTION;
BEGIN
  V_DATA := &INPUTDATA;
  IF V_DATA > 10 AND V_DATA < 100 THEN
    RAISE V_MYEXP; --抛出异常
  END IF;
EXCEPTION
  WHEN OTHERS THEN
    DBMS_OUTPUT.PUT_LINE('输入数据有误!');
    DBMS_OUTPUT.PUT_LINE('SQLCODE = ' || SQLCODE);   --默认情况下所有用户自定义异常SQLCODE为1
    DBMS_OUTPUT.PUT_LINE('SQLERRM =' || SQLERRM);
END;
1.2.6.1.2、 为自定义异常设置代码。
DECLARE
  V_DATA NUMBER;
  V_MYEXP EXCEPTION;
  PRAGMA EXCEPTION_INIT(V_MYEXP, -20789);    --为自定义异常设置代码
BEGIN
  V_DATA := &INPUTDATA;
  IF V_DATA > 10 AND V_DATA < 100 THEN
    RAISE V_MYEXP; --抛出异常
  END IF;
EXCEPTION
  WHEN V_MYEXP THEN   --捕获自定义异常
    DBMS_OUTPUT.PUT_LINE('输入数据有误!');
    DBMS_OUTPUT.PUT_LINE('SQLCODE = ' || SQLCODE);
    DBMS_OUTPUT.PUT_LINE('SQLERRM =' || SQLERRM);
END;
1.2.6.1.3、 绑定已有的错误号:将自定义异常与存在的预定义异常错误号绑定。
DECLARE
  v_myexp EXCEPTION;
  v_input_rowid VARCHAR2(18);
  PRAGMA EXCEPTION_INIT(v_myexp, -01410);
BEGIN
  v_input_rowid := '&inputRowid';
  IF length(v_input_rowid) <> 18 THEN
    RAISE v_myexp;
  END IF;
EXCEPTION
  WHEN v_myexp THEN
    dbms_output.put_line('SQLCODE = ' || SQLCODE);
    dbms_output.put_line('SQLERRM = ' || SQLERRM);
END;

1.2.6.2、执行块中构建动态异常

RAISE_APPLICATION_ERROR(错误号,错误信息[, 是否添加到错误堆栈]);
错误号:接收-20000~20999范围错误号,和声明的错误号一致。
错误信息:定义在使用SQLERRM输出时的错误提示信息。
是否添加到错误堆栈:如果设置为true,则表示将错误添加到任意已有的错误堆栈,默认false

1.2.6.2.1、构建动态异常
DECLARE
  v_data NUMBER;
  v_myexp EXCEPTION;
  PRAGMA EXCEPTION_INIT(v_myexp, -20789);
BEGIN
  v_data := &inputdata;
  IF v_data > 10 AND v_data < 100 THEN
    raise_application_error(-20789, '输入的数字不能在10~100之间!');   --此处错误号与声明处要一致
  END IF;
EXCEPTION
  WHEN v_myexp THEN
    dbms_output.put_line('输入数据有误!');
    dbms_output.put_line('SQLCODE = ' || SQLCODE);
    dbms_output.put_line('SQLERRM = ' || SQLERRM);
END;
1.2.6.2.2、 不声明异常变量,直接构建异常,同时使用others捕获,可以省略PRAGMA EXCEPTION_INIT()
DECLARE
  v_data NUMBER;
  v_myexp EXCEPTION;   --该句也可以省略,只留下构建动态异常语句和用others捕获
BEGIN
  v_data := &inputdata;
  IF v_data > 10 AND v_data < 100 THEN
    raise_application_error(-20789, '输入数字不能再10~100之间!');
  END IF;
EXCEPTION
  WHEN OTHERS THEN
    dbms_output.put_line('输入数据有误!');
    dbms_output.put_line('SQLCODE = ' || SQLCODE);
    dbms_output.put_line('SQLERRM = ' || SQLERRM);
END;

第二章:集合

2.1、记录类型

类似结构体或类,可以保存多个变量

在使用记录类型更新时,只需要在更新语句后写上row,就可以找到对应字段,记录定义时字段顺序要与表中一致。

2.1.1、简单类型的记录类型

DECLARE
  v_emp_empno emp.empno%TYPE;
  TYPE emp_type IS RECORD(
     ename    emp.ename%TYPE,
     job      emp.job%TYPE,
     sal      emp.sal%TYPE,
     hiredate emp.hiredate%TYPE,
     comm     emp.comm%TYPE
  );
  v_emp       emp_type;
BEGIN
  v_emp_empno := &inputEmpno;
  SELECT ename, job, sal, hiredate, comm INTO v_emp FROM emp WHERE empno = v_emp_empno;
  dbms_output.put_line('雇员编号'||v_emp_empno||',雇员姓名:'||v_emp.ename||',雇员职位:'||v_emp.job||'
           ,雇员薪资:'||v_emp.sal||',雇员雇佣日期:'||v_emp.hiredate);
EXCEPTION
    WHEN OTHERS THEN  --可以不定义异常,直接在exception子句中抛出动态异常
        raise_exception_error(-20789, '此雇员信息不存在!');   
END;

2.1.2、嵌套的记录类型

DECLARE
  TYPE dept_type IS RECORD(
       deptno dept.deptno%TYPE := 80,
       dname dept.dname%TYPE,
       loc dept.loc%TYPE
  );
  TYPE emp_type IS RECORD(
       empno emp.empno%TYPE,
       ename emp.ename%TYPE,
       job emp.job%TYPE,
       hiredate emp.hiredate%TYPE,
       sal emp.sal%TYPE,
       dept dept_type    --嵌套记录类型
       --mgr emp_type    --不能在里面定义一个自己类型的mgr成员,编译会报错
  );
  v_emp  emp_type;
  BEGIN
    SELECT e.empno, e.ename, e.job, e.hiredate, e.sal, d.deptno, d.dname, d.loc INTO v_emp
    FROM emp e, dept d WHERE e.deptno = d.deptno(+) AND e.empno = 7369;
    dbms_output.put_line('输出对应信息'||);
END;

2.2 、索引表(又叫表类型或者内存表)

类似于程序语言中数组,可以保存多个数据,且可以通过下标访问数据。
区别:
1、索引表不需要初始化,可以直接为指定索引赋值,开辟的索引表的索引不一定必须连续。
2、可以使用数字或字符串表示索引下标,也可以为负数。
3、索引表不存在遍历操作。

定义索引表语法:
TYPE 类型名 IS TABLE OF 数据类型 [NOT NULL] INDEX BY [PLS_INTEGER | BINARY_INTEGER | VARCHAR2(长度)];

2.2.1、临时索引表

2.2.1.1、临时创建的数字下标的索引表

DECLARE
  TYPE info_index IS TABLE OF VARCHAR(20) INDEX BY PLS_INTEGER;
  v_info info_index;
BEGIN
  v_info(1) := 'MLDN';
  v_info(10) := 'JAVA';
  IF v_info.exists(10) THEN
    dbms_output.put_line(v_info(10));
  END IF;
  IF v_info.exists(30) THEN   --使用EXISTS()函数验证索引合法性
    dbms_output.put_line(v_info(30));
  ELSE
    dbms_output.put_line('索引号为30的数据不存在!');
  END IF;
END;

2.2.1.2、临时创建的varchar2()下标的索引表

DECLARE
  TYPE info_index IS TABLE OF VARCHAR(50) INDEX BY VARCHAR2(50);
  v_info info_index;
BEGIN
  v_info('公司名称') := 'XX科技';
  v_info('培训项目') := 'ORACLE';
  dbms_output.put_line(v_info('公司名称'));
  IF v_info.exists('培训项目') THEN
    dbms_output.put_line(v_info('培训项目'));
  END IF;
  IF v_info.exists('SAD') THEN
    dbms_output.put_line(v_info('SAD'));
  ELSE
    dbms_output.put_line('不存在索引为SAD的数据');
  END IF;
END;

2.2.1.3、临时创建的rowtype类型的索引表

DECLARE 
  TYPE dept_index IS TABLE OF dept%TYPE 
  INDEX BY PLS_INTEGER;
  v_dept  dept_index;
  BEGIN
    v_dept(0).deptno := 80;
    v_dept(0).dname := 'SALE';
    v_dept(0).loc := '深圳';
    IF v_dept.exists(0) THEN
      dbms_output.put_line('部门信息');
END;

2.2.1.4、临时创建的自定义记录类型的索引表

DECLARE
      TYPE dept_type IS RECORD(
           deptno dept.deptno%TYPE,
           dname dept.dname%TYPE,
           loc dept.loc%TYPE
      );
      TYPE dept_index IS TABLE OF dept_type
           INDEX BY PLS_INTEGER;           
BEGIN
    v_dept(0).deptno := 80;
    v_dept(0).dname := 'SALE';
    v_dept(0).loc := '深圳';
    IF v_dept.exists(0) THEN
      dbms_output.put_line('部门信息');
END;

2.2.2、数据库索引表(可作为表使用)

2.2.2.1、数据库的数字下标的索引表

create or replace TYPE cux_string_tbl_type as table of varchar2(5000) INDEX  BY PLS_INTEGER

2.2.2.2、数据库的varchar2()下标的索引表

create or replace TYPE cux_string_tbl_type as table of varchar2(5000)  INDEX BY VARCHAR2(50);

2.2.2.3、数据库的rowtype类型的索引表

create or replace TYPE cux_string_tbl_type as table of dept%TYPE;

2.2.2.4、数据库的自定义记录类型的索引表

CREATE OR REPLACE TYPE cux_yang_tools_record_type IS OBJECT
(
     keychar1    VARCHAR2(240)
    ,keychar2    VARCHAR2(240)
    ,keychar3    VARCHAR2(240)
)
CREATE OR REPLACE TYPE cux_yang_tools_table_type IS TABLE OF cux_yang_tools_record_type

2.3、 嵌套表

实际开发用的不多,只在Oralce支持,并非标准SQL实现
嵌套表类似索引表,可以保存多个数据,也可以保存复合数据;指的是在一个数据表定义时同时加入其他内部表的定义。
一对多的存储关系:两张数据表,一张为主表(需要定义主键或唯一约束),另一张为子表(设置外键),其中
主表的一行记录会对应多行子表记录。而如果将主表和子表合为一体,子表就可以理解为主表的一个嵌套表,所以
嵌套表是多行子表关联数据的集合,在主表中表示为其中的某一列。

语法:
CREATE [OR REPLACE] TYPE 类型名 AS | IS TABLE OF 数据类型 [NOT NULL];

场景:每部门有多个项目在同时实施,按之前定义分两表:
部门表:部门编号(PK) 、 名称、位置。
项目表:项目编号(PK)、部门编号(FK)、项目名称。
嵌套表形式:
部门表:部门编号(PK)、名称、位置、项目(类型为嵌套表)
这样可以通过部门表直接访问项目表中记录,不需要夺标连接而直接查询数据,使得用户对数据访问更容易。

2.3.1、简单类型的表类型

CREATE OR REPLACE TYPE project_nested AS TABLE OF VARCHAR2(50) not NULL;

2.3.2、复合类型的表类型

CREATE OR REPLACE TYPE project_type AS OBJECT(
       projectid NUMBER,
       projectname VARCHAR2(50),
       projectfunds NUMBER,
       pubdate DATE
);
--定义嵌套表类型
CREATE OR REPLACE TYPE project_nested AS TABLE OF project_type NOT NULL;

2.3.3、在表中使用简单类型的表类型

DROP TABLE department PURGE;
CREATE TABLE department(
       did NUMBER,
       deptname VARCHAR2(30) NOT NULL,
       projects project_nested,
       CONSTRAINT pk_did PRIMARY KEY(did)       
)NESTED TABLE projects STORE AS projects_nested_table;
--再查看department表结构
DESC department;
--增加和查找数据
INSERT INTO department(did, deptname, projects) VALUES(10, '销售', project_nested('<销售1>', '<销售2>', '<销售3>'));
INSERT INTO department(did, deptname, projects) VALUES(20, '管理', project_nested('<管理1>', '<惯例2>', '<管理3>'));
SELECT * FROM department;
--错误的查询20部门的项目:显示集合信息
SELECT projects FROM department WHERE did = 20;
--将嵌套表数据变为数据表查询:一行行显示数据
SELECT * FROM TABLE(SELECT projects FROM department WHERE did = 20);
 
--修改部门20中信息
UPDATE TABLE (SELECT projects FROM department WHERE did = 20) pro
SET VALUE(pro) = '<管理2>' WHERE pro.column_value = '<惯例2>';
COMMIT;
--删除嵌套表记录
DELETE FROM TABLE (SELECT projects FROM department WHERE did = 20) p WHERE p.column_value = '<管理3>';

2.3.4、在表中使用简单类型的表类型

CREATE OR REPLACE TYPE project_type AS OBJECT(
       projectid NUMBER,
       projectname VARCHAR2(50),
       projectfunds NUMBER,
       pubdate DATE
);
--定义嵌套表类型
CREATE OR REPLACE TYPE project_nested AS TABLE OF project_type NOT NULL;
--创建部门表
DROP TABLE department PURGE;
CREATE TABLE department(
       did NUMBER,
       deptname VARCHAR2(50) NOT NULL,
       projects project_nested,
       CONSTRAINT pk_did PRIMARY KEY(did)
)NESTED TABLE projects STORE AS project_nested_table;

简化定义

DROP TABLE department PURGE;
CREATE TABLE department(
       did NUMBER,
       deptname VARCHAR2(50) NOT NULL,
       projects project_nested,
       CONSTRAINT pk_did PRIMARY KEY(did)
)NESTED TABLE projects STORE AS projects_nested_table((
        projectid PRIMARY KEY,
        projectname NOT NULL,
        projectfunds NOT NULL,
        pubdate NOT NULL
));

使用示例

--向部门表增加数据:由于此时嵌套表中保存数据为复合类型,所以在进行数据添加时需要为复合数据中每一列设置好相应数据。
INSERT INTO department(did, deptname, projects) VALUES(10, '魔乐科技',
       project_nested(
            project_type(1, 'java', 8900, to_date('2014-05-30', 'yyyy-mm-dd')),
            project_type(2, 'c++', 13567, to_date('2017-06-30', 'yyyy-mm-dd'))            
       ));
 
INSERT INTO department(did, deptname, projects) VALUES(20, '出版部',
       project_nested(
            project_type(10, 'oracle', 3900, to_date('2015-03-30', 'yyyy-mm-dd')),
            project_type(12, 'js', 13567, to_date('2017-07-31', 'yyyy-mm-dd')),            
            project_type(13, 'xml', 1267, to_date('2017-09-21', 'yyyy-mm-dd'))
       ));
COMMIT;       
SELECT * FROM department;
SELECT * FROM TABLE(SELECT projects FROM department WHERE did = 20);
--修改某部门某项目信息
UPDATE TABLE(SELECT projects FROM department WHERE did = 20) pro
SET VALUE(pro) = PROJECT_type(10, 'oracle', 98.0, to_date('1989-09-21', 'yyyy-mm-dd'))
WHERE pro.projectid = 10;
COMMIT;
 
--删除嵌套表中一行记录
DELETE FROM TABLE (SELECT projects FROM department WHERE did = 20) pro
WHERE pro.projectid = 12;
COMMIT;
SELECT * FROM TABLE(SELECT projects FROM department WHERE did = 20);

2.3.5、在PL/SQL中使用简单类型的嵌套表

嵌套表应用中可以使用嵌套表.count获取数组长度。
PL/SQL中定义的嵌套表类型必须使用IS不能使用AS。
循环次数可用集合.first~集合.last完成

DECLARE 
   TYPE project_nested IS TABLE OF VARCHAR2(50) NOT NULL;
   v_projects project_nested := project_nested('java', 'android');
BEGIN
   FOR x IN 1..v_projects.count LOOP   --还可用 for x in v_projects.first..v_projects.last loop
     dbms_output.put_line(v_projects(x));
   END LOOP;
END;

在SQLPlus中定义嵌套表数据类型,在PL/SQL中直接使用project_nested

CREATE OR REPLACE TYPE project_nested IS TABLE OF VARCHAR2(50) NOT NULL;
DECLARE
  v_projects project_nested := project_nested('c', 'js', 'java');
BEGIN
  FOR x IN v_projects.first .. v_projects.last LOOP
    dbms_output.put_line(v_projects(x));
  END LOOP;
END;

示例:

CREATE OR REPLACE TYPE project_nested IS TABLE OF VARCHAR2(50) NOT NULL;
/
--再创建一张包含嵌套表的数据表
DROP TABLE department PURGE;
CREATE TABLE department(
       did  NUMBER,
       deptname  VARCHAR2(50) NOT NULL,
       projects  project_nested,
       CONSTRAINT pk_did PRIMARY KEY(did)
)NESTED TABLE projects STORE AS projects_nested_table;
--利用嵌套表实现数据增加操作
DECLARE
  v_project_list project_nested := project_nested('c++', 'java', 'js');
  v_dept         department%ROWTYPE;
BEGIN
  v_dept.did      := 88;
  v_dept.deptname := 'xx科技';
  v_dept.projects := v_project_list; --直接赋予嵌套表
  INSERT INTO department VALUES v_dept; --直接使用rowtype对象增加
END;

利用嵌套表实现修改数据操作

DECLARE
  v_project_list project_nested := project_nested('js', 'c++', 'java');
BEGIN
  UPDATE department SET projects = v_project_list WHERE did = 88; --直接使用嵌套表保存数据
END;

2.3.6、在PL/SQL中使用复合类型的嵌套表

CREATE OR REPLACE TYPE project_type AS OBJECT(
       projectid  NUMBER,
       projectname  VARCHAR2(50),
       projectfunds  NUMBER,
       pubdate  DATE
);
 
DECLARE
  TYPE project_nested IS TABLE OF project_type NOT NULL;
  v_projects  project_nested := project_nested(
              project_type(10, 'java', 79.8, to_date('2012-09-21', 'yyyy-mm-dd')),
              project_type(11, 'c++', 49.8, to_date('2012-05-26', 'yyyy-mm-dd')),
              project_type(12, 'js', 99.8, to_date('2002-05-21', 'yyyy-mm-dd'))              
  );
BEGIN
  FOR x IN v_projects.first..f_projects.last LOOP
    dbms_output.put_line('项目编号:'||v_projects(x).projectid||',项目名:'||v_projects(x).projectname
      ||',项目金额:'||v_projects(x).projectfunds||',项目日期:'||v_projects(x).pubdate);
  END LOOP;
END;

2.4、 可变数组

嵌套表最大特点是没有长度限制,如果需要固定长度集合类型,那么就只能利用可变数组完成。
语法:
CREATE [OR REPLACE] TYPE 类型名 AS | IS VARRAY(长度) OF 数据类型 [not NULL];

2.4.1、简单类型的可变数组

–定义简单类型的可变数组
–创建项目数组,数组长度为3,即每个部门项目最多3个

CREATE OR REPLACE TYPE project_varray AS VARRAY(3) OF VARCHAR2(50);

2.4.2、复合类型的可变数组

--创建一个表示项目类型对象
CREATE OR REPLACE TYPE project_type AS OBJECT(
       projectid  NUMBER,
       projectname  VARCHAR2(30),
       projectfunds  NUMBER,
       pubdate  DATE
);
--定义新的数组类型
CREATE OR REPLACE TYPE project_varray AS VARRAY(3) OF project_type;

2.4.3、在表中使用简单类型的可变数组

定义部门表

CREATE OR REPLACE TYPE project_varray AS VARRAY(3) OF VARCHAR2(50);
DROP TABLE department PURGE;
CREATE TABLE department(
       did  NUMBER,
       deptname  VARCHAR2(30)  NOT NULL,
       projects  project_varray,
       CONSTRAINT pk_did PRIMARY KEY(did)
);
--增加数据,项目名称最多3个
INSERT INTO department(did, deptname, projects) VALUES(10, 'sale', project_varray('ERP', 'CRM', 'CMS'));
SELECT * FROM department;
--查找一个部门所有项目
SELECT * FROM TABLE (SELECT projects FROM department WHERE did = 20);
--修改一个部门项目
UPDATE department SET 
projects = project_varray('asd', 'cd', 'eds') WHERE did = 20;
COMMIT;

2.4.4、在表中使用复合类型的可变数组

--创建一个表示项目类型对象
CREATE OR REPLACE TYPE project_type AS OBJECT(
       projectid  NUMBER,
       projectname  VARCHAR2(30),
       projectfunds  NUMBER,
       pubdate  DATE
);
--定义新的数组类型
CREATE OR REPLACE TYPE project_varray AS VARRAY(3) OF project_type;
--定义数据表,使用可变数组
DROP TABLE department PURGE;
CREATE TABLE department(
       did  NUMBER,
       deptname  VARCHAR2(40) NOT NULL,
       projects  project_varray,
       CONSTRAINT pk_did PRIMARY KEY(did)
);
--向表中加数据
INSERT INTO department(did, deptname, projects) VALUES(10, '科技',
       project_varray(
            project_type(10, 'ERP系统', 900000, to_date('2017-03-12', 'yyyy-mm-dd')),
            project_type(11, 'CRM客户管理系统', 1000000, to_date('2017-04-22', 'yyyy-mm-dd'))
       ));
 
INSERT INTO department(did, deptname, projects) VALUES(11, '三大',
       project_varray(
            project_type(10, 'java', 30, to_date('2017-03-12', 'yyyy-mm-dd')),
            project_type(11, 'c++', 50, to_date('2017-04-22', 'yyyy-mm-dd')),
            project_type(11, 'js', 40, to_date('2017-05-27', 'yyyy-mm-dd'))
       ));
COMMIT;
SELECT * FROM department;
SELECT * FROM TABLE(SELECT projects FROM department WHERE did = 10);
--更新信息
UPDATE department SET
       projects = project_varray(
                project_type(15, 'java', 89.9, to_date('2015-02-21', 'yyyy-mm-dd')),
                project_type(16, 'oracle', 84.9, to_date('2015-03-21', 'yyyy-mm-dd')),
                project_type(17, 'c++', 89.9, to_date('2015-06-21', 'yyyy-mm-dd'))
       )
WHERE did = 20;
COMMIT;
SELECT * FROM TABLE(SELECT projects FROM department WHERE did = 20);

2.4.5、在PL/SQL中使用简单类型的可变数组

DECLARE
  TYPE project_varray IS VARRAY(3) OF VARCHAR2(50);
  v_projects project_varray := project_varray(NULL, NULL, NULL);
BEGIN
  v_projects(1) := 'java se';
  v_projects(2) := 'oracle';
  v_projects(3) := 'c++';
  FOR x IN v_projects.first .. v_projects.last LOOP
    dbms_output.put_line(v_projects(x));
  END LOOP;
END;

2.4.6、在PL/SQL中使用复合结构的可变数组

CREATE OR REPLACE TYPE project_tyep AS OBJECT(
       projectid  NUMBER,
       projectname  VARCHAR2(50),
       projectfunds  NUMBER,
       pubdate  DATE
);
/
DECLARE 
   TYPE project_varray IS VARRAY(3) OF project_type NOT NULL;
   v_projects  project_varray := project_varray(
            project_type(15, 'java', 89.9, to_date('2015-02-21', 'yyyy-mm-dd')),
            project_type(16, 'oracle', 84.9, to_date('2015-03-21', 'yyyy-mm-dd')),
            project_type(17, 'c++', 89.9, to_date('2015-06-21', 'yyyy-mm-dd')));
BEGIN
  FOR x IN v_projects.first..v_projects.last LOOP
    dbms_output.put_line('项目编号:'||v_projects(x).projectid||',项目名:'||v_projects(x).projectname
      ||',项目金额:'||v_projects(x).projectfunds||',项目日期:'||v_projects(x).pubdate);
  END LOOP;
END;

2.5、 集合运算符

相关使用方法
1、 CARDINALITY(集合):取得集合中所有元素个数。
2、变量 IS [NOT] EMPTY:判断集合是否为NULL。
3、变量 MEMBER OF 集合:判断某一数据是否为集合中的成员。
4、集合1 MULTISET EXCEPT 集合2:从一个集合中删除另外一个集合中相同的数据,并返回新集合。
5、集合1 MULTISET INTERSECT 集合2:返回交集
6、集合1 MULTISET UNION 集合2:返回并集
7、SET:删除集合中重复元素,类似于DISTINCT操作,语法:SET(集合)。也可以利用SET检查变量是否为null:变量 IS[NOT] A SET.
8、集合1 SUBMULTISET OF 集合2:判断集合1是否为集合2的子集

2.5.1、CARDINALITY(集合):取得集合中所有元素个数。

DECLARE
  TYPE list_nested IS TABLE OF VARCHAR2(50) NOT NULL;
  v_all list_nested := list_nested('a', 'a', 'b', 'c', 'c', 'd', 'e');
BEGIN
  dbms_output.put_line('集合长度:' || cardinality(v_all));
END;

2.5.2、变量 IS [NOT] EMPTY:判断集合是否为NULL。

DECLARE
  TYPE list_nested IS TABLE OF VARCHAR2(50) NOT NULL;
  v_alla list_nested := list_nested('sadf', 'cdas', 'ewew');
  v_allb list_nested := list_nested();
BEGIN
  IF v_alla IS NOT empty THEN
    dbms_output.put_line('v_allA不是一个空集合');
  END IF;
  IF v_allb IS empty THEN
    dbms_output.put_line('v_allB是一个空集合');
  END IF;
END;

2.5.3、变量 MEMBER OF 集合:判断某一数据是否为集合中的成员。

DECLARE
  TYPE list_nested IS TABLE OF VARCHAR2(50) NOT NULL;
  v_all list_nested := list_nested('tab', 'cad', 'asd');
  v_str VARCHAR2(10) := 'tab';
BEGIN
  IF v_str MEMBER OF v_all THEN
    dbms_output.put_line('v_str是v_all的成员!');
  END IF;
END;

2.5.4、集合1 MULTISET EXCEPT 集合2:从一个集合中删除另外一个集合中相同的数据,并返回新集合。

2.5.5、集合1 MULTISET INTERSECT 集合2:返回交集

2.5.6、集合1 MULTISET UNION 集合2:返回并集

DECLARE
  TYPE list_nested IS TABLE OF VARCHAR2(50) NOT NULL;
  v_alla    list_nested := list_nested('sadf', 'cdas', 'ewew');
  v_allb    list_nested := list_nested('123', 'ewew', '567');
  v_newlist1 list_nested;
  v_newlist2 list_nested;
  v_newlist3 list_nested;
BEGIN
  --从一个集合中删除另外一个集合中相同的数据,并返回新集合。
  v_newlist1 := v_alla MULTISET EXCEPT v_allb;
  dbms_output.put_line('v_newlist1信息:');
  FOR x IN 1 .. v_newlist1.count LOOP
    dbms_output.put_line(v_newlist1(x));
  END LOOP;
  --返回两个集合的交集。
  v_newlist2 := v_alla MULTISET INTERSECT v_allb;
  dbms_output.put_line('v_newlist2信息:');
  FOR x IN 1 .. v_newlist2.count LOOP
    dbms_output.put_line(v_newlist2(x));
  END LOOP;
  --返回两个集合的并集。
  v_newlist3 := v_alla MULTISET UNION  v_allb;
  dbms_output.put_line('v_newlist3信息:');
  FOR x IN 1 .. v_newlist3.count LOOP
    dbms_output.put_line(v_newlist3(x));
  END LOOP;
END;

2.5.7、删除集合中重复元素,类似于DISTINCT操作,语法:SET(集合)。也可以利用SET检查变量是否为null:变量 IS[NOT] A SET.

DECLARE
  TYPE list_nested IS TABLE OF VARCHAR2(50) NOT NULL;
  v_all list_nested := list_nested('a', 'a', 'b', 'c', 'c', 'd', 'e');
BEGIN
  IF v_all IS a SET THEN
    dbms_output.put_line('v_all是一个集合');
  END IF;
  dbms_output.put_line('原集合长度:' || cardinality(v_all));
  dbms_output.put_line('集合长度:' || cardinality(SET(v_all)));
END;

2.5.8、集合1 SUBMULTISET OF 集合2:判断集合1是否为集合2的子集


DECLARE
  TYPE list_nested IS TABLE OF VARCHAR2(50) NOT NULL;
  v_alla list_nested := list_nested('cvd', 'dsfsd', 'sd');
  v_allb list_nested := list_nested('cvd');
BEGIN
  IF v_allb submultiset v_alla THEN
    dbms_output.put_line('v_allB是v_allA的子集');
  END IF;

2.6、使用FORALL批量绑定

forall 的使用(FORALL index IN INDICES OF sparse_collection和FORALL index IN VALUES OF pointer_array)
将集合中所有数据进行批量绑定,之后将多条SQL语句一次性发送到数据库中进行执行

DECLARE
  TYPE empno_varray IS VARRAY(8) OF emp.empno%TYPE;
  v_empno empno_varray := empno_varray(7688, 4567, 0982, 7902, 2354);
BEGIN
  FORALL x IN v_empno.first .. v_empno.last
    UPDATE emp SET sal = 9000 WHERE empno = v_empno(x);
  FOR x IN v_empno.first .. v_empno.last LOOP
    dbms_output.put_line('雇员编号:' || v_empno(x) || '更新操作受影响的数据行为:' ||
                         SQL%BULK_ROWCOUNT(x));
  END LOOP;
END;

2.7 、BULK COLLECT批量接收数据

Fetch Bulk Collect into的使用,使用FORALL可以一次性向数据库中发出多条SQL命令,而使用BULK COLLECT可以一次性从数据库中取出多条数据

DECLARE
  TYPE ename_varray IS VARRAY(8) OF emp.ename%TYPE;
  v_ename ename_varray;
BEGIN
  SELECT ename BULK COLLECT INTO v_ename FROM emp WHERE deptno = 10;
  FOR x IN v_ename.first .. v_ename.last LOOP
    dbms_output.put_line('10部门雇员姓名:' || v_ename(x));
  END LOOP END;
DECLARE
  TYPE dept_nested IS TABLE OF dept%ROWTYPE;
  v_dept dept_nested;
BEGIN
  SELECT * BULK COLLECT INTO v_dept FROM dept; --将雇员表信息全部复制到嵌套表中
  FOR x IN v_dept.first .. v_dept.last LOOP
    dbms_output.put_line('部门编号:' || v_dept(x).deptno || ',名称:' || v_dept(x)
                         .dname || ',位置:' || v_dept(x).loc);
  END LOOP;
END;
  DECLARE
   l_Cur_Data cux_item_tbl := cux_item_tbl();
  BEGIN
        l_Cur_Data.Delete;
        SELECT cux_item_rec(tab1.org_code,
                            tab1.wuliaobianma)
          BULK COLLECT
          INTO l_cur_data
          FROM cux_wip_itemfactory_v tab1
         WHERE 1 = 1
           AND tab1.row_num = aaa;
END;

2.8 、with as 的使用

10、with as 的使用
例如:

WITH item_code AS (SELECT 'Z13010002200' item FROM dual)
SELECT q.SEGMENT1,q.DESCRIPTION 
FROM mtl_system_items_b q,
         item_code ic
WHERE 1=1
AND q.SEGMENT1 = ic.item

第三章:游标

游标可以对结果集中每一条数据分别进行操作,当数据量较大时,游标的使用可能会带来性能的降低。
使用前考虑是否有必要使用游标。
游标类型:
静态游标:结果集已经存在(静态定义)的游标。包含(隐式游标、显示游标)
隐式游标:所有DML语句为隐式游标,通过隐式游标属性可以获取SQL语句信息。
显示游标:用户显示声明的游标,即指定结果集。当查询返回结果超过一行时,就需要一个显示游标。
动态游标:
REF游标:动态关联结果集的临时对象,做法更灵活。

3.1、REF动态游标(做法更灵活)

语法:有return属于强类型游标变量,没有为弱类型游标变量
CURSOR 游标变量类型名称 IS REF CURSOR [RETURN 数据类型];
范例:定义一个游标类型,此游标类型为dept类型

DECLARE
  TYPE dept_ref IS REF CURSOR RETURN dept%ROWTYPE; --定义游标类型
  cur_dept  dept_ref; --定义游标变量
  v_deptrow dept%ROWTYPE; --定义行类型
BEGIN
  OPEN cur_dept FOR
    SELECT * FROM dept; --打开游标
  LOOP
    FETCH cur_dept
      INTO v_deptrow;
    EXIT WHEN cur_dept%NOTFOUND; --没有数据退出
    dbms_output.put_line('部门名称:' || v_deptrow.dname);
  END LOOP;
  CLOSE cur_dept;
END;

如果设置为弱类型游标,操作的ROWTYPE必须与游标类型相符
示例:设置错误的数据结构

DECLARE
  TYPE dept_ref IS REF CURSOR;
  cur_dept  dept_ref;
  v_deptrow dept%ROWTYPE;
BEGIN
  OPEN cur_dept FOR
    SELECT * FROM emp; --打开游标,类型错误
  LOOP
    FETCH cur_dept
      INTO v_deptrow; --取得游标数据
    EXIT WHEN cur_dept%NOTFOUND;
    dbms_output.put_line('部门名称:' || v_deptrow.dname);
  END LOOP CLOSE cur_dept;
EXCEPTION
  WHEN rowtype_mismatch THEN
    dbms_output.put_line('游标数据类型不匹配异常。SQLCODE = ' || SQLCODE ||
                         ', SQLERRM = ' || SQLERRM);
END;

定义弱类型游标变量:可以重复操作多种结构

DECLARE
  TYPE cursor_ref IS REF CURSOR;
  cur_var   cursor_ref;
  v_deptrow dept%ROWTYPE;
  v_emprow  emp%ROWTYPE;
BEGIN
  OPEN cur_var FOR
    SELECT * FROM dept; --打开游标
  LOOP
    FETCH cur_var
      INTO v_deptrow;
    EXIT WHEN cur_var%NOTFOUND;
    dbms_output.put_line('部门名称:' || v_deptrow.dname);
  END LOOP;
  CLOSE cur_var; --关闭游标
  OPEN cur_var FOR
    SELECT * FROM emp WHERE deptno = 10; --打开游标
  LOOP
    FETCH cur_var
      INTO v_emprow;
    EXIT WHEN cur_var%NOTFOUND;
    dbms_outupt.put_line('雇员姓名:' || v_emprow.ename);
  END LOOP;
  CLOSE cur_var; --关闭游标
END;

在Oracle 9i之后,为了方便用户使用弱类型的游标变量类型,专门提供了一个SYS_REFCURSOR来代替
TYPE cursor_ref IS REF CURSOR声明,上述游标变量定义可为:
cur_var SYS_REFCURSOR;

3.2、隐式游标(隐式游标可分为单行隐式游标和多行隐式游标)

在PL/SQL块中编写的每条SQL语句实际上是隐式游标。通过DML操作后使用SQL%ROWCOUNT属性,可以知道语句所改变行数(ISNERT、UPDATE、DELETE返回更新行数,SELECT返回查询行数)
SQL%ROWCOUNT操作中,SQL是一个关键字,表示的是任意的一个隐式游标.

PL/SQL中,对于隐式游标SQL可用属性有:
1、%FOUND:当用户使用DML操作数据时,该属性返回TRUE
2、%ISOPEN:判断游标是否打开,该属性对任何的隐式游标总是返回FALSE,表示已经打开。
3、%NOTFOUND:如果执行DML操作时没有返回的数据行,则返回TRUE,否则返回FALSE。
4、%ROWCOUNT:返回更新操作的行数或SELECT INTO返回的行数。

DECLARE
  v_count NUMBER;
BEGIN
  SELECT COUNT(*) INTO v_count FROM stock; --只返回一行结果
  dbms_output.put_line('SQL%ROWCOUNT = ' || SQL%ROWCOUNT);

  INSERT INTO dept (deptno, dname, loc) VALUES (90, 'MLDN', '北京');
  dbms_output.put_line('SQL%ROWCOUNT = ' || SQL%ROWCOUNT);
END;

3.2.2 单行隐式游标:

当通过SQL语句查询时,可以使用SELECT…INTO这样的结构,将查询结果设置给指定的变量,此时返回的结果一般是
一行数据,这样游标称为单行隐式游标。

DECLARE
  v_emprow emp%ROWTYPE; --保存emp每行记录
BEGIN
  SELECT * INTO v_emprow FROM emp WHERE empno = 73689;
  IF SQL%FOUND THEN
    --发现数据
    dbms_output.put_line('雇员姓名:' || v_emprow.ename);
  END IF;
END;

3.2.2 多行隐式游标

主要指的是更新多行数据,或者是查询返回多行数据的操作。

BEGIN
  UPDATE emp SET sal = sal * 1.2;
  IF SQL%FOUND THEN
    --发现数据
    dbms_output.put_line('更新数据行数:' || SQL%ROWCOUNT);
  ELSE
    dbms_output.put_line('没有数据被更改!');
  END IF;
END;

3.3、显示游标

隐式游标是用户操作SQL时自动生成的,而显示游标指的是在声明块中直接定义的游标,每一个游标中,都会保存
SELECT查询后返回结果。

显示游标属性:
1、%FOUND:光标打开后未曾执行FETCH,则值为NULL;如果最近一次在该光标上执行的FETCH返回一行,则值为TRUE,否则FALSE。
2、%ISOPEN:如果光标打开状态则为TRUE,否则FALSE。
3、%NOTFOUND:如果该光标最近一次FETCH语句没有返回行,则值为TRUE,否则为FALSE。如果光标刚打开还未执行FETCH,值为NULL。
4、%ROWCOUNT:值为在该光标上到目前为止执行FETCH语句所返回的行数,光标打开时,%ROWCOUNT初始化为0,每执行一次FETCH
如果返回一行则%ROWCOUNT增加1。
游标操作中不再使用INTO,而是用FETCH…INTO
游标操作前必须打开,关闭后也不可再用。

语法:
CURSOR 游标名([参数列表]) [RETURN 返回值类型]
IS 子查询
[FOR UPDATE [OF 数据列, 数据列,] [NOWAIT]] ;

DECLARE
  CURSOR cur_emp IS
    SELECT * FROM emp;
  v_emprow emp%ROWTYPE;
BEGIN
  IF cur_emp%ISOPEN THEN
    --游标已经打开
    NULL;
  ELSE
    --游标未打开
    OPEN cur_emp; --打开游标
  END IF;
  FETCH cur_emp
    INTO v_emprow; --取出游标当前行数据
  WHILE cur_emp%FOUND LOOP
    --判断是否有数据
    dbms_output.put_line(cur_emp%ROWCOUNT || '、雇员姓名:' || v_emprow.ename ||
                         ',职位:' || v_emprow.job || ',工资:' || v_emprow.sal);
    FETCH cur_emp
      INTO v_emprow; --把游标指向下一行
  END LOOP;
  CLOSE cur_emp; --关闭游标
END;

还可以将游标取得的数据保存在索引表中,随后可以利用索引下标进行指定数据的访问

DECLARE
  CURSOR cur_emp IS
    SELECT * FROM emp;
  TYPE emp_index IS TABLE OF emp%ROWTYPE INDEX BY PLS_INTEGER; --定义索引表
  v_emp emp_index; --定义索引表变量
BEGIN
  FOR emp_row IN cur_emp LOOP
    --利用循环取得每一行记录
    v_emp(emp_row.empno) := cur_emp; --将雇员编号作为索引表下标
  END LOOP;
  dbms_output.put_line('雇员编号:' || v_emp(7369).ename);
END;

使用嵌套表接收游标数据

DECLARE
  TYPE dept_nested IS TABLE OF dept%ROWTYPE; --定义dept的嵌套表类型
  v_dept dept_nested;
  CURSOR cur_dept IS
    SELECT * FROM dept; --定义游标
BEGIN
  IF cur_dept%ISOPEN THEN
    NULL;
  ELSE
    OPEN cur_dept;
  END IF;
  FETCH cur_dept BULK COLLECT
    INTO v_dept; --保存整个游标
  CLOSE cur_dept; --关闭游标
  FOR x IN v_dept.first .. v_dept.last LOOP
    dbms_output.put_line('部门编号:' || v_dept(x).deptno);
  END LOOP;
END;

如果游标中结果集数据量很大上述方法不适合,用户可以使用可变数组限定每次取得的游标数据
取得部分数据保存在数组中.
下面程序采用可变数组保存游标数据,同时为可变数组默认开辟大小为2,所以LIMIT 部门限定了只能取得2行数据。
主要功能是从取得的2行数据中取得第一行。

DECLARE
  TYPE dept_varray IS VARRAY(2) OF dept%ROWTYPE;
  v_dept  dept_varray;
  v_row  NUMBER := 2;  --每次提取行数
  v_count  NUMBER := 1;  --每次少显示1条记录
  CURSOR cur_dept IS SELECT * FROM dept;  --定义游标
BEGIN
  IF cur_dept%ISOPEN THEN
    NULL;
  ELSE
    OPEN cur_dept;
  FETCH cur_dept BULK COLLECT INTO v_dept LIMIT v_rows;  --保存指定行数
  CLOSE cur_dept;  --关闭游标
  FOR x IN v_dept.first..(v_dept.last - v_count) LOOP
    dbms_output.put_line('部门编号:'||v_dept(x).deptno);
  END LOOP;
END;

3.3.1 FOR UPDATE子句

如果游标需要执行更新或删除操作必须带有for update子句,该子句会将游标提取出来的数据进行行级锁定,
这样在本会话更新期间,其他用户的会话就不能对当前游标中数据行进行更新操作,使用形式:
1、FOR UPDATE[OF 列,列…]
为游标中数据列增加行级锁定,这样游标在更新时,其他用户会话将无法更新指定数据
2、FOR UPDATE NOWAIT子句
Oracle中,所有的事务都具备隔离性,当一个用户会话更新数据且事务未提交时,其他的用户是无法对数据进行更新的。
如果此时执行游标数据的更新操作,就会进入到死锁状态,为避免游标出现死锁,可以在创建时使用
FOR UPDATE NOWAIT子句,如果发现所操作数据行已经被锁定,将不会等待立即返回。

--为游标数据增加行级锁
CURSOR cur_emp IS SELECT * FROM emp WHERE deptno = 10 FOR upadte OF sal, comm;
--创建不等待游标
DECLARE
  CURSOR cur_emp IS
    SELECT * FROM emp WHERE deptno = 10 FOR UPDATE NOWAIT;
BEGIN
  FOR emp_row IN cur_emp LOOP
    UPDATE emp SET sal = 9999 WHERE empno = emp_row.empno;
  END LOOP;
END;

本操作创建了一个不等待的游标,验证步骤:
1、启动一个SQLPlus窗口。
2、执行更新操作:update emp set sal = 6666, comm = 3000 where empno = 10;不使用commit提交或ROLLBACK回滚数据
,此时其他用户会话将无法更新这些数据。
3、启动另一个SQLPlus窗口,执行以上游标操作,出现下面提示信息:
ORA-00054:资源正忙,但指定以NOWAIT方式获取资源,或者超时失效。
可发现,此时游标不会出现等待情况,从而避免了出现死锁情况。

3.3.2 WHERE CURRENT OF子句

当用户使用FOR UPDATE语句锁定数据行之后,可以直接利用WHERE CURRENT OF子句进行当前行的更新或删除操作,
语法:WHERE CURRNET OF 游标名称
原理:基于ROWID概念,在更新或删除游标数据时候,可以利用此子句定位数据行,而此子句的创建必须存在有FOR UPDATE子句。

--使用WHERE CURRENT OF子句
DECLARE
  CURSOR cur_emp IS
    SELECT * FROM emp WHERE empno = 10 FOR UPDATE OF sal, comm;
BEGIN
  FOR emp_row IN cur_emp LOOP
    UPDATE emp SET sal = 9999 WHERE CURRENT OF cur_emp; --更新
    --DELETE from emp WHERE CURRENT OF cur_emp;  --使用游标删除数据
  END LOOP;
END;

FOR UPDATE 与 FOR UPDATE OF 列,…区别
建议使用FOR UPDATE OF 列,…形式,可以保证正常更新
区分这两种操作,必须结合where current of子句一起使用,同时创建的游标也要是多表数据。
验证:
使用FOR UPDATE,此时表中sal数据并不会更新,因为当使用多表查询时,直接使用WHERE CURRENT OF子句无法定位到要更新的数据行。
使用FOR UPDATE OF 列,…子句,此时可以正常更新

第四章:子程序(过程和函数)

之前所编写的PL/SQL程序都是以一个程序块的形式出现的,但是这样的程序块并不能被数据库方便的管理,
用户也不能方便使用。可以考虑将程序块封装到一个过程或函数中,这样的结构在Oracle中称为子程序,
通过子程序可以方便的管理或重复使用。
定义为子程序的代码块成为Oracle数据库的对象,会将其对象信息保存在相应的数据字典中。
Oracle中子程序分为两种,过程和函数。

如果用户要创建子程序(过程与函数),需要相关权限:
CREATE ANY PROCEDUR:为任意用户创建存储过程的权限。
CREATE PROCEDUR:为用户创建存储过程的权限。
ALTER PROCEDUR:修改拥有的存储过程权限。
EXECUTE ANY PROCEDUR:执行任意存储过程的权限。
EXECUTE FUNCTION:执行存储函数的权限。
EXECUTE PROCEDURE:执行用户存储过程权限。
DROP ANY PROCEDURE:删除任意存储过程权限。

4.1、子程序定义

过程与函数的选择
1、过程处理返回值时不如函数方便,过程只能依靠OUT或IN OUT参数模式返回数据。
2、编程语言调用过程要比函数更加实用。

4.1.1、定义过程

语法:
CREATE [OR REPLACE] PROCEDURE 过程名称([参数名称 [参数模式] NOCOPY 数据类型
[,参数名 [参数模式] NOCOPY 数据类型, …]])
[AUTHID [DEFINER | CURRENT_USER]]
AS | IS
[PRAGMA AUTONOMOUS_TRANSACTION;]
声明部分;
BEGIN
程序部分;
EXCEPTION
异常处理;
END;

1、参数中定义的参数模式表示过程的数据接收操作,一般分为IN、OUT、IN OUT三类。
2、CREATE [OR REPLACE]:表示创建或替换过程。
3、AUTHID子句定义了一个过程的所有者权限,DEFINER(默认)表示为定义者权限执行,或者使用CURRENT_USER
覆盖程序的默认行为,变为使用者权限执行。
4、PRAGMA AUTONOMOUS_TRANSACTION:表示由过程启动一个自治事务,自治事务可以让主事务挂起,在过程中
执行完SQL后,由用户处理提交或回滚自治事务,然后再恢复主事务。

参数模式
1、IN (默认,数值传递):在子程序中所做修改不会影响原始参数内容。
过程使用IN模式:可以使用default为参数设置默认值,即使调用时不传递此参数内容,也不出错
2、OUT(空进带值出):不带任何数值到子程序中,子程序可以通过此变量将数值返回给调用处。
out模式参数在使用时不会像IN模式需要明确传入一个具体数值,在使用OUT模式时主要将变量传递到过程中,但是
此变量内容不会传递到过程中,过程中如OUT模式的参数修改时最终结果也会返回给相应的实参。
3、IN OUT(按地址传递):可以将值传递到子程序中,同时也会将子程序中对变量的修改返回到调用处。
可以将变量内容传递到过程中,也可以将过程中对其变量的修改返回到原始变量.
IN OUT类似于引用传递(或者理解为指针)

CREATE OR REPLACE PROCEDURE get_emp_info_proc(p_eno emp.empno%TYPE) AS
  v_ename emp.ename%TYPE;
  v_sal   emp.sal%TYPE;
  v_count NUMBER;
BEGIN
  SELECT COUNT(empno) INTO v_count FROM emp WHERE  empno = p_eno;
  IF v_count = 0 THEN
    RETURN; --没有数据则返回
  END IF;
  SELECT ename, sal INTO v_ename, v_sal FROM emp WHERE empno = p_eno;
  dbms_output.put_line('编号为:' || p_eno || '的员工姓名:' || v_ename || ',工资:' ||
                       v_sal);
END;

4.1.2、定义函数

函数与过程最大区别:函数是可以有返回值的,而过程只能依靠OUT或IN OUT返回数据

语法:
CREATE [OR REPLACE] FUNCTION 函数名(参数,…)
RETURN 返回值类型
[AUTHID {DEFINER | CURRENT_USER}]
AS | IS
[PRAGMA AUTONOMOUS_TRANSACTION;]
声明部分;
BEGIN
程序部分;
[RETURN 返回值;]
[EXCEPTION
异常处理]
END [函数名];

CREATE OR REPLACE FUNCTION get_sal_func(p_eno emp.empno%TYPE) RETURN NUMBER AS
  v_salary emp.sal%TYPE;
BEGIN
  SELECT sal + nvl(comm, 0) INTO v_salary FROM emp WHERE empno = p_eno;
  RETURN v_salary;
END;

4.2、查询子程序

当用户创建一个函数或过程后,就相当于创建了一个新的数据库对象,可以利用数据字典查看子程序相关信息:
user_procedures:查询出所有的子程序信息。
user_objects:查询出所有的用户对象(包括表、索引、序列、子程序等)。
user_source:查看用户所有对象源代码。
user_errors:查看所有子程序的错误信息。

4.3、删除子程序

DROP PROCEDURE 过程名;
DROP FUNCTION 函数名;

4.4、子程序嵌套

在一个子程序中定义其他的子程序,此时只需要在子程序的声明部分编写即可

4.4.1、定义嵌套过程

CREATE OR REPLACE PROCEDURE dept_insert_proc(p_dno    dept.deptno%TYPE,
                                             p_dna    dept.dname%TYPE,
                                             p_loc    dept.loc%TYPE,
                                             p_result OUT NUMBER) AS
  v_deptcount NUMBER;
  PROCEDURE get_dept_count_proc( --定义嵌套过程,判断部门编号是否存在
                                p_temp  dept.deptno%TYPE,
                                p_count OUT NUMBER) --返回统计结果
   AS
  BEGIN
    SELECT COUNT(deptno) INTO p_count FROM dept WHERE deptno = p_temp;
  END;
  PROCEDURE insert_operate_proc( --定义嵌套过程,执行增加
                                p_temp_dno dept.deptno%TYPE,
                                p_temp_dna dept.dname%TYPE,
                                p_temp_loc dept.loc%TYPE,
                                p_count    NUMBER,
                                p_flag     OUT NUMBER) --通过此参数返回
   AS
  BEGIN
    IF p_count > 0 THEN
      p_flag := -1;
    ELSE
      INSERT INTO dept
        (deptno, dname, loc)
      VALUES
        (p_temp_dno, p_temp_dna, p_temp_loc);
      p_flag := 0; --修改返回标记
      COMMIT;
    END IF;
  END;
BEGIN
  get_dept_count_proc(p_dno, v_deptcount); --判断是否有此部门
  insert_operate_proc(p_dno, p_dna, p_loc, v_deptcount, p_result);
END;

简化做法:本程序中每一个子过程都是按照独立的方式设计的,即过程有自己的参数来接受,但是这种内部结构可以直接访问外部结构中定义的参数、变量,所以也可以简化为以下形式

CREATE OR REPLACE PROCEDURE dept_insert_proc(p_dno    dept.deptno%TYPE,
                                             p_dna    dept.dname%TYPE,
                                             p_loc    dept.loc%TYPE,
                                             p_result OUT NUMBER) AS
  v_deptcount NUMBER;
  PROCEDURE get_dept_count_proc --返回统计结果
   AS
  BEGIN
    SELECT COUNT(deptno) INTO v_deptcount FROM dept WHERE deptno = p_dno;
  END;
  PROCEDURE insert_operate_proc AS
  BEGIN
    IF v_deptcount > 0 THEN
      p_result := -1;
    ELSE
      INSERT INTO dept (deptno, dname, loc) VALUES (p_dno, p_dna, p_loc);
      p_result := 0;
      COMMIT;
    END IF;
  END;
BEGIN
  get_dept_count_proc();
  insert_operate_proc();
END;

4.4.2、子程序重载

DECLARE
  PROCEDURE get_dept_info_proc(p_deptno dept.deptno%TYPE) AS
  BEGIN
    dbms_output.put_line('部门编号:' || p_deptno);
  END;
  PROCEDURE get_dept_info_proc(p_dname dept.dname%TYPE) AS
  BEGIN
    dbms_output.put_line('部门名称:' || p_dname);
  END;
BEGIN
  get_dept_info_proc(10);
  get_dept_info_proc('销售部');
END;

4.4.3、实现函数递归调用

DECLARE
  v_sum NUMBER;
  FUNCTION add_func(p_num NUMBER) RETURN NUMBER AS
  BEGIN
    IF p_num = 1 THEN
      RETURN 1;
    ELSE
      RETURN p_num + add_func(p_num - 1);
    END IF;
  END;
BEGIN
  v_sum := add_func(100); --进行1~100累加
  dbms_output.put_line('累加结果:' || v_sum);
END;

4.5、NOCOPY选项

默认情况下,对于IN模式传递的参数都是引用传递方式,所以性能较高,而对于OUT或IN OUT模式采用的是值传递,
在传递时会将数据复制给形参,过程结束后,被赋予OUT或IN OUT形参上的值会复制回对应的实参。当传递数据较大
时(如集合、记录等),复制过程会很长,这时就可以用NOCOPY选项,将值传递变为引用传递。
注意:IN参数模式无法使用NOCOPY

除了提高传递性能,利用NOCOPY方式定义的参数,即使程序中出现了错误,也可以正常返回。

语法
参数名称 [参数模式] NOCOPY 数据类型;

DECLARE
  TYPE dept_nested IS TABLE OF dept%ROWTYPE;
  v_dept dept_nested;
  PROCEDURE usenocopy_proc(p_temp IN OUT NOCOPY dept_nested) IS
  BEGIN
    --相关代码
  END;
BEGIN
  SELECT * BULK COLLECT INTO v_dept FROM dept; --将雇员信息复制到嵌套表中
  v_dept.extend(200000, 1); --将集合扩充,数据以第1条记录为准进行填充
  usenocopy_proc(v_dept); --使用nocopy
END;

4.6、自治事务

子程序中需要进行独立的子事务处理的时候,就需要用到自治事务,自治事务是在主事务上单独开启的独立事务,
自治事务处理期间,主事务会暂时挂起,一直等到自治事务执行COMMIT或ROLLBACK之后才恢复主事务执行。

语法:
PRAGMA AUTONOMOUS_TRANSACTION;

--范例:使用自治事务:自治事务能插入数据,主事务插入的数据回滚了
DECLARE
  PROCEDURE dept_insert_proc AS
    PRAGMA AUTONOMOUS_TRANSACTION; --自治事务
  BEGIN
    --此处更新将使用自治事务,主事务将被挂起
    INSERT INTO dept (deptno, dname, loc) VALUES (50, 'MLDN', '北京');
    COMMIT;
  END;
BEGIN
  INSERT INTO dept (deptno, dname, loc) VALUES (60, '开发部', '天津');
  dept_insert_proc();
  ROLLBACK; --此处为主事务回滚
END;

4.7、 子程序权限

如果不同用户之间的子程序要进行访问,则必须授权

范例:将bonus_proc子程序的执行权限授予c##mldnuser用户,使用c##scott用户名密码登录再授权
GRANT EXECUTE ON c##scott.bonus_proc TO c##mldnuser;

不管在哪个用户下执行的子程序,都会将数据更改操作反应到子程序的创建者那里。即如果c##mldnuser用户执行了
子程序,数据变动会反应到c##scott用户下的相关表中,即使c##mldnuser用户有同样的表。
默认情况下,一个子程序“创建时”访问的资源只能是当前用户下的对象,即子程序定义权限为DEFINER(AUTHID DEFINER,
此为默认设置),为了解决这样问题(不同用户调用子程序应该当前用户名下表操作),用户创建子程序时可以改为
CURRENT_USER选项,这样操作资源是当前连接用户下的资源。

CREATE OR REPLACE PROCEDURE bonus_proc AUTHID CURRENT_USER AS
BEGIN
  INSERT INTO bonus
    (ename, job, sal, comm)
  VALUES
    ('张三', '程序员', 5000, 2000);
  COMMIT;
END;

第五章:包

包是一种程序模块化设计的主要实现手段,通过包可以将一个模块中所要使用的各个程序结构(过程、函数、游标
类型、变量)放在一起进行管理,同时包中所定义的程序结构也可以方便进行互相调用。

包的组成:
1、包规范:定义包中可以被外部访问的部分,在包规范中声明的内容可以从应用程序和包任何地方访问
语法:
CREATE OR [REPLACE] PACKAGE 包名
[AUTHID CURRENT_USER | DEFINER]
IS | AS
结构名称定义(类型、过程、函数、游标、异常等)
END [包名];
2、包体:负责包规范中定义的函数或过程的具体实现代码,如果在包体中定义了包规范中没有内容,则此部分内容被
设置为私有访问。
语法:
CREATE OR [REPLACE] PACKAGE BODY 包名
IS | AS
结构实现(类型、过程、函数、游标、异常等)
BEGIN
包初始化程序代码;
END [包名];

范例:
定义包规范:主要定义了一个get_emp_func()函数,该函数可以被任何应用程序所调用。

CREATE OR REPLACE PACKAGE mldn_pkg AS
  FUNCTION get_emp_func(p_dno dept.deptno%TYPE) RETURN SYS_REFCURSOR;
  --返回弱游标类型
END;

定义包体实现get_emp_func()函数

CREATE OR REPLACE PACKAGE BODY mldn_pkg AS
  FUNCTION get_emp_func(p_dno dept.deptno%TYPE) RETURN SYS_REFCURSOR AS
    cur_var SYS_REFCURSOR;
  BEGIN
    OPEN cur_var FOR
      SELECT * FROM emp WHERE deptno = p_dno; --打开参数游标
    RETURN cur_var;
  END;
END;

定义完包后,可以通过user_objects和user_source两个数据字典查看
SELECT * FROM user_objects WHERE object_type IN(‘PACKAGE’, ‘PACKAGE BODY’);
SELECT * FROM user_source WHERE TYPE = ‘PACKAGE’ AND NAME = ‘MLDN_PKG’;

5.1、调用包中函数

DECLARE
  V_RECEIVE SYS_REFCURSOR;
  V_EMPROW  EMP%ROWTYPE;
BEGIN
  V_RECEIVE := MLDN_PKG.GET_EMP_FUNC(10);
  LOOP
    FETCH V_RECEIVE
      INTO V_EMPROW; --取得游标数据
    EXIT WHEN V_RECEIVE%NOTFOUND;
    DBMS_OUTPUT.PUT_LINE('雇员姓名:' || V_EMPROW.ENAME);
  END LOOP;
END;

5.2、删除包

–删除包规范:在删除包规范时会将其对应的包体一起删除
DROP PACKAGE 包名;
–删除包体
DROP PACKAGE BODY 包名;

5.3、重新编译包

语法:
ALTER PACKAGE 包名 COMPILE [DEBUG] PACKAGE | SPECIFICATION | BODY [REUSE SETTINGS];

进行包重新编译时有3种编译方式:
PACKAGE:重新编译包规范和包体
SPECIFICATION:重新编译包规范。
BODY:重新编译包体。

ALTER PACKAGE mldn_pkg COMPILE PACKAGE;
ALTER PACKAGE mldn_pkg COMPILE BODY;

5.4、包的作用域

默认情况下,所有的包在第一次被调用时才会进行初始化操作,而后包的运行状态保存到用户全局区的会话中,
在一个会话期间内,此包会一直被用户占用,一直到会话结束才会被释放。因此包中的任何一个变量或游标等可以在
一个会话期间一直存在,相当于全局变量,同时可以被所有子程序所共享。

在包规范中定义全局变量

CREATE OR REPLACE PACKAGE MLDN_PKG AS
  V_DEPTNO DEPT.DEPTNO%TYPE := 10;
  FUNCTION GET_EMP_FUNC(P_ENO EMP.EMPNO%TYPE) RETURN EMP%ROWTYPE;
END;

定义包体实现

CREATE OR REPLACE PACKAGE BODY MLDN_PKG AS
  FUNCTION GET_EMP_FUNC(P_ENO EMP.EMPNO%TYPE) RETURN EMP%ROWTYPE AS
    V_EMPROW EMP%ROWTYPE;
  BEGIN
    SELECT *
      INTO V_EMPROW
      FROM EMP
     WHERE EMPNO = P_ENO
       AND DEPTNO = V_DEPTNO;
    RETURN V_EMPROW;
  END;
END;

5.5、重载包中的子程序

包规范:

CREATE OR REPLACE PACKAGE emp_delete_pkg AS
  --删除雇员时所发生的异常
  emp_delete_exception EXCEPTION;
  PROCEDURE delete_emp_proc(p_empno emp.empno%TYPE);
  PROCEDURE delete_emp_proc(p_ename emp.ename%TYPE);
  PROCEDURE delete_emp_proc(p_deptno emp.deptno%TYPE, p_job emp.job%TYPE);
END;

包体:

CREATE OR REPLACE PACKAGE  BODY emp_delete_pkg 
AS
  PROCEDURE delete_emp_proc(p_empno emp.empno%TYPE) AS
    BEGIN
      DELETE FROM emp WHERE empno = p_empno;
      IF SQL%NOTFOUND THEN
        RAISE emp_delete_exception;
      END IF;
      END delete_emp_proc;
  PROCEDURE delete_emp_proc(p_ename emp.ename%TYPE) AS
    BEGIN
      DELETE FROM emp WHERE ename = p_ename;
      IF SQL%NOTFOUND THEN
        RAISE emp_delete_exception;
      END IF;
  END delete_emp_proc;
  PROCEDURE delete_emp_proc(p_deptno emp.deptno%TYPE, p_job emp.job%TYPE) AS
    BEGIN
      DELETE FROM emp WHERE deptno = p_deptno AND job = p_job;
      IF SQL%NOTFOUND THEN
        RAISE emp_delete_exception
      END IF;
  END delete_emp_proc;
END;

5.6、包的初始化

如果当某个会话第一次使用时某个包时可以由用户制定一些属于自己的初始化操作,例如为集合数据
进行内容填充或一些更加复杂的业务代码。
如果编写包初始化代码,可以直接在包体中定义BEGIN语句,在此部分编写初始化代码。

定义包规范:

CREATE OR REPLACE PACKAGE init_pkg AS
  --定义索引表类型,使用数字做索引
  TYPE dept_index IS TABLE OF dept%ROWTYPE INDEX BY PLS_INTEGER;
  --定义游标
  CURSOR dept_cur RETURN dept%ROWTYPE;
  v_dept dept_index;
  --定义部门增加操作函数
  FUNCTION dept_insert_func(p_deptno dept.deptno%TYPE,
                            p_dname  dept.dname%TYPE,
                            p_loc    dept.loc%TYPE) RETURN BOOLEAN;
END;

定义包体:

CREATE OR REPLACE PACKAGE BODY init_pkg AS
  CURSOR dept_cur RETURN dept%ROWTYPE IS
    SELECT * FROM dept;
  FUNCTION dept_insert_func(p_deptno dept.deptno%TYPE,
                            p_dname  dept.dname%TYPE,
                            p_loc    dept.loc%TYPE) RETURN BOOLEAN AS
  BEGIN
    IF NOT v_dept.exists(p_deptno) THEN
      --数据不存在
      INSERT INTO dept
        (deptno, dname, loc)
      VALUES
        (p_deptno, p_dname, p_loc);
      v_dept(p_deptno).deptno := p_deptno;
      v_dept(p_deptno).dname := p_dname;
      v_dept(p_deptno).loc := p_loc;
      RETURN TRUE;
    ELSE
      RETURN FALSE;
    END IF;
  END dept_insert_func;
BEGIN
  --包初始化操作,将游标中数据保存到索引表中,以部门编号为索引
  FOR dept_row IN dept_cur LOOP
    v_dept(dept_row.deptno) := dept_row;
  END LOOP;
EXCEPTION
  WHEN OTHERS THEN
    dbms_output.put_line('程序出现错误!');
END;

5.7、包的纯度级别

有时候需要对包中函数进行限制。
纯度等级:
WNDS:函数不能修改数据库表数据(即无法使用DML更新)。
RNDS:函数不能读数据库表(即无法使用SELECT查询)。
WNPS:函数不允许修改包中变量内容。
RNPS:函数不允许读取包中变量内容。

定义包中函数纯度级别:

CREATE OR REPLACE PACKAGE purity_pkg AS
  --定义包中变量
  v_name VARCHAR2(10) := 'mldn';
  FUNCTION emp_delete_func_wnds(p_empno emp.empno%TYPE) RETURN NUMBER;
  FUNCTION emp_find_func_rnds(p_empno emp.empno%TYPE) RETURN NUMBER;
  FUNCTION change_name_func_wnps(p_param VARCHAR2) RETURN VARCHAR2;
  FUNCTION get_name_func_rnps(p_param NUMBER) RETURN VARCHAR2;
  --设置函数纯度级别
  PRAGMA RESTRICT_REFERENCES(emp_delete_func_wnds, WNDS);
  PRAGMA RESTRICT_REFERENCES(emp_find_func_rnds, RNDS);
  PRAGMA RESTRICT_REFERENCES(change_name_func_wnps, WNPS);
  PRAGMA RESTRICT_REFERENCES(get_name_func_rnps, RNPS);
END;

定义违反纯度级别的包体:

CREATE OR REPLACE PACKAGE BODY purity_pkg AS
  FUNCTION emp_delete_func_wnds(p_empno emp.empno%TYPE) RETURN NUMBER AS
  BEGIN
    --此函数由于定义了wnds纯度,所以无法执行更新操作
    DELETE FROM emp WHERE empno = p_empno;
    RETURN 0;
  END;
  --根据雇员编号查找雇员信息,但函数不能执行SELECT操作
  FUNCTION emp_find_func_rnds(p_empno emp.empno%TYPE) RETURN NUMBER AS
    v_emp emp%ROWTYPE;
  BEGIN
    SELECT * INTO v_emp FROM emp WHERE empno = p_empno;
    RETURN 0;
  END;
  --使用新的内容修改v_name变量内容,但此函数不能修改包中变量
  FUNCTION change_name_func_wnps(p_param VARCHAR2) RETURN VARCHAR2 AS
  BEGIN
    v_name := p_param;
  END;
  --读取v_name属性内容,但此函数不能读取包中变量
  FUNCTION get_name_func_rnps(p_param NUMBER) RETURN VARCHAR2 AS
  BEGIN
    RETURN v_name;
  END;
END;

5.8、关于公用函数的说明

如果用户在编写可被SQL直接引用的包公共函数,函数必须符合WNDS、WNPS、RNPS这三个纯度级别。

CREATE OR REPLACE PACKAGE purity2_pkg AS
  FUNCTION tax_func(p_sal emp.sal%TYPE) RETURN NUMBER;
  PRAGMA RESTRICT_REFERENCES(tax_func, WNDS, WNPS, RNPS);
END;

5.9、系统工具包

1、DBMS_OUTPUT包
SELECT * FROM all_source WHERE TYPE = ‘PACKAGE’ AND NAME = ‘DBMS_OUTPUT’;
2、DBMS_JOB包与数据库作业
3、DBMS_ASSERT包
4、DBMS_LOC包

第六章:触发器

触发器可以在数据库中对用户所发出的操作进行跟踪,同时作出处理,基本定义形式与过程及函数类似,唯一不同的是
所有过程与函数需要用户显示调用,触发器是由操作隐式调用的。
Oracle中触发器主要分为
1、DML触发器、
2、instead_of(替代)触发器、
3、DDL触发器、
4、系统或数据库时间触发器。

语法:
CREATE [OR REPLACE] TRIGGER 触发器名
[BEFORE | AFTER] --触发时间(操作之前还是之后触发)
[INSTEAD OF]
[INSERT | UPDATE | UPDATE OF 列名[,列名…] | DELETE] --触发事件
ON [表名 | 视图 | DATABASE | SCHEMA] --触发对象
[REFERENCING [OLD AS 标记] [NEW AS 标记] [PARENT AS 标记]]
[FOR EACH ROW] --触发频率:定义行级触发,如果不写表示定义表级触发器
[FOLLOWS 触发器名] --配置多个触发器执行的先后次序
[DISABLE] --触发器建立之后默认是启用状态,可用此句禁用
[WHEN 触发条件]
[DECLARE] --触发操作(程序主体)
[程序声明部分;]
[PRAGMA AUTONOMOUS_TRANSACTION;] --自治事务声明
BEGIN
程序代码部分;
END [触发器名];

编写触发器时注意点:
1、触发器不接受任何参数,并且只能是在产生了某一触发事件之后才会自动调用。
2、对于一张表的触发器,最多只能有12个,同一种类型触发器只能定义一次。
3、一个触发器最大为32KB,所以如果需要编写的代码较多,可以通过过程或函数调用完成。
4、默认情况下,触发器中不能使用事务处理操作,或者采用自治事务进行处理。
5、在一张数据表中,如果定义过多触发器,则会造成DML性能下降。

6.1、DML触发器

DML触发器主要由DML语句触发,当用户执行了增删改操作时就会触发。

语法:
CREATE [OR REPLACE] TRIGGER 触发器名称
[BEFORE | AFTER]
[INSERT | UPDATE | UPDATE OF 列名[, 列名…] | DELETE] ON 表名
[FOR EACH ROWCOUNT]
[DISABLE]
[WHEN 触发条件]
[DECLARE]
[程序声明部分;]
BEGIN
程序代码部分;
END [触发器名];

触发器执行操作顺序:
1、BEFORE表级触发器执行。
2、BEFORE行级触发器执行。
3、执行更新操作。
4、AFTER行级触发器执行。
5、AFTER表级触发器执行。

6.2、表级触发器

表级触发器指的是针对全表数据的检查,每次更新数据表时,只会在更新之前或之后触发一次,表级触发器不需要配置FOR EACH ROW选项。

范例1:只有在每个月10日才允许办理离职与入职,其他时间不允许增加新雇员数据。

CREATE OR REPLACE TRIGGER FORBID_EMP_TRIGGER
  BEFORE INSERT OR DELETE ON EMP
DECLARE
  V_CURRENTDATE VARCHAR2(20);
BEGIN
  SELECT TO_CHAR(SYSDATE, 'dd') INTO V_CURRENTDATE FROM DUAL;
  IF TRIM(V_CURRENTDATE) != '10' THEN
    RAISE_APPLICATION_ERROR(-20008, '在每月的10号才允许办理入/离职手续!');
  END IF;
END;

范例2:在星期一、周末及每天下班时间(9:00前,18:00后)不允许更新emp数据表

CREATE OR REPLACE TRIGGER forbid_emp_trigger
BEFORE INSERT OR DELETE OR UPDATE
ON
emp
DECLARE
  v_currentweak VARCHAR(20);
  v_currenthour VARCHAR(20);
BEGIN
  SELECT to_char(SYSDATE, 'day'), to_char(SYSDATE, 'hh24') INTO v_currentweak, v_currenthour FROM dual;
  IF TRIM(v_currentweak) = '星期一' OR TRIM(v_currentweak) = '星期六' OR TRIM(v_currentweak) = '星期日' THEN
    raise_application_error(-20008, '在周末以及周一不允许对emp表进行更新!');
  ELSE IF TRIM(v_currenthour) < '9' OR TRIM(v_currenthour) > '18' THEN
      RAISE_application_error(-20008, '在下班时间不能够修改emp数据!');
  END IF;
END;

范例3:在每天12点后,不允许修改雇员工资和佣金。

CREATE OR REPLACE TRIGGER forbid_emp_trigger
BEFORE UPDATE OF sal, comm
ON emp
DECLARE
  V_CURRENTHOUR VARCHAR(20);
BEGIN
  SELECT TO_CHAR(SYSDATE, 'hh24') INTO V_CURRENTHOUR FROM DUAL;
  IF TRIM(V_CURRENTHOUR) > '12' THEN
    RAISE_APPLICATION_ERROR(-20008,
                            '每天12点后,不允许修改员工工资和佣金!');
  END IF;
END;

6.3、行级DML触发器

前面所讲的触发器操作是对整张表进行DML操作前后才进行的触发操作,并且只在更新前后触发一次,而行级触发器指的是表中每行记录出现更新操作时进行的触发操作,即如果某些操作影响了多行数据,则每行数据更新时都会引起触发,行级触发器需要使用FOR EACH ROW
在使用行级触发器时,可以在触发器内部访问正在处理中的行数据,此时可以通过两个相关的标识符“:.old.字段”和“:.new.字段”实现,分别表示触发前后值(insert前old未定义,delete后new未定义)

范例1:增加雇员信息时,其职位只能在已有职位中选择,且工资不超过5000

CREATE OR REPLACE TRIGGER forbid_emp_trigger
BEFORE INSERT 
ON emp
FOR EACH ROW
DECLARE
  V_JOBCOUNT NUMBER;
BEGIN
  SELECT COUNT(EMPNO)
    INTO V_JOBCOUNT
    FROM EMP
   WHERE :NEW.JOB IN (SELECT DISTINCT (JOB) FROM EMP);
  IF V_JOBCOUNT = 0 THEN
    RAISE_APPLICATION_ERROR(-20008, '增加雇员职位信息错误!');
  ELSE
    IF :NEW.SAL > 5000 THEN
      RAISE_APPLICATION_ERROR(-20008, '增加雇员工资不能超过5000!');
    END IF;
  END IF;
END;

范例2:修改emp表基本工资涨幅不超过10%

CREATE OR REPLACE TRIGGER forbid_emp_trigger
BEFORE UPDATE OF sal
ON emp
FOR EACH ROW
BEGIN
  IF ABS((:NEW.SAL - :OLD.SAL) / :OLD.SAL) > 0.1 THEN
    RAISE_APPLICATION_ERROR(-20008, '雇员工资修改幅度太大!');
  END IF;
END;

范例3:不能删除所有10部门雇员。

CREATE OR REPLACE TRIGGER emp_delete_trigger
BEFORE DELETE
ON emp
FOR EACH ROW
BEGIN
  IF :OLD.DEPTNO = 10 THEN
    RAISE_APPLICATION_ERROR(-20008, '不能删除部门为10的雇员信息!');
  END IF;
END;

注意:不能将:new或:old直接赋值给一个ROWTYPE变量,但可以通过:new或:old访问字段。
错误的程序,触发器中无法修改“:old”数据

CREATE OR REPLACE TRIGGER EMP_UPDATE_OLD_TRIGGER
  BEFORE UPDATE OF SAL ON EMP
  FOR EACH ROW
BEGIN
  :OLD.SAL = 100; --错误,无法修改“:old”数据
END;

虽然不允许修改“:OLD”数据,但是Oracle中触发器可以修改“:OLD”数据,如通过序列手工实现数据的自动增长,MYSQL和DB2可以实现数据列的自动增长,但Oracle稍麻烦些,现在可以利用触发器方式解决。操作流程:
第一步:用户发出一个执行INSERT指令,但是此时不设置自动增长列内容。
第二步:定义一个增加前的触发器,在触发器中,修改“:OLD”标识符对应的自动增长列的内容。
第三步:由触发器发出一条INSERT语句,执行数据增加。
若将以上三步都编写在对于一张表执行增加的触发器操作的话,就会出现触发器自己调用自己的情况,造成死循环,解决此类问题最简单方法是,触发器不直接对member表触发,而是针对一张与member表结构完全相同的membertemp表触发,用户通过membertemp表执行增加操作,然后在membertemp表中的触发器中将这些增加数据插入到member表中,同时删除membertemp表中数据。

1、创建数据库脚本

DROP SEQUENCE member_sequence;
DROP TABLE MEMBER PURGE;
DROP TABLE membertemp PURGE;
CREATE SEQUENCE member_sequence;
CREATE TABLE MEMBER(
  mid NUMBER,
  NAME VARCHAR2(30),
  address VARCHAR2(30),
  CONSTRAINT pk_mid PRIMARY KEY(mid)
);
CREATE TABLE membertemp AS SELECT * FROM MEMBER WHERE 1 = 2;

2、触发器中修改:new数据

CREATE OR REPLACE TRIGGER MEMBER_INSERT_TRIGGER
  BEFORE INSERT ON MEMBERTEMP
  FOR EACH ROW
BEGIN
  DELETE FROM MEMBERTEMP;
  INSERT INTO MEMBER
    (MID, NAME, ADDRESS)
  VALUES
    (MEMBER_SEQUENCE.NEXTVAL, :NEW.NAME, :NEW.ADDRESS);
END;

3、向membertemp表中增加数据

INSERT INTO membertemp(NAME, address) VALUES('aaa', 'bbb');
INSERT INTO membertemp(NAME, address) VALUES('ccc', 'ddd');

4、查询member表

SELECT * FROM MEMBER;

换种方式实现本程序的触发器

CREATE OR REPLACE TRIGGER MEMBER_INSERT_TRIGGER
  BEFORE INSERT ON MEMBERTEMP
  FOR EACH ROW
DECLARE
  V_MEMBERROW MEMBER%ROWTYPE;
BEGIN
  DELETE FROM MEMBERTEMP;
  SELECT MEMBER_SEQUENCE.NEXTVAL INTO :NEW.MID FROM DUAL;
  V_MEMBERROW.MID     := :NEW.MID;
  V_MEMBERROW.NAME    := :NEW.NAME;
  V_MEMBERROW.ADDRESS := :NEW.ADDRESS;
  INSERT INTO MEMBER VALUES V_MEMBERROW;
END;

使用REFERENCING子句设置别名

CREATE OR REPLACE TRIGGER EMP_INSERT_EMP
  BEFORE UPDATE OF SAL ON EMP
  REFERENCING OLD AS EMP_OLD NEW AS EMP_NEW
  FOR EACH ROW
BEGIN
  IF ABS((:EMP_NEW.SAL - :EMP_OLD.SAL) / :EMP_OLD.SAL) > 0.1 THEN
    RAISE_APPLICATION_ERROR(-20008, '雇员工资涨幅太大!');
  END IF;
END;

使用WHEN子句定义触发条件
WHEN子句使用new和old可以不用:

CREATE OR REPLACE TRIGGER EMP_INSERT_TRIGGER
  BEFORE INSERT ON EMP
  FOR EACH ROW
  WHEN (NEW.SAL = 0)
BEGIN
  RAISE_APPLICATION_ERROR(-20008,
                          :NEW.EMPNO || '的工资为0,不符合工资规定!');
END;

范例:要求工资只能上涨,不能降低

CREATE OR REPLACE TRIGGER EMP_SAL_UPDATE_TRIGGER
  BEFORE UPDATE ON EMP
  WHEN (NEW.SAL < OLD.SAL)
BEGIN
  RAISE_APPLICATION_ERROR(-20008,
                          :OLD.EMPNO || '的工资少于其原本工资,不符合规定!');
END;

触发器谓词:区分不同的DML操作
范例:对dept表执行一个操作日志的功能。

DROP TABLE dept_log PURGE;
DROP SEQUENCE dept_log_seq;
CREATE SEQUENCE dept_log_seq;
CREATE TABLE dept_log(
  logid NUMBER,
  TYPE VARCHAR2(20) NOT NULL,
  deptno NUMBER(2),
  logdate DATE,
  dname VARCHAR2(14) NOT NULL,
  oc VARCHAR2(13) NOT NULL,
  CONSTRAINT pk_logid PRIMARY KEY(logid)
);

定义触发器,对不同的DML操作进行日志记录

CREATE OR REPLACE TRIGGER dept_update_trigger
BEFORE INSERT OR UPDATE OR DELETE
ON emp
FOR EACH ROW
  BEGIN
    IF inserting THEN
      INSERT INTO dept_log(logid, TYPE, logdate, deptno, dname, loc)
             VALUES(dept_log_seq.nextval, 'INSERT', SYSDATE, :new.deptno, :new.dname, :new.loc);
    ELSE IF UPDATING THEN
      INSERT INTO dept_log(logid, TYPE, logdate, deptno, dname, loc)
             VALUES(dept_log_seq.nextval, 'UPDATE', SYSDATE, :new.deptno, :new.dname, :new.loc);
    ELSE
      INSERT INTO dept_log(logid, TYPE, logdate, deptno, dname, loc)
             VALUES(dept_log_seq.nextval, 'DELETE', SYSDATE, :old.deptno, :old.dname, :old.loc);
    END IF;
END;

如果一个表创建了多个触发器,又想指定执行顺序,可以使用FOLLOWS子句

CREATE OR REPLACE TRIGGER EMP_INSERT_ONE
  BEFORE INSERT ON EMP
  FOR EACH ROW
BEGIN
  DBMS_OUTPUT.PUT_LINE('执行第1个触发器(emp_insert_one)');
END;
/
CREATE OR REPLACE TRIGGER EMP_INSERT_TWO
  BEFORE INSERT ON EMP
  FOR EACH ROW FOLLOWS EMP_INSERT_ONE
BEGIN
  DBMS_OUTPUT.PUT_LINE('执行第2个触发器(emp_insert_two)');
END;
/
CREATE OR REPLACE TRIGGER EMP_INSERT_THREE
  BEFORE INSERT ON EMP
  FOR EACH ROW FOLLOWS EMP_INSERT_TWO
BEGIN
  DMPS_OUTPUT.PUT_LINE('执行第3个触发器(emp_isnert_three)');
END;

变异表:当一张表上执行了增删改操作后,该表就变成了一张变异表,如果该表设置了行级触发器,则会出现ORA-04091的异常,
举例说明:

CREATE TABLE info(
  ID NUMBER,
  title VARCHAR2(50)
);
INSERT INTO info(ID, title) VALUES(1, 'abcd');
--为info增加一个触发器
CREATE OR REPLACE TRIGGER INFO_TRIGGER
  BEFORE INSERT OR UPDATE OR DELETE ON INFO
  FOR EACH ROW
DECLARE
  V_INFOCOUNT NUMBER;
BEGIN
  SELECT COUNT(ID) INTO V_INFOCOUNT FROM INFO;
END;

执行更新操作将会报错
UPDATE info SET ID = 2;

分析:此时,在修改数据表的时候,一定会引起触发器工作,而触发器试图取得表记录数,但是由于操作的数据表没有更新结束,所以无法得到个数。对于这种错误,在没有触发器的时候是不会发生的,但是如果有了行级触发器,则表示每次当表被修改时都会由于每行的更新操作被触发,直到修改操作之前触发器都无法看到数据表的变化,虽然可以使用:new和:old这两个标识符,但是不能读取到表的状态。

6.4、复合触发器

复合触发器在Oracle 11g后引入,它既是表级触发器又是行级触发器。在之前对于不同级别的触发器,如果要在一张表完成表记触发(before after)和行级触发器(before after),需要编写4个触发器才行,有了复合触发器后,用一个就行了。
触发执行语句之前:BEFORE STATEMENT
触发语句中的每一行发生变化之前:BEFORE EACH ROW
触发语句中的每一行发生变化之后:AFTER EACH ROW
触发执行语句之后:AFTER STATEMENT

验证复合触发器

CREATE OR REPLACE TRIGGER COMPOUND_TRIGGER
  FOR INSERT OR UPDATE OR DELETE ON DEPT COMPOUND TRIGGER       --此时是FOR
  BEFORE STATEMENT IS --语句执行前触发(表级)
BEGIN
  DBMS_OUTPUT.PUT_LINE('1、BEFORE STATEMENT');
END BEFORE STATEMENT;
  BEFORE EACH ROW IS --语句执行前触发(行级)
BEGIN DBMS_OUTPUT.PUT_LINE('2、BEFORE EACH ROW.'); END
  BEFORE EACH ROW;
  AFTER STATEMENT IS --语句执行后触发(表级)
BEGIN DBMS_OUTPUT.PUT_LINE('3、AFTER STATEMENT.'); END
  AFTER STATEMENT;
  AFTER EACH ROW IS --语句执行后触发(行级)
BEGIN DBMS_OUTPUT.PUT_LINE('4、AFTER EACH ROW.'); END
  AFTER EACH ROW; END;

复合触发器执行流程与前面不同的DML触发器(行级+表级)执行顺序相同:
执行更新前“表级”触发器
执行更新前“行级”触发器
执行更新后“行级”触发器
执行更新后“表级”触发器

范例,在dept表定义一个触发器,如果执行增加操作,且增加的部门名称或位置没有填写时,将部门名称
设置为mldnjava,位置设置为中国

CREATE OR REPLACE TRIGGER COMPOUND_TRIGGER
  FOR INSERT OR UPDATE OR DELETE ON DEPT COMPOUND TRIGGER
  BEFORE EACH ROW IS --语句执行前触发(行级)
BEGIN
  IF INSERTING THEN
    IF :NEW.DNAME IS NULL THEN
      :NEW.DNAME := 'mldnjava';
    END IF;
    IF :NEW.LOC IS NULL THEN
      :NEW.LOC = '中国';
    END IF;
  END IF;
END BEFORE EACH ROW;
END;

增加信息测试

INSERT INTO dept(deptno) VALUES(99);
SELECT * FROM dept WHERE deptno = 99;

范例:对emp定义一个触发器,要求:在周末不允许更新emp表数据;更新数据时,要求将所有增加的数据自动更改为大写;重新完成后,新增雇员的工资不得高于公司平均工资。

CREATE OR REPLACE TRIGGER EMP_COMPOUND_TRIGGER
FOR INSERT OR UPDATE OR DELETE ON EMP
COMPOUND TRIGGER
  BEFORE STATEMENT IS --周末不允许更新
    V_CURRENTWEAK VARCHAR2(20);
  BEGIN
    SELECT TO_CHAR(SYSDATE, 'day') INTO V_CURRENTWEAK FROM DUAL;
    IF TRIM(V_CURRENTWEAK) IN ('星期六', '星期日') THEN
      RAISE_APPLICATION_ERROR(-20008, '在周末不允许更新emp表数据!');
    END IF;
  END BEFORE STATEMENT;
  BEFORE EACH ROW IS 
    V_AVGSAL EMP.SAL%TYPE; 
  BEGIN 
    IF INSERTING OR UPDATING THEN 
      :NEW.ENAME := UPPER(:NEW.ENAME); 
      :NEW.JOB := UPPER(:NEW.JOB); 
    END IF; 
    IF INSERTING THEN 
      SELECT AVG(SAL) INTO V_AVGSAL FROM EMP; 
      IF :NEW.SAL > V_AVGSAL THEN 
        RAISE_APPLICATION_ERROR(-20008, '新增雇员工资不得高于公司平均工资!'); 
      END IF; 
    END IF;
  END BEFORE EACH ROW; 
END;

6.5、instead-of触发器

如果一张视图的数据由多张表组成,那么视图是无法直接更新的,如果想实现此类视图的更新操作,需要用到替代触发器。instead-of触发器不需要编写BEFORE 和 AFTER

范例:创建一张视图,包含20部门员工信息

CREATE OR REPLACE VIEW v_myview AS
SELECT e.empno, e.ename, e.job, e.sal, d.deptno, d.dname, d.loc 
FROM emp e, dept d
WHERE e.deptno = d.deptno AND d.deptno = 20;

向视图插入一条数据将会报错:ORA-01776:无法通过联结视图修改多个基表

INSERT INTO v_myview(empno, ename, job, sal, deptno, dname, loc)
VALUES(...);

分析:如果想实现数据的增加,就需要把视图中设置的增加数据分别设置到不同数据表。

CREATE OR REPLACE TRIGGER view_trigger
INSTEAD OF INSERT ON v_myview
FOR EACH ROW
DECLARE
  v_empCount NUMBER;
  v_deptCount NUMBER;
BEGIN
  --判断要增加的雇员是否存在
  SELECT COUNT(empno) INTO v_empCount FROM emp WHERE empno = :new.empno;
  --判断要增加的部门是否存在
  SELECT COUNT(deptno) INTO v_deptCount FROM dept WHERE deptno = :new.deptno;
  IF v_deptCount = 0 THEN 
    INSERT INTO dept(deptno, dname, loc) VALUES(:new.deptno, :new.dname, :new.loc);
  END IF;
  IF v_empCount = 0 THEN
    INSERT INTO emp(empno, ename, job, sal, deptno) VALUES(:new.empno, :new.ename, :new.job, :new.sal, :new.deptno);
  END IF;
END;

执行视图增加操作

INSERT INTO v_myview(empno, ename, job, sal, deptno, dname, loc)    
VALUES(...);
SELECT * FROM emp;
SELECT * FROM deptno;

实现视图修改数据的替代触发器:更新操作时需要更新多表

CREATE OR REPLACE TRIGGER view_trigger
INSTEAD OF UPDATE ON v_myview
FOR EACH ROW
BEGIN
  UPDATE emp SET ename = :new.ename, job = :new.job, sal = :new.sal WHERE empno = :new.empno;
  UPDATE dept SET dname = :new.dname, loc = :new.loc WHERE deptno = :new.deptno;
END;

执行更新操作

UPDATE v_myview SET ename = '..', sal = 200, dname = '..' WHERE empno = 1234;
COMMIT;
SELECT * FROM v_myview;
SELECT * FROM emp;
SELECT * FROM dept;

实现视图删除数据的替代触发器

CREATE OR REPLACE TRIGGER view_trigger
INSTEAD OF DELETE ON v_myview
FOR EACH ROW
DECLARE
  v_empCount NUMBER;
BEGIN
  DELETE FROM emp WHERE empno = :old.empno;
  SELECT COUNT(empno) INTO v_empCount FROM emp WHERE empno = :old.empno;
  IF v_empCount = 0 THEN
    DELETE FROM dept WHERE deptno = :old.deptno;
  END IF;
END;

删除视图中所有20部门的雇员信息

DELETE FROM v_myview WHERE deptno = 29;
COMMIT;
SELECT * FROM emp WHERE deptno = 20;
SELECT * FROM dept WHERE deptno = 20;

将3个不同功能的替代触发器变为一个

CREATE OR REPLACE TRIGGER view_trigger
INSTEAD OF INSERT OR UPDATE OR DELETE ON v_myview
FOR EACH ROW
DECLARE
  v_empCount NUMBER;
  v_deptCount NUMBER;
BEGIN
  IF inserting THEN
    --判断要增加雇员是否存在
    SELECT COUNT(empno) INTO v_empCount FROM emp WHERE empno = :new.empno;
    SELECT COUNT(deptno) INTO v_deptCount FROM dept WHERE deptno = :new.deptno;
    IF v_deptCount = 0 THEN
      INSERT INTO dept(deptno, dname, loc) VALUES(:new.deptno, :new.dname, :new.loc);
    END IF;
    IF v_empCount = 0 THEN
      INSERT INTO emp(empno, ename, job, sal, deptno) VALUES(:new.empno, :new.ename, :new.job, :new.sal, :new.deptno);
    END IF;
  ELSE IF updating THEN
    UPDATE emp SET ename = :new.ename, job = :new.job, sal = :new.sal WHERE empno = :new.empno;
    UPDATE dept SET dname = :new.dname, loc = :new.loc WHERE deptno = :new.deptno;
  ELSE IF deleting THEN
    DELETE FROM emp WHERE empno = :old.empno;
    SELECT COUNT(empno) INTO v_empCount FROM mep WHERE empno = :old.empno;
    IF v_empCount = 0 THEN  --此部门没有雇员
      DELETE FROM dept WHERE deptno = :old.deptno;
    END IF;
  ELSE
    NULL;
  END IF;
END;

Tips:当视图包含了以下结构,就表示为不可更新的视图:
1、统计函数。
2、CASE 或 DECODE语句;
3、GROUP BY、 HAVING子句;
4、DISTINCT消除重复列;
5、集合运算连接。

在嵌套表上定义替代触发器
如果用户所创建的视图中包含了嵌套表的数据,则在对视图更新时,必须使用替代触发器操作
定义嵌套表
1、定义复合类型

DROP TYPE project_nested;
DROP TYPE project_type;
CREATE OR REPLACE TYPE project_type AS OBJECT(
  projectid NUMBER,
  projectname VARCHAR2(50),
  projectfunds NUMBER,
  pubdate DATE
);

2、嵌套表类型

CREATE OR REPLACE TYPE project_nested AS TABLE OF project_type NOt NULL;

3、创建嵌套表类型的数据表

DROP TABLE department PURGE;
CREATE TABLE department(
  did NUMBER,
  depename VARCHAR2(50) NOT NULL,
  projects project_nested,
  CONSTRAINT pk_die PRIMARY KEY(did)
)NESTED TABLE projects STORE AS projects_nested_table;

4、增加测试数据

INSERT INTO department(did, deptname, projects) VALUES(10, 'asd',
  project_nested(
    project_type(1, 'java', 9000, to_date('2004-05-06', 'yyyy-mm-dd')),
    project_type(2, 'Android', 8900, to_date('2005-06-07', 'yyyy-mm-dd'))
  ));
 
INSERT INTO department(did, deptname, projects) VALUES(20, 'qwe',
  project_nested(
    project_type(10, 'c++', 7900, to_date('2004-05-06', 'yyyy-mm-dd')),
    project_type(11, 'python', 6000, to_date('2005-06-07', 'yyyy-mm-dd'))
  ));
  COMMIT;

上面已经定义完成了一个嵌套表,同时也增加了相应数据。再创建一张只包含10部门信息的视图,在此视图中存在嵌套表类型projects列

CREATE OR REPLACE VIEW v_department10
AS
SELECT did, deptname, projects
FROM department
WHERE did = 10;

查看视图中嵌套表数据

SELECT * FROM TABLE(SELECT projects FROM v_department10);

对视图中嵌套表执行DML操作会报错

定义替代触发器实现对视图中嵌套表数据更新:

CREATE OR REPLACE TRIGGER nested_trigger
INSTEAD OF INSERT OR UPDATE OR DELETE
ON NESTED TABLE projects OF v_department10
DECLARE
BEGIN
  IF inserting THEN
    INSERT INTO TABLE (SELECT projects FROM department WHERE did = :parent.did)
           VALUES(:new.projectid, :new.projectname, :new.projectfunds, :new.pubdate);
  ELSE IF updating THEN
    UPDATE TABLE (SELECT projects FROM department WHERE did = :parent.did) pro
           SET VALUE(pro) = project_type(:new.projectid, :new.projectname, :new.projectfunds,
           :new.pubdate)
           WHERE pro.projectid = :old.projectid;
  ELSE IF deleting THEN
    DELETE FROM TABLE(
      SELECT projects FROM department WHERE did = :parent.did) pro
      WHERE pro.projectid = :old.projectid;
  END IF;
END;

6.6、DDL触发器

当创建、修改或删除数据库对象时,也会引起相应的触发器操作事件,而此时就可以利用触发器对这些数据库对象的DDL操作进行监控。

语法:
CREATE [OR REPLACE] TRIGGER 触发器名
[BEFORE | AFTER | INSTEAD OF][DLL 事件] ON [DATABASE | SCHEMA]
[WHEN 触发条件]
[DECLARE]
[程序声明部分;]
BEGIN
程序代码部分;
END [触发器名];

在DDL触发器创建语法中,给出的操作对象有两种:
DATABASE:对数据库级的触发,需要相应的管理员权限(如sys用户)才可以创建。
SCHEMA:对一个具体的模式(用户)的触发,每个用户都可以直接创建。

DDL触发器支持事件
NO. DDL事件 触发时机 描述
1 ALTER BEFORE/AFTER 修改对象结构时触发
2 ANALYZE BEFORE/AFTER 分析数据库对象时触发
3 ASSOCIATE STATISTICS … 启动统计数据库对象时触发
4 AUDIT … 开启审核数据库对象时触发
5 COMMENT … 为数据库对象设置注释信息触发
6 CREATE … 创建数据库对象时触发
7 DDL … 针对出现的所有DDL事件触发
8 DISASSOCIATE STATISTICS … 关闭统计数据库对象时触发
9 DROP … 删除数据库对象时触发
10 GRANT … 用户授权时触发
11 NOAUDIT … 禁用审核数据库对象时触发
12 RENAME … 为数据库对象重命名时触发
13 REVOKE … 用户撤销权限时触发
14 TRUNCATE … 截断数据表时触发

常用的事件属性函数:
NO. 事件属性函数 描述
1 ORA_CLIENT_IP_ADDRESS 取得客户端IP,如果本地连接则为NULL,返回数据类型VARCHAR2
2 ORA_DATABASE_NAME 取得数据库名,返回类型VARCHAR2
3 ORA_DES_ENCRYPTED_PASSWORD 取得加密后口令内容,返回VARCHAR2
4 ORA_DICT_OBJ_NAME 取得对象名,返回VARCHAR2
5 ORA_DICT_OBJ_NAME_LIST(nameList OUT ORA_NAME_LIST_T) 返回被修改的对象名称列表
6 ORA_DICT_OBJ_OWNER 取得对象的拥有者,返回的数据类型为VARCHAR2
7 ORA_DICT_OBJ_OWNER_LIST(objList OUT_ORA_NAME_LIST_T) 返回被修改对象的所有者列表
8 ORA_DICT_OBJ_TYPE 返回对象类型
9 ORA_GRANTEE(nameList OUT ORA_NAME_LIST_T) 返回授予的权限或角色列表
10 ORA_INSTANCE_NUM 取得当前数据库中的实例编号
11 ORA_IS_ALTER_COLUMN(columnName VARCHAR2) 判断列名称是否被修改,返回数据类型为BOOLEAN
12 ORA_IS_CREATING_NESTED_TABLE 如果创建一个嵌套表,返回数据类型为BOOLEAN
13 ORA_IS_DROP_COLUMN(columnName VARCHAR2) 判断一个列是否被删除,返回BOOLEAN
14 ORA_IS_SERVERERROR(errorCode Number) 判断是否出现了指定的错误编号,返回BOOLEAN
15 ORA_LOGIN_USER 取得当前模式名称,返回VARCHAR2
16 ORA_REVOKE(nameList OUT ORA_NAME_LIST_T) 返回撤销的权限或角色列表
17 ORA_SERVER_ERROR(point NUMBER) 返回错误堆栈信息中的错误号,其中1为错误堆栈顶端,返回NUMBER
18 ORA_SERVER_ERROR_MSG 返回错误堆栈信息中的错误信息,其中1为错误堆栈顶端,返回VARCHAR2
19 ORA_SYSEVENT 返回触发器操作事件,返回VARCHAR2
ORA_NAME_LIST_T是定义在DBMS_STANDARD包中的一个嵌套表类型,结构定义如下:
TYPE ora_name_list_t IS TABLE OF VARCHAR2(64);

范例:禁止c##scott用户的所有DDL操作

CREATE OR REPLACE TRIGGER SCOTT_FORBID_TRIGGER
  BEFORE DDL ON SCHEMA
BEGIN
  RAISE_APPLICATION_ERROR(-20008, 'SCOOT用户禁止使用任何DDL操作!');
END;

该用户创建一个序列会报错:Oracle中保存的数据表、序列、视图、用户、索引都是以对象形式出现。
CREATE SEQUENCE seq_mldn;

数据库对象操作日志记录表创建脚本

DROP TABLE object_log PURGE;
DROP SEQUENCE object_log_seq;
CREATE SEQUENCE object_log_seq;
CREATE TABLE object_log(
  oidd NUMBER CONSTRAINT pk_oid PRIMARY KEY,
  username VARCHAR2(50) NOT NULL,
  operatedate DATE NOT NULL,
  objecttype VARCHAR2(50) NOt NULL,
  objectowner VARCHAR2(50) NOt NULL
);

编写触发器实现数据库对象操作记录

CREATE OR REPLACE TRIGGER OBJECT_TRIGGER
  AFTER CREATE OR DROP OR ALTER ON DATABASE
DECLARE
BEGIN
  INSERT INTO C##SCOTT.OBJECT_LOG
    (OIDD, USERNAME, OPERATEDATE, OBJECTTYPE, OBJECTOWNER)
  VALUES
    (C##SCOTT.OBJECT_LOG_SEQ.NEXTVAL,
     ORA_LOGIN_USER,
     SYSDATE,
     ORA_DICT_OBJ_TYPE,
     ORA_DICT_OBJ_OWNER);
 
END;

禁止修改emp表主键empno和deptno列定义结构.

分析:每次修改需要取出表所有列判断其是否被修改,可以用all_tab_columns数据字典来查询,再用游标将信息依次判断,而后使用ORA_IS_ALTER_COLUMN和ORA_IS_DROP_COLUMN事件属性判断当前修改列或删除列的名字是否empno或deptno。
为了保证此操作可以对当前用户有用,可以定义一个参数游标,通过ORA_DICT_OBJ_OWNER取得操作表的用户,通过ORA_DICT_OBJ_NAME取得操作数据表名称。

在c##scott用户中定义参数游标。

CREATE OR REPLACE TRIGGER emp_alter_trigger
BEFORE ALTER
ON SCHEMA
DECLARE
  --操作的所有者及操作的表名称由外部传递
  CURSOR emp_column_cur(p_tableOwner All_Tab_Columns.owner%TYPE, p_tableName All_Tab_Columns.table_name%TYPE) IS
  SELECT column_name FROM All_Tab_Columns
  WHERE owner = p_tableOwner AND table_name = p_tableName;
BEGIN
  IF ora_dict_obj_type = 'TABLE' THEN   --如果操作的是数据表
    FOR empColumnRow IN emp_column_cur(ora_dict_obj_owner, ora_dict_obj_name) LOOP
      IF ora_is_alter_column(empColumnRow.column_name) THEN
        --empno字段要被修改
        IF empColumnRow.column_name = 'EMPNO' THEN
          raise_application_error(-20007, 'empno字段不允许修改!');
        END IF;
        --deptno字段要被修改
        IF empColumnRow.column_name = 'DEPTNO' THEN
          raise_application_error(-20007, 'deptno字段不允许被修改!');
        END IF;
        IF ora_is_drop_column(empColumnRow.column_name) THEN
          --empno字段要被删除
          IF empColumn.column_name = 'EMPNO' THEN
            raise_application_error(-20007, 'empno字段不允许删除!');
          END IF;
          --deptno字段要被删除
          IF empColumn.column_name = 'DEPTNO' THEN
            raise_application_error(-20007, 'DEPTNO字段不允许删除!');
          END IF;
        END IF;
      END LOOP;
    END IF;
  END;

修改empno字段

ALTER TABLE emp MODIFY(empno NUMBER(6));

删除deptno字段

ALTER TABLE emp DROP COLUMN deptno;

6.7、系统触发器

系统触发器用于监视数据库服务的打开、关闭、错误等信息的取得,或者监控用户的行为操作等。

语法:
CREATE [OR REPLACE] TRIGGER 触发器名称
[BEFORE | AFTER] [数据库事件] ON [DATABASE | SCHEMA]
[WHEN 触发条件]
[DECLARE]
[程序声明部分;]
BEGIN
程序代码部分;
END [触发器名称];

系统触发器事件:
NO. 事件 触发时机 描述
1 STARTUP AFTER 数据库实例启动之后触发
2 SHUTDOWN BEFORE 数据库实例关闭之前触发
3 SERVERERROR AFTER 出现错误时触发
4 LOGON AFTER 用户登录后触发
5 LOGOFF BEFORE 用户注销前触发

6.7.1 、登陆日志功能

创建一个监控数据库打开(STARTUP)及关闭(SHUTDOWN)的触发器,在database_log表中保存信息。
由于本程序是针对数据库级的触发操作,所以以下数据表及触发器创建都是在sys用户下完成。

编写user_log表

DROP SEQUENCE user_log_seq;
DROP TABLE user_log PURGE;
CREATE SEQUENCE user_log_seq;
CREATE TABLE user_log(
  logid NUMBER CONSTRAINT pk_logid PRIMARY KEY,
  username VARCHAR2(50) NOT NULL,
  logondate DATE,
  logoffdate DATE,
  ip VARCHAR2(20),
  logtype VARCHAR2(20)
);

监控用户登录触发器logon_trigger.

CREATE OR REPLACE TRIGGER logon_trigger
AFTER logon
ON DATABASE
BEGIN
  INSERT INTO user_log(logid, username, logondate, ip, logtype)
              VALUES(user_log_seq.nextval, ora_login_user, SYSDATE, ora_client_ip_address, 'LOGON');
END;

监控用户注销触发器logoff_trigger

CREATE OR REPLACE TRIGGER logoff_trigger
BEFORE logoff
ON DATABASE
BEGIN
  INSERT INTO user_log(logid, username, logondate, ip, logtype)
              VALUES(user_log_seq.nextval, ora_login_user, SYSDATE, ora_client_ip_address, 'LOGOFF');
END;

回到sys用户查看user_log表记录

conn SYS/PASSWORD AS SYSDBA;
SELECT * FROM user_log;

由于上述操作触发时机不同(AFTER BEFORE),所以并不能将上述两个触发器合并为一个

6.7.2、系统启动和关闭日志功能

在一张数据库事件表(db_event_log)记录数据库实例启动和关闭的日期时间

DROP TABLE db_event_log PURGE;
DROP SEQUENCE db_event_log_seq;
CREATE SEQUENCE db_event_log_seq;
CREATE TABLE db_event_log(
  eventId NUMBER CONSTRAINT pk_eventId PRIMARY KEY,
  eventType VARCHAR2(50) NOT NULL,
  eventDate DATE NOT NULL,
  eventUser VARCHAR2(50) NOT NULL
);

编写触发器,启动之后触发

CREATE OR REPLACE TRIGGER startup_trigger
AFTER startup
ON DATABASE
BEGIN
  INSERT INTO db_event_log(eventId, eventType, eventDate, eventUser)
              VALUES(db_event_log_seq.nextval, 'STARTUP', SYSDATE, ora_login_user);
  COMMIT;
END;

编写触发器,关闭之前触发

CREATE OR REPLACE TRIGGER shutdown_trigger
BEFORE SHUTDOWN
ON DATABASE
BEGIN
  INSERT INTO db_event_log(eventId, eventType, eventDate, eventUser)
              VALUES(db_event_log_seq.nextval, 'SHUTDOWN', SYSDATE, ora_login_user);
  COMMIT;
END;

创建完后,使用sys用户,依次执行数据库实例关闭(shutdown immediate)与打开(startup)两个操作再查表。

SHUTDOWN ABORT
startup
SELECT * FROM db_event_log;

6.7.3、错误信息日志

数据库在开发和使用中会出现许多错误信息,可以利用SERVERERROR对所出现的错误进行触发。在对错误信息进行触发操作时,可以使用一个DBMS_UTILITY.FORMAT_ERROR_STACK来获取错误堆栈信息。

创建一张记录错误信息表db_error

DROP SEQUENCE db_error_seq;
DROP TABLE db_error PURGE;
CREATE SEQUENCE db_error_seq;
CREATE TABLE db_error(
  eId NUMBER CONSTRAINT pk_eId PRIMARY KEY,
  username VARCHAR2(50),
  errorDate DATE,
  dbname VARCHAR2(50),
  errContent CLOB                   --最多可以保存2GB
);

定义触发器

CREATE OR REPLACE TRIGGER error_trigger
AFTER servererror ON DATABASE
BEGIN
  INSERT INTO db_error(eId, username, errorDate, dbname, errContent)
              VALUES(db_error_seq.nextval, ora_login_user, Sysdate, ora_database_name,
                                           dbms_utility.format_error_stack);
END;

触发器定义完成后,可以在c##scott用户下分别执行两个错误操作,然后查表

SELECT * FROM mldn;   --ORA-00942:表或视图不存在
INSERT INTO dept(deptno, dname, loc) VALUES(...);   --ORA-00001:违反唯一约束条件
SELECT * FROM db_error;

6.8、管理触发器

触发器本身属于数据库中对象,所有的数据库对象都可以被创建、删除、修改、查询。

6.8.1、查询触发器:使用三个字典user_triggers、all_triggers、dba_triggers

SELECT * FROM user_triggers; --trigger_body字段给出代码

6.8.2、禁用/启用触发器

ALTER TRIGGER 触发器名 [DISABLE | ENABLE];
查询数据字典,确定触发器状态
SELECT trigger_name, status FROM user_triggers;
oracle 11g之后可以创建禁用触发器
CREATE OR REPLACE TRIGGER emp_update_trigger
BEFORE INSERT OR UPDATE OR DELETE
ON dept
DISABLE --禁用
FOR EACH ROW
BEGIN
NULL;
END;
禁用一张表全部触发器
ALTER TABLE [scheme.]表名 [ENABLE | DISABLE] ALL TRIGGER;
启用一张表全部触发器
ALTER TABLE emp ENABLE ALL TRIGGER;

6.8.3、删除触发器

DROP TRIGGER 触发器名;

6.9、触发器中调用子程序

一个触发器最多只能由32KB代码,如果代码量过大,可以通过调用子程序实现。
如果所调用的函数或过程删除了,会导致触发器无法使用,如果子程序被改变,触发器操作状态可能无效,此时可以使用after trigger[模式.]触发器名 compile [debug]语法重新编译触发器。

范例:每月10号允许办理新近人员入职,同时入职新员工工资不超过公司平均工资。分别定义一个函数和过程

CREATE OR REPLACE PROCEDURE emp_update_date_proc
AS
  v_currentdate VARCHAR2(20);
BEGIN
  SELECT to_char(SYSDATE, 'dd') INTO v_currentdate FROM dual;
  IF TRIM(v_currentdate) != '10' THEN
    raise_application_error(-20008, '每月10号才可以办理入职手续!');
  END IF;
END;

函数:检索平均工资

CREATE OR REPLACE FUNCTION emp_avg_sal
RETURN NUMBER
AS
  v_avg_sal emp.sal%TYPE;
BEGIN
  SELECT AVG(sal) INTO v_avg_sal FROM emp;
  RETURN v_avg_sal;
END;

触发器中调用

CREATE OR REPLACE TRIGGER frobid_emp_trigger
BEFORE INSERT
ON emp
FOR EACH ROW
BEGIN
  emp_update_date_proc;   --调用过程
  IF :new.sal > emp_avg_sal() THEN   --调用函数
    raise_application_error(-20008, '新进雇员工资不得高于公司平均工资!');
  END IF;
END;

第七章:动态SQL

7.1、动态SQL简介

前面所编写的PL/SQL有一个特点:所操作数据库对象必须存在,否则创建的子程序就会出问题,这样的操作在开发中称为静态SQL操作。
程序中编写静态SQL操作方便实现,但很多时候一些子程序所操作的数据库对象需要由外部绑定支持,此时就必须依靠动态SQL完成,实现动态绑定。

范例:利用动态SQL在执行时创建一张数据表,该函数主要任务是查询指定数据表中记录数,若表不存在则创建。

CREATE OR REPLACE FUNCTION get_table_count_fun(p_table_name VARCHAR2)
RETURN NUMBER
AS
  v_sal_statement VARCHAR2(200);   --定义操作的SQL语句
  v_count NUMBER;                  --定义表中记录
BEGIN
  SELECT COUNT(*) INTO v_count FROM user_tables WHERE table_name = UPPER(p_table_name);
  IF v_count = 0 THEN  --数据表不存在
    v_sql_statement := 'create table ' || p_table_name ||
                                '(id number,
                                  name varchar2(30) not null,
                                  constraint pk_id_' || p_table_name ||' primary key(id) )';  --创建数据表
    EXECUTE IMMEDIATE v_sql_statement;       --执行动态SQL
  END IF;
  v_sql_statement := 'select count(*) from ' || p_table_name;  --查询数据表记录
  EXECUTE IMMEDIATE v_sql_statement INTO v_count;  --执行动态SQL并保存数据表记录
  RETURN v_count;
END; 

提示:如果不使用EXECUTE IMMEDIATE程序会出错。

7.2、EXECUTE IMMEDIATE子句

动态中该子句非常重要,使用此子句可以很方便的在PL/SQL中执行DML、DDL、DCL(GRANT、REVOKE)语句。
语法:
EXECUTE IMMEDIATE 动态SQL字符串 [[BULK COLLECT] INTO 自定义变量,… | 记录类型]
[USING [IN | OUT | IN OUT]绑定参数,…]
[[RETURNING | RETURN] [ BULK COLLECT] INTO 绑定参数,…];
EXECUTE IMMEDIATE由以下3个主要子句组成:
1、INTO:保存动态SQL执行结果,如果返回多行结果,可以通过BULK COLLECT设置批量保存。
2、USING:用来为动态SQL设置占位符设置内容
3、RETURNING | RETURN:使用效果一样,是取得更新表记录被影响的数据,通过BULK COLLECT设置批量绑定。

7.2.1、使用动态SQL创建表和PL/SQL块:

DECLARE
  v_sql_statement VARCHAR2(200);
  v_count NUMBER;   --保存查找结果
BEGIN
  SELECT COUNT(*) INTO v_count FROM user_tables WHERE table_name = 'MLDN_TAB';
  IF v_count = 0 THEN   --数据表不存在
    v_sql_statement := 'create table mldn_tab(
                                       id number primary key,
                                       url varchar2(50) not null)';    --定义动态SQL
    EXECUTE IMMEDIATE v_sql_statement;
  ELSE   --表存在
    v_sql_statement := 'truncate table MLDN_TAB';
    EXECUTE IMMEDIATE v_sql_statement;
  END IF;
  v_sql_statement := 'BEGIN
                  FOR X IN 1... 10 LOOP
                    INSERT INTO MLDN_TAB(id, url) values(x, "www.mldnjava.com-"||x);
                  end loop;
                  end;';
  EXECUTE IMMEDIATE v_sql_statement;  
  COMMIT;
END;

7.2.2、使用绑定变量

DECLARE
  v_sql_statement VARCHAR2(200);
  v_deptno dept.deptno%TYPE := 60;
  v_dname dept.dname%TYPE := 'mldn';
  v_loc dept.loc%TYPE := '北京';
BEGIN
  v_sql_statement := 'insert into dept(deptno, dname, loc) values(:dno, :dna, :dl)';
  EXECUTE IMMEDIATE v_sql_statement USING v_deptno, v_dname, v_loc;
  COMMIT;
END;

注意,如果有字段为NULL,则不能直接绑定NULL,需要通过变量设置,比如希望loc内容为NULL,可以将v_loc变量设置为NULL

7.2.3、利用集合操作多条记录

DECLARE
  v_sql_statement VARCHAR2(200);
  TYPE deptno_nested IS TABLE OF dept.deptno%TYPE NOT NULL;
  TYPE dname_nested IS TABLE OF dept.dname%TYPE NOT NULL;
  v_deptno deptno_nested := deptno_nested(10, 20, 30, 40);
  v_dname dname_nested := dname_nested('财务部', '研发部', '销售部', '操作不');
BEGIN
  v_sql_statement := 'update dept set dname = :dna where deptno = :dno';
  FOR x IN 1.. v_deptno.count LOOP
    EXECUTE IMMEDIATE v_sql_statement USING v_dname(x), v_deptno(x);
  END LOOP;
  COMMIT;
END;

7.2.4、拼接字符串形成动态SQL

通过以上操作可以发现,所有使用绑定变量的代码都只是针对基本的数据类型,例如字符串,数字等,但是这种方式不可能针对DDL操作,例如,将要创建或截断的表名称使用绑定变量就会出现错误。
此时create是DDL操作命令,所以无法使用绑定变量设置表名称,同理,对于删除、截断表等操作也一样无法使用,如果要使用,可以采用拼接字符串的方式完成。

DECLARE
  v_sql_statement VARCHAR2(200);
  v_table_name VARCHAR2(200) := 'mldn';
  v_id_column VARCHAR2(200) := 'id';
BEGIN
  v_sql_statement := 'create table '||v_table_name|| '(' || v_id_column || ' number primary key)';
  EXECUTE IMMEDIATE v_sql_statement;
END;

接收DML更新行数:当用户使用DML进行更新操作后,可以利用RETURNING INTO子句接收更新(INSERT DELETE UPDATE)后影响的数据行的详细信息。使用RETURN RETURNING都行更新数据,取得更新后的结果

DECLARE
  v_sql_statement VARCHAR2(200); --定义sql操作语句
  v_empno emp.empno%TYPE := 7269;  --要更新的雇员编号
  v_salary emp.sal%TYPE;   --保存更新后sql内容
  v_job emp.job%TYPE;  --保存更新后job内容
BEGIN
  v_sql_statement := 'update emp set sal = sal * 1.2 , job = "开发" ' ||
                     ' where empno =:eno returning sal, job into :salary, :job ';
  EXECUTE IMMEDIATE v_sql_statement USING v_empno RETURNING INTO v_salary, v_job;
  dbms_outupt.put_line('调整后的工资:' || v_salary ||',新的职位:'||v_job);
END;

删除数据,取得删除前的结果

DECLARE
  v_sql_statement VARCHAR2(200);   --定义SQL操作语句
  v_emprow emp%ROWTYPE;   --保存emp类型
  v_empno emp.empno%TYPE := 7369;  --删除的雇员编号
  v_ename emp.ename%TYPE;   --删除的雇员姓名
  v_sal emp.sal%TYPE;       --删除的雇员工资
BEGIN
  v_sql_statement := 'delete from emp where empno =: eno returning ename, sal into
                     :name, :sal ';
  EXECUTE IMMEDIATE v_sql_statement USING v_empno RETURNING INTO v_ename, v_sal;
  dbms_output.put_line('删除的雇员编号:' || v_empno ||',姓名:'||v_ename||',工资:'||v_sal);                     
  END;

在使用USING或RETURNING语句时都可以设置参数模式(IN、OUT、IN OUT),其中对USING子句主要是使用变量定义的
内容,所以默认的是IN模式,使用RETURNING子句时不需要设置内容,只需要接收返回内容,所以模式为OUT

--编写部门增加过程
CREATE OR REPLACE PROCEDURE dept_insert_proc(
  p_deptno IN OUT dept.deptno%TYPE,      --此处可以将p_deptno内容回传
  p_dname dept.dname%TYPE,               --默认为IN模式
  p_loc dept.loc%TYPE) AS                --默认为IN模式
BEGIN
  SELECT MAX(deptno) INTO p_deptno FROM dept;     --取得最大的dept内容
  p_deptno := p_deptno + 1;        --让最大部门编号加1,此处不考虑超过2位数的情况
  INSERT INTO dept(deptno, dname, loc) VALUES(p_deptno, p_dname, p_loc);
END;
--编写PL/SQL块,调用过程
DECLARE
  v_sql_statement VARCHAR2(200);
  v_deptno dept.deptno%TYPE
  v_dname dept.dname%TYPE := 'MLDN教学部';
  v_loc dept.loc%TYPE := '北京';
BEGIN
  v_sql_statement := 'begin
                        dept_insert_proc(:dno, :dna, :dl);
                      end;';                   --定义PL/SQL块
  EXECUTE IMMEDIATE v_sql_statement USING IN OUT v_deptno, IN v_dname, v_loc;
  dbms_output.put_line('新增部门编号为:' || v_deptno);                      
END;

7.3 、批量绑定

通过动态SQL进行查询或更新操作时,每次都是向数据库提交了一条操作语句,如果现在希望数据库可以一次性接收
多条SQL,以及数据库可以一次性将操作结果返回到某一个集合中,就可以采用批量绑定操作完成,主要依靠BULK
COLLECT进行操作。

7.3.1、更新时使用BULK COLLECT语句

DECLARE
  TYPE ename_index IS TABLE OF emp.ename%TYPE INDEX BY PLS_INTEGER;
  TYPE job_index IS TABLE OF emp.job%TYPE INDEX BY PLS_INTEGER;
  TYPE sal_index IS TABLE OF emp.sal%TYPE INDEX BY PLS_INTEGER;
  v_ename ename_index;
  v_job job_index;
  v_sal sal_index;
  v_sql_statement VARCHAR2(200);          --定义动态SQL
  v_deptno emp.deptno%TYPE := 10;         --查询10部门
BEGIN
  v_sql_statement := 'update emp set sal = sal * 1.2 where deptno := dno '||
                     ' returning ename, job, sal into :ena, :ej, :es' ;--此时返回多行更新结果
  EXECUTE IMMEDIATE v_sql_statement USING v_deptno RETURNING BULK COLLECT INTO v_ename, v_job, v_sal;
  FOR x IN 1.. v_ename.count LOOP
    dbms_output.put_line('雇员姓名:'||v_ename(x)||',职位:'||v_job(x)||',工资:'||v_sal(x));                     
  END LOOP;
END;

7.3.2、查询时使用BULK COLLECT语句

DECLARE
  TYPE ename_index IS TABLE OF emp.ename%TYPE INDEX BY PLS_INTEGER;
  TYPE job_index IS TABLE OF empl.job%TYPE INDEX BY PLS_INTEGER;
  TYPE sal_index IS TABLE OF emp.sal%TYPE INDEX BY PLS_INTEGER;
  v_ename ename_index;
  v_job job_index;
  v_sal sal_index;
  v_sql_statement VARCHAR2(200);
  v_deptno emp.deptno%TYPE : = 10;
BEGIN
  v_sql_statement := 'select ename, job, sal from emp where deptno := dno';  --返回多条结果
  EXECUTE IMMEDIATE v_sql_statement BULK COLLECT INTO v_ename, v_job, v_sal
  USING v_deptno;
  FOR x IN 1..v_ename.count LOOP
    dbms_output.put_line('雇员姓名:'||v_ename(x)||',职位:'||v_job(x)||',工资:'||v_sal(x));    
  END LOOP;
END;

7.4 、处理游标操作

打开游标变量中使用USING
OPEN 游标名 FOR 动态SQL语句 [USING 绑定变量,绑定变量,…]

7.4.1、在游标中使用动态SQL

DECLARE
  cur_emp SYS_REFCURSOR;    --定义游标变量
  v_emprow emp%ROWTYPE;     --定义emp行类型
  v_deptno emp.deptno%TYPE := 10;   --定义要查询雇员的部门编号
BEGIN
  OPEN cur_emp FOR 'select * from emp where deptno =: dno' USING v_deptno;
  LOOP
    FETCH cur_emp INTO v_emprow;  --取得游标数据
    EXIT WHEN cur_emp%NOTFOUND;   --如果没有数据则退出
    dbms_output.put_line('雇员姓名:'||v_emprow.ename||',雇员职位:'||v_emprow.job);
  END LOOP;
  CLOSE cur_emp;
END;

7.4.2、利用批量处理也可以在fetch语句中利用BULK COLLECT一次性将多个数据保存到集合类型中

FETCH 动态游标 BULK COLLECT INTO 集合变量…;

DECLARE
  cur_emp SYS_REFCURSOR;     --定义游标变量
  TYPE emp_index IS TABLE OF emp%ROWTYPE INDEX BY PLS_INTEGER;   --定义索引表
  v_emprow emp_index;     --定义emp行类型
  v_deptno emp.deptno%TYPE := 10;
BEGIN
  OPEN cur_emp FOR 'select * from emp where deptno =: dno' USING v_deptno;
  FETCH cur_emp BULK COLLECT INTO v_emprow;
  CLOSE cur_emp;
  FOR x IN 1..v_emprow.count LOOP
    dbms_output.put_line('雇员姓名:'||v_emprow.ename||',雇员职位:'||v_emprow.job);
  END LOOP;
END;

7.5、dmbs_sql包简介

为了解决对象依赖关系问题,引入了该包,但现在少使用了,只简要讲解。

查看DBMS_SQL包定义
SELECT * FROM all_source WHERE TYPE = ‘PACKAGE’ AND NAME = ‘DBMS_SQL’;

通过DBMS_SQL包查询数据

DECLARE
  v_sql_statement VARCHAR2(200);
  v_cid NUMBER;   --保存游标ID,以方便关闭
  v_ename emp.ename%TYPE;
  v_job emp.job%TYPE;
  v_sal emp.sal%TYPE;
  v_stat NUMBER;
  v_deptno emp.deptno%TYPE := 10;
BEGIN
  v_cid := DBMS_SQL.open_cursor;    --打开游标
  v_sql_statement := 'select ename, job, sal from emp where deptno =: dno';
  dbms_sql.parse(v_cid, v_sql_statement, dbms_sql.native);
  dbms_sql.define_column(v_cid, 1, v_ename, 10);  --定义OUT模式变量
  dbms_sql.define_column(v_cid, 2, v_job, 9);     --定义OUT模式变量
  dbms_sql.define_column(v_cid, 3, v_sal);        --定义OUT模式变量
  dbms_sql.bind_variable(v_cid, ':dno', v_deptno);    --绑定变量
  v_stat := dbms_sql.execute(v_cid);    --执行游标,返回更新行数
  LOOP
    EXIT WHEN dbms_sql.fetch_rows(v_cid) = 0;
    dbms_sql.column_value(v_cid, 1, v_ename);
    dbms_sql.column_value(v_cid, 2, v_job);
    dbms_sql.column_value(v_cid, 3, v_sal);
    dbms_output.put_line('雇员姓名:'||v_ename||',职位:'||v_job||',薪金:'||v_sal);
  END LOOP;
  dbms_sql.close_cursor(v_cid);          --关闭游标
END;

通过DBMS_SQL包执行修改操作

DECLARE
  v_sql_statement VARCHAR2(200);
  v_cid NUMBER;   --保存游标id,方便关闭
  v_comm emp.comm%TYPE := 500;
  v_empno emp.empno%TYPE := 7369;
  v_stat NUMBER;
BEGIN
  v_cid := dbms_sql.open_cursor;         --打开游标
  v_sql_statement := 'update emp set comm =: ec where deptno =: eno';
  dbms_sql.parese(v_cid, v_sql_statement, dbms_sql.native);
  dbms_sql.bind_variable(v_cid, ':ec', v_comm); --绑定变量
  dbms_sql.bind_variable(v_cid, ':eno', v_empno);     --绑定变量
  v_stat := dbms_sql.execute(v_cid);    --执行游标,返回更新行数
  dbms_output.put_line('更新行数为:'||v_stat);
  dbms_sql.close_cursor(v_cid);         --关闭游标
END;

第八章:面向对象编程

8.1、面向对象编程简介

三大特性:封装、继承、多态

类规范定义格式:
01:CREATE [OR REPLACE] TYPE 类规范名
02:[AUTHID CURRENT_USER | DEFINER]
03:[IS | AS] OBJECT | UNDER 父规范类型名称(
04: 属性名 数据类型,…
05: [MAP | ORDER] MEMBER 函数名,
06: [FINAL | NOTFINAL] MEMBER 函数名,
07: [INSTANTIABLE | NOTINSTANTIABLE] MEMBER 函数名,
08: CONSTRUCTOR MEMBER 子程序名,…
09: OVERRIDING MEMBER 子程序名,…
10: [MEMBER | STATIC] 子程序名称,…
11:)[FINAL | NOTFINAL]
12:[INSTANTIABLE | NOTINSTANTIABLE];
各行解释:
01,用于定义类规范,与包规范功能类似。
02:此类的使用授权
03:使用OBJECT表示定义一个新对象,如果使用UNDER,则表示要定义指定父类对象规范的子类对象规范。
04:定义类中若干个组成属性。
05:定义该函数是否用于对象间的比较。
06:如果函数使用了FINAL定义,则表示子类实例不可以覆盖这个函数,而NOTFINAL表示子类可以覆盖此函数。
07:表示此函数是否可以被实例化对象调用,INSTANTIABLE表示此函数可以被实例化,可以通过对象调用,
而NOTINSTANTIABLE则表示这个函数专门用于子类重载函数使用,实例化对象无法调用此函数。
08:定义构造方法。
09:进行函数覆写。
10:定义函数,其中MEMBER定义的函数表示由实例化对象调用,如果是STATIC定义的函数则由类调用。
11:如果使用了FINAL表示此类不允许有子类,NOTFINAL表示可以有子类。
12:此类对象是否可以被实例化。

除了定义对象规范外,还需要针对对象规范定义实现的对象体:
CREATE [OR REPLACE] TYPE BODY 对象规范名 [IS | AS]
[MAP | ORDER] MEMBER 函数体;
[MEMBER | STATIC] 子程序体,…
END;

定义类规范:

CREATE OR REPLACE TYPE emp_object AS OBJECT(
  --定义对象属性,与emp表对应
  atri_empno NUMBER(4),           --雇员编号
  atri_sal NUMBER(7, 2),          --雇员工资
  atri_deptno NUMBER(2),          --部门编号
  --定义对象操作方法
  --此过程的功能是根据部门编号按照一定的百分比增长部门雇员工资
  MEMBER PROCEDURE change_dept_sal_proc(p_deptno NUMBER, p_percent NUMBER),
  --此函数功能是取得指定雇员的工资
  MEMBER FUNCTION get_sal_fun(p_empno NUMBER) RETURN NUMBER
)NOT FINAL;

定义类体:

CREATE OR REPLACE TYPE BODY emp_object AS
MEMBER PROCEDURE change_dept_sal_proc(p_deptno NUMBER, p_percent NUMBER)
  AS
  BEGIN
    UPDATE emp SET sal = sal * (1 + p_percent) WHERE deptno = p_deptno;
  END;
MEMBER FUNCTION get_sal_fun(p_empno NUMBER)  RETURN NUMBER
  AS
    v_sal emp.sal%TYPE;
    v_comm emp.comm%TYPE;    
  BEGIN
    SELECT sal, NVL(comm, 0) INTO v_sal, v_comm FROM emp WHERE empno = p_empno;
    RETURN v_sal + v_comm;
  END;
END;

在类规范中定义了两个普通的函数,但是此函数并没有具体的函数体,所以要在类体中实现这两个函数,实现之后
就可以通过一个PL/SQL块使用类产生对象,并进行操作。调用方式:
调用类中属性:对象.属性。
调用类中函数:对象.函数()

声明对象并使用类:

DECLARE
  v_emp emp_object;
BEGIN
  --实例化类对象
  v_emp := emp_object(7368, 800.0, 20);
  --修改对象中atri_sal属性内容
  v_emp.atri_sal := 1000;
  --取得修改后工资数额
  dbms_output.put_line('7368雇员修改后工资是:'||v_emp.atri_sal);
  --通过调用类中函数,取得7566雇员工资
  dbms_output.put_line('7566雇员工资是:'||v_emp.get_sal_fun(7566));
  --修改20部门雇员工资,上涨30%
  v_emp.change_dept_sal_proc(20, 0.3);
  --通过调用类中函数,取得7566雇员工资
  dbms_output.put_line('部门工资修改后,7566雇员工资是:'||v_emp.get_sal_fun(7566));
END;

删除类型:
DROP TYPE emp_object;

8.2、操作类中其他结构

8.2.1、 定义函数

在PL/SQL定义的类中,函数定义方式有:
MEMBER型函数:该函数需要通过对象进行定义,使用MEMBER定义的函数可以利用SELF关键字访问类中属性内容。
STATIC函数:该函数独立于类之外,可以直接通过类名进行调用,使用STATIC定义的函数无法访问类中属性。

使用两种不同方式定义函数:

CREATE OR REPLACE TYPE emp_object AS OBJECT(
  --定义对象属性,与emp表对应
  atri_empno NUMBER(4),           --雇员编号  
  --修改当前雇员编号工资,使用类中的empno和sal属性
  MEMBER PROCEDURE change_emp_sal_proc(p_sal NUMBER),
  --取得当前雇员工资
  MEMBER FUNCTION get_emp_fun(p_empno NUMBER) RETURN NUMBER,
  --修改制定部门全体雇员工资
  STATIC PROCEDURE change_dept_sal_proc(p_deptno NUMBER, p_sal NUMBER),
  --取得此部门的工资总和
  STATIC FUNCTION get_dept_sal_sum_fun(p_deptno NUMBER) RETURN NUMBER
)NOT FINAL;

定义类体实现类规范:

CREATE OR REPLACE TYPE BODY emp_object AS
  MEMBER PROCEDURE change_emp_sal_proc(p_sal NUMBER) AS
  BEGIN
    --使用self.atri_empno找到本类中的属性(与java中this类似),即更新当前对象中雇员工资
    UPDATE emp SET sal = p_sal WHERE empno = self.atri_empno;
  END;
  MEMBER FUNCTION get_emp_sal_fun RETURN NUMBER AS
    v_sal emp.sal%TYPE;
    v_comm emp.comm%TYPE;
  BEGIN
    --取得当前对象中指定雇员编号工资
    SELECT sal, NVL(comm, 0) INTO v_sal, v_comm FROM emp WHERE empno = self.atri_empno;
    RETURN v_sal + v_comm;
  END;
  STATIC PROCEDURE change_dept_sal_proc(p_deptno NUMBER, p_sal NUMBER) AS
  BEGIN
    --更新指定部门全部雇员工资
    UPDATE emp SET sal = p_sal WHERE deptno = p_deptno;
  END;
  STATIC FUNCTION get_dept_sal_sum_fun(p_deptno NUMBER) RETURN NUMBER AS
    v_sum NUMBER;
  BEGIN
    --查询指定部门工资总和
    SELECT SUM(sal) INTO v_sum FROM emp WHERE deptno = p_deptno;
    RETURN v_sum;
  END;
END;

编写PL/SQL块实例化类对象:

DECLARE
  v_emp emp_object;
BEGIN
  v_emp := emp_object(7369);   --实例化emp_object类对象
  v_emp.change_emp_sal_proc(3800);  --修改7369工资
  dbms_output.put_line('7369员工工资是:'||v_emp.get_emp_sal_fun());
  dbms_output.put_line('10部门工资总和;'||emp_object.get_dept_sal_fum_fun(10));  --通过类调用
  emp_object.change_dept_sal_proc(10, 7000);       --通过类调用
END;

8.2.2、 构造函数

要求:
构造函数名必须与类名一致
构造函数必须使用CONSTRUCTOR关键字定义
构造函数必须定义返回值,且返回类型必须为SELF AS RESULT
构造函数可以进行重载,重载的构造函数参数的类型及个数不同

定义类规范:

CREATE OR REPLACE TYPE emp_object AS OBJECT(
  atri_empno NUMBER(4),           --雇员编号
  arti_sal NUMBER(7, 2),          --雇员工资
  atri_comm NUMBER(7, 2),         --雇员佣金
  --定义构造函数,只接收雇员编号
  CONSTRUCTOR FUNCTION emp_object(p_empno NUMBER) RETURN SELF AS RESULT,
  --重载构造函数
  CONSTRUCTOR FUNCTION emp_object(p_empno NUMBER, p_comm NUMBER) RETURN AS RESULT  
)NOT FINAL;

定义类体,实现类规范:

CREATE OR REPLACE TYPE BODY emp_object AS
  CONSTRUCTOR FUNCTION emp_object(p_empno NUMBER) RETURN AS RESULT AS
  BEGIN
    self.atri_empno := p_empno;           --保存雇员编号属性
    --查询指定的雇员工资,并将其内容赋给atri-sal属性
    SELECT sal INTO self.atri FROM emp WHERE empno = p_empno;
    RETURN;
  END;
  CONSTRUCTOR FUNCTION emp_object(p_empno NUMBER, p_comm NUMBER) RETURN AS RESULT AS
  BEGIN
    self.atri_empno := p_empno;
    self.atri_comm := p_comm;
    self.atri_sal := 200.0;               --为atri_sal设置默认值
    RETURN;
  END;
END;

使用PL/SQL块测试构造函数:

DECLARE
  v_emp1 emp_object;
  v_emp2 emp_object;
  v_emp3 emp_object;
BEGIN
  v_emp1 := emp_object(7369, 3500);       --自定义构造函数
  v_emp2 := emp_object(7566);             --自定义构造函数
  v_emp3 := emp_object(7839, 0.0);        --默认构造函数
  dbms_output.put_line('7369雇员工资:'||v_emp1.atri_sal);
  dbms_output.put_line('7566雇员工资:'||v_emp2.atri_sal);
  dbms_output.put_line('7839雇员工资:'||v_emp3.atri_sal);
END;

8.2.3 、定义MAP与ORDER函数

当用户声明了多个类对象后,如果要对这些对象信息排序,不能按照number或varchar2这种基本数据类型排序,
必须指定专门比较规则。
MAP函数:使用MAP定义的函数将会按照用户定义的数据组合值区分大小,然后利用ORDER BY 排序
ORDER函数,与MAP函数类似,也是定义了一个排序规则,进行数据排序时会默认调用,同时ORDER函数还可以
比较两个对象大小关系,所以如果要比较多个对象时ORDER函数会被重复调用,性能不如MAP函数。
MAP和ORDER函数只能二选一。

在类规范中定义MAP函数:

CREATE OR REPLACE TYPE emp_object_map AS OBJECT(
  atri_empno NUMBER(4),               --雇员编号
  atri_ename VARCHAR2(10),            --雇员名
  atri_sal NUMBER(7, 2),
  atri_comm NUMBER(7, 2),
  --定义MAP函数,此函数会在进行排序时自动调用
  MAP MEMBER FUNCTION compare RETURN NUMBER
)NOT FINAL;

本类中MAP关键字定义了一个compare()函数,同时此函数返回一个数字,该数字为用户自定义的一个排序规则组合.
定义类体实现MAP函数:

CREATE OR REPLACE TYPE BODY emp_object_map AS
  MAP MEMBER FUNCTION compare RETURN NUMBER AS
  BEGIN
    RETURN self.atri_sal + SELF.atri_comm;
  END;
END;

类体中实现了compare函数,同时返回的是工资+佣金组合,这样使用ORDER BY时,将对返回值大小排序
编写数据库创建脚本
按照emp_object_map的结构创建一张新表,这样就可以使用MAP函数排序了

CREATE TABLE emp_object_map_tab OF emp_object_map;
INSERT INTO emp_object_map_tab(atri_empno, atri_ename, atri_sal, atri_commo) VALUES(...);
--多插入几条数据
--通过查询实现排序
SELECT VALUE(e) ve, e.atri_empno, e.atri_ename, e.atri_sal + e.atri_comm
FROM emp_object_map_tab e
ORDER BY ve;
CREATE TABLE emp_object_map_tab OF emp_object_map;
INSERT INTO emp_object_map_tab(atri_empno, atri_ename, atri_sal, atri_commo) VALUES(...);
--多插入几条数据
--通过查询实现排序
SELECT VALUE(e) ve, e.atri_empno, e.atri_ename, e.atri_sal + e.atri_comm
FROM emp_object_map_tab e
ORDER BY ve;

定义类规范时使用ORDER定义函数:

CREATE OR REPLACE TYPE emp_object_order AS OBJECT(
  atri_empno NUMBER(4),                
  atri_ename VARCHAR2(10),
  atri_sal NUMBER(7,2),
  atri_comm NUMBER(7,2),
  --定义ORDER函数,该函数可以用于两个对象间的比较
  ORDER MEMBER FUNCTION compare(obj emp_object_order) RETURN NUMBER
)NOT FINAL;

定义类体实现类规范,同时实现ORDER类型函数:

CREATE OR REPLACE BODY emp_object_order AS
  ORDER MEMBER FUNCTION compare(obj emp_object_order) RETURN NUMBER AS
  BEGIN
    IF (self.atri_sal + self.atri_comm) > (obj.atri_sal + obj.atri_comm) THEN
      RETURN 1;;
    ELSE IF(self.atri_sal + self.atri_comm) < (obj.atri_sal + obj.atri_comm) THEN
      RETURN -1;
    ELSE
      RETURN 0;
    END IF;
  END;
END;

定义PL/SQL块进行对象排序:

DECLARE
  v_emp1 emp_object_order;
  v_emp2 emp_object_order;
BEGIN
  v_emp1 := emp_object_order(7499, 'ALLEN', 1600, 300);
  v_emp2 := emp_object_order(7521, 'WARD', 1250, 500);
  IF v_emp1 > v_emp2 THEN
    dbms_output.put_line('7499的工资高于7521的工资!');
  ELSE IF v_emp1 < v_emp2 THEN
    dbms_output.put_line('7499的工资低于7521的工资!');
  ELSE
    dbms_output.put_line('7499的工资等于7521的工资!');  
  END IF;
END;

除了在PL/SQL中进行比较之外,也可以利用此类型创建数据表通过ORDER BY 进行排序
根据emp_object_order创建数据表:

CREATE TABLE emp_object_order_tab OF emp_object_order;
INSERT INTO emp_object_order_tab(atri_empno, atri_ename, atri_sal, atri_comm)
VALUES(...);

进行数据查询,同时用ORDER BY 排序:

SELECT VALUE(e) ve, e.atri_empno, e.atri_ename, e.atri_sal, e.atri_comm
FROM emp_object_order_tab e
ORDER BY ve;

8.2.4、对象嵌套关系

利用PL/SQL的面向对象编程除了可以将基本数据类型定义为属性之外,还可以结合对象的引用传递方式,进行
对象类型的嵌套。如在部门雇员关系中,每一个雇员都有一个所在部门的信息,就可以将这样的信息
通过嵌套方式来表示。

定义部门类

CREATE OR REPLACE TYPE dept_object AS OBJECT(
  atri_deptno NUMBER(2),
  atri_dname VARCHAR2(14),
  atri_loc VARCHAR2(13),
  --取得对象信息
  MEMBER FUNCTION tostring RETURN VARCHAR2
)NOT FINAL;

定义雇员类

CREATE OR REPLACE TYPE emp_object AS OBJECT(
  atri_empno NUMBER(4),
  atri_ename VARCHAR(10),
  atri_job VARCHAR2(9),
  atri_hiredate DATE,
  atri_sal NUMBER(7, 2),
  atri_comm NUMBER(7, 2),
  atri_dept dept_object,   --雇员部门
  --取得对象信息
  MEMBER FUNCTION tostring RETURN VARCHAR2
)NOT FINAL;

定义类体实现类规范

CREATE OR REPLACE TYPE BODY dept_object AS
  MEMBER FUNCTION tostring RETURN VARCHAR2 AS
  BEGIN
    RETURN '部门编号:'||self.atri_deptno||',名称:'||self.atri_dname||',位置:'||self.atri_loc;
  END;
END;

定义emp_object类体

CREATE OR REPLACE TYPE BODY emp_object AS
  MEMBER FUNCTION tostring RETURN VARCHAR2 AS
  BEGIN
    RETURN '雇员编号:'||self.atri_empno||',姓名:'||self.atri_ename||',职位:'||...其他信息
  END;
END;

编写PL/SQL块验证关系:

DECLARE
  v_dept dept_object;
  v_emp emp_object;
BEGIN
  --首先定义部门对象,此对象需要通过emp_object类构造方法保存到v_emp对象属性中
  v_dept := dept_object(10, 'ACCOUNTING', 'NEW YORK');
  --定义雇员对象,传递此雇员所属部门对象
  v_emp := emp_object(7839, 'KING', 'PRESIDENT', TO_DATE('1981-11-11', 'YYYY-MM-DD'), 5000, NULL, v_dept);
  --直接输出雇员完整信息
  dbms_outupt.put_line(v_emp.tostring());
  --根据信息找到其对应的部门信息
  dbms_outupt.put_line(v_emp.atri_dept.tostring());
END;

8.2.5、 继承性

定义父类person_object

CREATE OR REPLACE TYPE person_object AS OBJECT(
  atri_pid NUMBER,  --人员编号
  atri_name VARCHAR2(10),  --人员姓名
  atri_sex VARCHAR2(10),  --人员性别
  MEMBER FUNCTION get_person_info_fun RETURN VARCHAR2
)NOT FINAL;

定义person类体

CREATE OR REPLACE TYPE BODY person_object AS
  MEMBER FUNCTION get_person_info_fun RETURN VARCHAR2 AS
  BEGIN
    RETURN '人员编号:'||sefl.atri_pid||',姓名:'||self.atri_name||',性别:'||self.atri_sex;
  END;
END;

定义子类emp_object

CREATE OR REPLACE TYPE emp_object UNDER person_object(
  atri_job VARCHAR2(9),
  atri_sal NUMBER(7, 2),
  atri_comm NUMBER(7, 2),
  MEMBER FUNCTION get_emp_info_fun RETURN VARCHAR2
);

定义emp类体

CREATE OR REPLACE TYPE BODY emp_object AS
  MEMBER FUNCTION get_emp_info_fun RETURN VARCHAR2 AS
  BEGIN
    RETURN '人员编号:'||self.atri_pid||',姓名:'||self.atri_name||',性别:'||self.atri_sex||
           ',职位:'||self.atri_job||',工资:'||self.atri_sal||',佣金:'||self.atri_comm;
  END;
END;

利用PL/SQL块测试

DECLARE
  v_emp emp_object;
BEGIN
  --此处必须明确写出父类与子类全部参数
  v_emp := emp_object(7369, 'SMITH', 'FEMALE', 'CLERK', 800.0, 0.0);
  DBMS_OUTPUT.PUT_LINE('person类的函数:'||v_emp.get_person_info_fun());
  dbms_output.put_line('emp_object类的函数:'||v_emp.get_emp_info_fun());
END;

8.2.6、 函数覆写

实现覆写操作,必须在子类规范定义时明确的使用OVERRIDING关键字来定义某一个函数为覆写的函数.
定义程序,实现函数的覆写

定义person类规范

CREATE OR REPLACE TYPE person_object AS OBJECT(
  atri_pid NUMBER,
  atri_name VARCHAR2(10),
  atri_sex VARCHAR2(10),
  MEMBER FUNCTION tostring RETURN VARCHAR2
)NOT FINAL;

定义person类体

CREATE OR REPLACE TYPE BODY person_object AS
  MEMBER FUNCTION tostring RETURN VARCHAR2 AS
  BEGIN
    RETURN '人员编号:'||self.atri_pid||',姓名:'||self.atri_name||',性别:'||self.atri_sex;
  END;
END;

定义emp类规范,此类为person子类

CREATE OR REPLACE TYPE emp_object UNDER person_object(
  atri_job VARCHAR2(10),
  atri_sal NUMBER(7, 2),
  atri_comm NUMBER(7, 2),
  OVERRIDING MEMBER FUNCTION tostring RETURN VARCHAR2
);

定义emp类体

CREATE OR REPLACE TYPE BODY emp_object AS
  OVERRIDING MEMBER FUNCTION tostring RETURN VARCHAR2 AS
  BEGIN
    RETURN '人员编号:'||self.atri_pid||',姓名:'||self.atri_name||',性别:'||
           self.atri_sex||',职位:'||...其它信息
  END;
END;

编写PL/SQL块测试

DECLARE
  v_emp emp_object;
BEGIN
  --此处必须写出父类和子类全部参数
  v_emp := emp_object(7369, 'SMITH', 'FEMALE', 'CLERK', 800.0, 0.0);
  DBMS_OUTPUT.PUT_LINE(v_emp.tostring());
END;

8.2.7、 对象多态性

多态性特点体现在两方面:
函数的多态性:体现为函数的重载与覆写。
对象的多态性:子类对象可以为父类对象进行实例化。

在原有程序基础上增加新的子类

CREATE OR REPLACE TYPE person_object AS OBJECT(
  atri_pid NUMBER,
  atri_name VARCHAR2(10),
  atri_sex VARCHAR2(10),
  MEMBER FUNCTION tostring RETURN VARCHAR2
)NOT FINAL;

定义person类体

CREATE OR REPLACE TYPE BODY person_object AS
  MEMBER FUNCTION tostring RETURN VARCHAR2 AS
  BEGIN
    RETURN '人员编号:'||self.atri_pid||',姓名:'||self.atri_name||',性别:'||self.atri_sex;
  END;
END;

定义emp类规范,为person子类

CREATE OR REPLACE TYPE emp_object UNDER person_object(
  atri_job VARCHAR2(9),
  atri_sal NUMBER(9, 2),
  atri_comm NUMBER(9, 2),
  --此处函数与父类函数名一样,所以为覆写
  OVERRIDING MEMBER FUNCTION tostring RETURN VARCHAR2
);

定义emp类体

CREATE OR REPLACE TYPE BODY emp_object AS
  OVERRIDING MEMBER FUNCTION tostring RETURN VARCHAR2 AS
  BEGIN
    RETURN '人员编号:'||self.atri_pid||'姓名:'||...其他信息
  END;
END;

定义student类规范

CREATE OR REPLACE TYPE student_object UNDER person_object(
  atri_school VARCHAR2(15),
  atri_score NUMBER(5, 2),
  OVERRIDING MEMBER FUNCTION tostring RETURN VARCHAR2
);

定义student类体

CREATE OR REPLACE TYPE BODY student_object AS
  OVERRIDING MEMBER FUNCTION tostring RETURN VARCHAR2 AS
  BEGIN
    RETURN '人员编号:'||..其他信息||',学校:'||self.atri_school||',成绩:'||self.atri_score;
  END;
END;

通过两个子类为person_object类对象实例化

DECLARE
  v_emp person_object;
  v_student person_object;
BEGIN
  v_emp := emp_object(7369, 'SMITH', 'FEMALE', 'CLERK', 800.0, 0.0);
  v_student := emp_object(7566, 'ALLEN', 'FEMALE', 'MLDN', 99.9);
  dmbs_output.put_line('雇员信息:'||v_emp.tostring());
  dbms_output.put_line('学生信息:'||v_student.tostring());
END;

8.2.8、使用FINAL关键字

使用FINAL定义的类不能被继承

CREATE OR REPLACE TYPE person_object AS OBJECT(
  atri_pid NUMBER
)FINAL;  --不管是否写次语句,默认为FINAL

定义emp类规范,因为person_object无法被继承,所以会出错

CREATE OR REPLACE TYPE emp_object UNDER person_object(
  atri_job VARCHAR2(10)
);

使用FINAL定义的函数不能被子类覆写
定义person类规范

CREATE OR REPLACE TYPE person_object AS OBJECT(
  atri_pid NUMBER,
  FINAL MEMBER FUNCTION tostring RETURN VARCHAR2
)NOT FINAL;

定义emp类规范,但是此时由于person_object类无法继承,所以出现错误

CREATE OR REPLACE TYPE emp_object UNDER person_object(
  atri_job VARCHAR2(9),
  --错误:此处无法覆写tostring()函数
  OVERRIDING MEMBER FUNCTION tostring RETURN VARCHAR2
);

8.2.9、 定义抽象函数

当用户定义完一个类后,默认情况下可以直接利用此类实例化对象进行类中结构的操作。如果现在类中的函数不希望被类对象直接使用,而是通过继承其子类来实现时,就可以在定义函数时使用NOT INSTANTIALBE标记即可,这样的函数就成为抽象函数,同时包含抽象函数的类必须用NOT INSTANTIABLE定义,这样的类为抽象类。

定义抽象类与抽象函数

DROP TYPE emp_object;
--定义person类规范
CREATE OR REPLACE TYPE person_object AS OBJECT(
  atri_pid NUMBER,
  atri_name VARCHAR2(10),
  atri_sex VARCHAR2(10),
  NOT INSTANTIABLE MEMBER FUNCTION tostring RETURN VARCHAR2  --定义抽象方法  
)NOT FINAL NOT INSTANTIABLE;  --此处必须用NOT INSTANTIABLE声明类
--定义emp类规范,此类为person子类
CREATE OR REPLACE TYPE emp_object UNDER person_object(
  atri_job VARCHAR2(9),
  atri_sal NUMBER(7, 2),
  atri_comm NUMBER(7, 2),
  --此处覆写
  OVERRIDING MEMBER FUNCTION tostring RETURN VARCHAR2
);

定义emp类体

CREATE OR REPLACE TYPE BODY emp_object AS
  OVERRIDING MEMBER FUNCTION tostring RETURN VARCHAR2 AS
  BEGIN
    RETURN '人员编号:'||self.atri_pid||...其他信息
  END;
END;

PL/SQL测试

DECLARE
  v_emp person_object;
BEGIN
  --通过子类对象为父类实例化
  v_emp := emp_object(7369, 'SMITH', 'FEMALE', 'CLERK', 800.00, 0.0);
  dbms_output.put_line(v_emp.tostring());
END;

8.3、 对象表

8.3.1、对象表简介

Oracle属于面向对象的数据库,所以在Oracle中也允许用户基于类的结构进行数据表的创建,同时采用类的关系
进行表中数据的维护。

定义要使用的类结构

CREATE OR REPLACE  TYPE dept_object AS OBJECT(
  atri_deptno NUMBER(2),
  atri_dname VARCHAR2(14),
  atri_loc VARCHAR2(14),
  MEMBER FUNCTION tostring RETURN VARCHAR2
)NOT FINAL;

定义person类规范

CREATE OR REPLACE TYPE person_object AS OBJECT(
  atri_pid NUMBER,
  atri_name VARCHAR2(10),
  atri_sex VARCHAR2(10),
  NOT INSTANTIABLE MEMBER FUNCTION tostring RETURN VARCHAR2,--定义抽象方法
  --实现对象排序
  NOT INSTANTIABLE MAP MEMBER FUNCTION compare RETURN NUMBER
)NOT FINAL NOT INSTANTIABLE;  --必须使用NOT INSTANTIABLE声明类

定义emp类规范,此类为person子类

CREATE OR REPLACE TYPE emp_object UNDER person_object(
  atri_job VARCHAR2(10),
  atri_sal NUMBER(7, 2),
  atri_comm NUMBER(7, 2),
  atri_dept dept_object,
  --此函数名称与父类函数名一样,所以此处为覆写
  OVERRIDING MEMBER FUNCTION tostring RETURN VARCHAR2,
  --实现对象排序
  OVERRIDING MAP MEMBER FUNCTION compare RETURN NUMBER
);

定义dept_object类体

CREATE OR REPLACE TYPE BODY dept_object AS
  MEMBER FUNCTION tostring RETURN VARCHAR2 AS
  BEGIN
    RETURN '部门编号:'||self.atri_deptno||',名称:'||self.atri_dname||',位置:'||self.atri_loc;
  END;
END;

定义emp_object类体

CREATE OR REPLACE TYPE BODY emp_object AS
  OVERRIDING MEMBER FUNCTION tostring RETURN VARCHAR2 AS
  BEGIN
    RETURN '人员编号:'||self.atri_pid||',姓名:'||...其他信息
  END;
  OVERRIDING MEMBER FUNCTION compare RETURN NUMBER AS
  BEGIN
    RETURN self.atri_sal + self.atri_comm;
  END;
END;

创建对象表 create table 表名 of 类

CREATE TABLE emp_object_tab OF emp_object;
--查看表结构
DESC emp_object_tab;

如果不创建对象表,也可以将创建的类作为列的类型

CREATE TABLE my_emp_tab(
  empno NUMBER(4),
  ename VARCHAR2(10),
  dept dept_object
);

8.3.2、 维护对象表数据

8.3.2.1、数据增加

增加数据,但不增加部门属性数据

INSERT INTO emp_object_tab(atri_pid, atri_name, atri_sex, atri_job, atri_sal, atri_comm)
VALUES(10, 'xx', 'xx','xx', 3500, 100); 

增加数据,同时使用嵌套类型

insert into  emp_object_tab(atri_pid, atri_name, atri_sex, atri_job, atri_sal, atri_comm, atri_dept)
VALUES(20, 'xx', 'xx', 'xx', 5500, 200, dept_object(10, 'xx', 'xx'));

8.3.2.2、 数据查询

SELECT * FROM emp_object_tab;
8.3.2.2.1、value()函数

在查询中,利用VALUE()函数可以将对象表中数据转化为对象返回,这样就可以利用查询后的对象信息进行排序。

数据排序显示

SELECT VALUE(e) ve, atri_pid, atri_name, atri_sex, atri_job, atri_sal, atri_comm
FROM emp_object_tab e
ORDER BY ve DESC;

查询单挑数据并将数据设置给person_object类对象

DECLARE
  v_emp emp_object;
BEGIN
  --查询指定人员信息,并将此信息转化为对象
  SELECT VALUE(e) INTO v_emp FROM emp_object_tab e WHERE e.atri_pid = 20;
  --输出雇员信息
  dbms_output.put_line(v_emp.tostring());
  --输出雇员所在部门信息
  dbms_output.put_line(v_emp.atri_dept.tostring());
END;

利用游标来接收多条返回结果

DECLARE
  v_emp emp_object;
  CURSOR cur_emp IS SELECT VALUE(e) ve FROM emp_object_tab e;
BEGIN
  FOR v_emprow IN cur_emp LOOP
    v_emp := v_emprow.ve;  --取出一个雇员信息
    dbms_output.put_line(v_emp.tostring());
    IF v_emp.atri_dept IS NOT NULL THEN
      dbms_output.put_line('雇员部门-->'||v_emp.atri_dept.tostring());
    END IF;
  END LOOP;
END;
8.3.2.2.2、REF()函数

利用VALUE()函数是将嵌套的对象信息直接保存在对象表中,但是这种做法很多时候会造成数据的冗余。如一个部门会存在多个雇员,当增加雇员数据时,都需要重复保存部门信息,所以在Oracle中也提供了数据的地址指向

定义person_object新的子类emp_object_ref,该类对dept_object通过REF定义嵌套关系。
定义emp类规范,为person子类

CREATE OR REPLACE TYPE emp_object_ref UNDER person_object(
  atri_job VARCHAR2(9),
  atri_sal NUMBER(7, 2),
  atri_comm NUMBER(7, 2),
  atri_dept REF dept_object,  --雇员部门
  OVERRIDING MEMBER FUNCTION tostring RETURN VARCHAR2,
  OVERRIDING MAP MEMBER FUNCTION compare RETURN NUMBER
);

定义emp_object类体

CREATE OR REPLACE TYPE BODY emp_object_ref AS
  OVERRIDING MEMBER FUNCTION tostring RETURN VARCHAR2 AS
  BEGIN
    RETURN '人员编号:'||...其他信息
  END;
  OVERRIDING MAP MEMBER FUNCTION compare RETURN NUMBER AS
  BEGIN
    RETURN self.atri_sal + self.atri_comm;
  END;
END;
--创建emp_object_ref_tab与dept_object_ref_tab对象表
CREATE TABLE emp_object_ref_tab OF emp_object_ref;
CREATE TABLE dept_object_ref_tab OF dept_object;
--向dept_object_ref_tab表增加数据
INSERT INTO dept_object_ref_tab(atri_deptno, atri_dname, atri_loc)
VALUES(10, 'xx', 'xx');
--多插入几条数据
--查找数据
SELECT * FROM dept_object_ref_tab;
--向emp_object_ref_tab表增加数据
INSERT INTO emp_object_ref_tab(atri_pid, atri_name, atri_sex,atri_job, atri_sal, atri_comm, atri_dept)
VALUES(3010, 'xx', 'xx', 'xx', 4500, 100, (
  SELECT REF(d) 
  FROM dept_object_ref_tab d
  WHERE atri_deptno = 10
));
--再类似插入几条数据
SELECT * FROM emp_object_ref_tab;
--利用DEREF()函数查看REF引用数据信息
SELECT atri_pid, atri_name, atri_sex, atri_job, atri_sal, atri_comm, DEREF(atri_dept) dept
FROM emp_object_ref_tab;

8.3.2.3、数据更新

更新emp_object_tab对象表(此表不使用REF引用)

UPDATE emp_object_tab SET atri_job = 'yy'
  atri_dept = dept_object(30, 'cc', 'canada')
WHERE atri_pid = 10;

SELECT * FROM emp_object_tab WHERE atri_pid = 10;
--更新emp_object_ref_tab对象表,使用REF引用
UPDATE emp_object_ref_tab SET 
       atri_dept = (
         SELECT REF(d)
         FROM dept_object_ref_tab d
         WHERE atri_deptno = 30)
WHERE atri_pid = 3020;
--查询
SELECT atri_pid, atri_name, atri_sex, atri_job, atri_sal, atri_comm, DEREF(atri_dept) dept
FROM emp_object_ref_tab
WHERE atri_pid = 3020;

除了使用更新对象列之外,用户也可以取出相应的对象信息设置数据更新条件

UPDATE emp_object_ref_tab SET atri_name = 'xxx', atri_sal = 600
WHERE atri_dept = (SELECT REF(d)
                   FROM dept_object_ref_tab d
                   WHERE atri_deptno = 10);
--查询
SELECT atri_pid, atri_name, atri_sex, atri_job, atri_sal, atri_comm, DEREF(atri_dept) dept
FROM emp_object_ref_tab

8.3.2.4、删除数据

DELETE FROM emp_object_ref_tab
WHERE atri_dept = (SELECT REF(d)
                   FROM dept_object_ref_tab d
                   WHERE atri_deptno = 10);
                   
--查询emp_object_ref_tab数据
SELECT atri_pid, atri_name, atri_sex, atri_job, atri_sal, atri_comm DEREF(atri_dept) dept
FROM emp_object_ref_tab;

8.4、对象视图

要使用对象视图,需要先定义一个与指定视图查询结构类似的类:
定义类,此类结构与emp表结构一致

CREATE OR REPLACE TYPE emp_table_object AS OBJECT(
  atri_empno NUMBER(4),
  atri_ename VARCHAR2(10),
  atri_job VARCHAR2(9),
  atri_mgr NUMBER(4),
  atri_hiredate DATE,
  atri_sal NUMBER(7, 2),
  atri_comm NUMBER(7, 2),
  MEMBER FUNCTION tostring RETURN VARCHAR2
)NOT FINAL;
CREATE OR REPLACE TYPE BODY emp_table_object AS
  MEMBER FUNCTION tostring RETURN VARCHAR2 AS
  BEGIN
    RETURN '雇员编号:'||...其它信息
  END;
END;

这样就可以将emp表中数据通过对象视图转变为emp_table_object类对象
创建视图

CREATE OR REPLACE VIEW 视图名 OF 类
  WITH OBJECT IDENTIFIER(主键对象)
AS 子查询;

创建对象视图

CREATE OR REPLACE VIEW v_myview OF emp_table_object
  WITH OBJECT IDENTIFIER(atri_empno)
AS
  SELECT empno, ename, job, mgr, hiredate, sal, comm, deptno FROM emp;

编写PL/SQL访问视图

DECLARE
  v_emp emp_table_object;
BEGIN
  SELECT VALUE(ev) INTO v_emp FROM v_myview ev WHERE atri_empno = 7839;
  dbms_output.put_line(v_emp.tostring());
END;

猜你喜欢

转载自blog.csdn.net/qq_38696286/article/details/122995924