PL(Procedural Language)/SQL程序设计语言

PL/SQL基础

SQL是第四代(非结构化的)程序设计语言,PL/SQL是适用于ORACLE数据库编程的结构化的程序设计语言。

PL/SQL程序块

DECLARE            --可选
--variables,cursors,user-defined exceptions
BEGIN              --强制的(必须的)
  --一个或多个SQL语句
  --一个或多个PL/SQL语句
  EXCEPTION      --可选
    --当错误发生是要进行的处理
END;             --强制的(必须的)
/	
  1. 匿名程序块,不能存储在数据库中,执行时被编译并执行;
  2. 每一个SQL语句或PL/SQL语句都是以 ; 结束;
  3. 使用 / 结束PL/SQL程序块;
  4. 过程和函数是被命名的PL/SQL程序块,预编译在数据库中。

PL/SQL第一个程序

DECLARE            --可选
  --定义变量
  var VARCHAR2(100) := 'hello world!';
BEGIN              --强制的(必须的)
  --输出定义好的字符串变量
  dbms_output.put_line(var);
END;             --强制的(必须的)
/	

输出结果

绑定变量(宿主变量)

  1. 绑定变量是在使用(或调用)PL/SQL的环境中创建的,而不是在程序的声明段中定义的;
  2. 在程序块执行之后,绑定变量依然存在并可以访问;
  3. 如果创建的绑定变量是NUMBER类型,那么不能指定精度(位数)和规模(指数值);如果创建的绑定变量是VARCHAR2类型,则可以指定字符串的长度,长度的单位是字节;
  4. 用SQL* Plus的 VARIABLE 命令来定义绑定变量,在PL/SQL程序块(与该SQL*Plus同一窗口)中可以通过 : 来引用绑定变量;
VARIABLE a number;
BEGIN
  SELECT 1 into :a FROM dual;
END;
/

替代变量

  1. 在SQL* Plus环境中,可以使用SQL* Plus的替代变量将运行时的值传给PL/SQL程序块;
  2. 可以在执行的时候动态输入值或者通过SQL*Plus的define命令预先定义好替代变量;
  3. 通过前导的 & 符号或者 && 符号引用替代变量;
  4. & 每次引用都需要输入,&& 输入一次后不再提示输入;
define a = 1;
SELECT * FROM dual WHERE 1 = '&a';

变量的声明

标识符 [CONSTANT] 数据类型 [NOT NULL] [:= | DEFAULT 表达式]
  1. 标识符:所声明的变量名;
  2. CONSTANT:限制所声明变量的值不能更改(声明常量,必须初始化);
  3. 数据类型:可以是一个标量类型、组合类型或LOB类型;
  4. NOT NULL:限制变量不能为空,必须初始化;
  5. 初始化变量:既可以使用PL/SQL的赋值操作符 :=,也可以使用关键字 DEFAULT 来初始化变量;
  6. 表达式:PL/SQL表达式,可以是文字表达式、另外的变量或带有操作符和函数的表达式。
  7. 注意:PL/SQL中赋值操作符是 :=,而不是 =

字符串分隔符

  1. 直接使用单引号 ‘’ ,字符串中若有单引号 ,每个 需要重复一次进行转义;程序的可读性比较差,不建议使用这种方式;
SELECT 'What''s your name?' FROM dual;

What's your name?

  1. 使用 q’ 操作符定义定界符;
--通过q'定义||为定界符
SELECT q'|What's your name?|' FROM dual;
--通过q'定义//为定界符
SELECT q'/What's your name?/' FROM dual;
--通过q'定义{}为定界符
SELECT q'{What's your name?}' FROM dual;
--通过q'定义[]为定界符
SELECT q'[What's your name?]' FROM dual;

What's your name?

数据类型

数据类型 说明
VARCHAR2(size) size最大值为32767,VARCHAR2会把空字符串当做空值处理,在表中,size的最大值为4000
CHAR(size) size默认为值1,最大值为32767
NUMBER(p,s) p为数的精度(位数),s为规模(指数值),p最小为1,最大为38,s最小值为-84,最大值为124
BINARY_INTEGER 基本整型,范围:-2147483647~2147483647
PLS_INTEGER 基本带符号整型,范围:-2147483647~2147483647,相比NUMBER存储更少,效率高,在PLS_INTEGER的范围尽可能使用PLS_INTEGER
BINARY_FLOAT 浮点数,5个字节
BINARY_DOUBLE 浮点数,9个字节
BOOLEAN 逻辑类型,有3个可能值,TRUE、FALSE和NULL
DATE 日期和时间型,年月日时分秒
TIMESATMP 日期和时间型,最多可精确到小数点后9位的秒数,默认精确到小数点后6位
TIMESATMP WITH TIME ZONE 扩展的TIMESATMP,包括了时区
CLOB 存储单字节的大数据对象
BLOB 存储大的二进制对象
BFILE 外部数据类型,存储在数据库之外的,可能是操作系统文件
NCLOB 存储NCHAR类型的单字节或定长多字节的Unicode大数据对象

%TYPE属性

  1. 使用 %type 属性按照之前已经声明过的变量或者数据库中表的列来声明一个变量;
  2. 当存储在一个变量中的值来自于数据库中的表时,最好使用 %type 声明这个变量;
  3. 使用 %type 后,当数据库中表的数据类型改变时,无需修改程序。
--建表
CREATE TABLE example (
  example_id NUMBER,
  example_name VARCHAR2(200)
);

--插入数据
INSERT INTO example VALUES (1,'样例1');
INSERT INTO example VALUES (2,'样例2');
COMMIT;

--测试%TYPE
DECLARE
  --用%TYPE属性声明p_example_id 是与example表中example_id相同的数据类型
  p_example_id example.example_id%TYPE;
  --用%TYPE属性声明p_example_name 是与example表中example_name相同的数据类型
  p_example_name example.example_name%TYPE;
BEGIN
  --将从example表中读取的一行数据的两列分别写进p_example_id和p_example_name变量
  SELECT * 
  INTO p_example_id,p_example_name
  FROM example
  WHERE ROWNUM = 1;
  --在oracle输出区输出p_example_id的值
  dbms_output.put_line(p_example_id);
  --在oracle输出区输出p_example_name的值
  dbms_output.put_line(p_example_name);
END;
/

%TYPE

常用定界符

操作符 含义
+ 加法运算符
- 减法运算符/否定操作符
* 乘法运算符
** (指数)取幂运算符,只能在PL/SQL中使用,在SQL语句中使用会报错
/ 除法运算符
= 相等操作符
; 语句结束符
@ 远程访问符
:= 赋值操作符
!= 不等运算符
<> 不等运算符
/* 开始注释定界符
*/ 结束注释定界符
单行注释符

序列

--创建序列
CREATE SEQUENCE exp_seq  
  START WITH 100         --从100开始,初始值为100
  INCREMENT BY 1         --每次递增1
  MAXVALUE 380380        --可以递增到的最大值设置为380380
  NOCACHE                --不使用缓存
  NOCYCLE;               --到达最大值后不再从头开始

--使用序列
--nextval 每次递增
--currval 指向序列的当前值
SELECT exp_seq.nextval,exp_seq.currval FROM dual;

使用序列
使用序列

MERGE替代UPDATE

MERGE INTO table_name table_alias
USING (table|view|sub_query) alias
ON (join condition)
WHEN MATCHED THEN
  UPDATE SET
    col1 = col1_val,
    col2 = col2_val
WHEN NOT MATCHED THEN
  INSERT (column_list)
  VALUES (column_values);
  1. 在涉及到关联子查询更新数据的时候,尽量使用MERGE来替代UPDATE,MERGE的效率比UPDATE更高;
  2. 当表中存在该行数据的时候,进行更新操作,不存在这行数据的时候,则插入数据,这样的操作直接MERGE就能实现,就不需要PL/SQL复杂的逻辑判断;
  3. INTO 子句——说明正在修改或插入的目标表;
  4. USING 子句——标识要修改或插入的数据源,既可以是表,也可以是视图,甚至可以是子查询;
  5. ON 子句——定义MERGE 语句是进行修改操作还是插入操作的条件;
  6. WHEN MATCHED THEN——定义当条件满足时所做的操作;
  7. WHEN NOT MATCHED THEN——定义当条件不满足时所做的操作;
  8. ON 条件中的列是不能更新的;
  9. MERGE语句与其他DML语句一样,不会自动结束事务,需要手动COMMIT或者ROLLBACK;
  10. MERGE和USING后的表名必须给别名;
  11. UPDATE后是可以加WHERE条件的。
    例:
CREATE TABLE example (
  example_id NUMBER,
  example_name VARCHAR2(200)
);

INSERT INTO example VALUES (1,'样例1');
INSERT INTO example VALUES (2,'样例2');
COMMIT;

MERGE前

MERGE INTO example a
USING (
  SELECT 1 example_id, 'MERGE后的样例1' example_name FROM dual
  UNION 
  SELECT 2 example_id, 'MERGE后的样例2' example_name FROM dual
  UNION 
  SELECT 3 example_id, 'MERGE后的样例3' example_name FROM dual
) b
ON (a.example_id = b.example_id)
WHEN MATCHED THEN
  UPDATE SET
    a.example_name = b.example_name
  --限制当 example_id = 1 的时候才做更新
  WHERE 
    a.example_id = 1
WHEN NOT MATCHED THEN
  INSERT (a.example_id,a.example_name)
  VALUES (b.example_id,b.example_name);
COMMIT;

MERGE后

IF语句

IF 条件1 THEN
  --如果满足条件1,则执行该语句块中的语句
  statements;
[
--ELSIF 可以有0个或多个
ELSIF 条件2 THEN
  --如果不满足条件1但满足条件2,则执行该语句块中的语句
  statements;]
[
--ELSE可以有0个或1个
ELSE 
  --如果条件1和条件2都不满足,则执行该语句块中的语句
  statements;]
END IF;

DECLARE
  score NUMBER;
BEGIN

  score := 100;
  
  IF score >= 90 AND score <= 100 THEN
    dbms_output.put_line('Your score is ' || to_char(score) || ', you are very good!');
  ELSIF score >= 70 AND score < 90 THEN
    dbms_output.put_line('Your score is ' || to_char(score) || ', you are good!');
  ELSIF score >= 60 AND score < 70 THEN
    dbms_output.put_line('Your score is ' || to_char(score) || ', you are terrible!');
  ELSE 
    dbms_output.put_line('Your score is ' || to_char(score) || ', you are terrible very much!');
  END IF;
  
  score := 80;
  
  IF score >= 90 AND score <= 100 THEN
    dbms_output.put_line('Your score is ' || to_char(score) || ', you are very good!');
  ELSIF score >= 70 AND score < 90 THEN
    dbms_output.put_line('Your score is ' || to_char(score) || ', you are good!');
  ELSIF score >= 60 AND score < 70 THEN
    dbms_output.put_line('Your score is ' || to_char(score) || ', you are terrible!');
  ELSE 
    dbms_output.put_line('Your score is ' || to_char(score) || ', you are terrible very much!');
  END IF;
  
  score := 65;
  
  IF score >= 90 AND score <= 100 THEN
    dbms_output.put_line('Your score is ' || to_char(score) || ', you are very good!');
  ELSIF score >= 70 AND score < 90 THEN
    dbms_output.put_line('Your score is ' || to_char(score) || ', you are good!');
  ELSIF score >= 60 AND score < 70 THEN
    dbms_output.put_line('Your score is ' || to_char(score) || ', you are terrible!');
  ELSE 
    dbms_output.put_line('Your score is ' || to_char(score) || ', you are terrible very much!');
  END IF;
  
  score := 50;
  
  IF score >= 90 AND score <= 100 THEN
    dbms_output.put_line('Your score is ' || to_char(score) || ', you are very good!');
  ELSIF score >= 70 AND score < 90 THEN
    dbms_output.put_line('Your score is ' || to_char(score) || ', you are good!');
  ELSIF score >= 60 AND score < 70 THEN
    dbms_output.put_line('Your score is ' || to_char(score) || ', you are terrible!');
  ELSE 
    dbms_output.put_line('Your score is ' || to_char(score) || ', you are terrible very much!');
  END IF;
END;
/

输出结果

CASE表达式与CASE语句

--CASE表达式既可以用在SQL语句中,也可以用在PL/SQL中
--第一种CASE表达式
CASE selector
  WHEN 表达式1 THEN 结果1
  WHEN 表达式2 THEN 结果2
  ...
  WHEN 表达式n THEN 结果n
  [ELSE 结果n+1]
END;    --必须以END结尾
--当1=2时输出1,否则输出0
--显然结果是输出0
SELECT CASE 1 WHEN 2 THEN 1 ELSE 0 END FROM dual;

--第二种CASE表达式
CASE 
  WHEN 搜索条件1 THEN 结果1
  WHEN 搜索条件2 THEN 结果2
  ...
  WHEN 搜索条件n THEN 结果n
  [ELSE 结果n+1]
END;    --必须以END结尾
--当1=2时输出1,否则输出0
--显然结果是输出0
SELECT CASE WHEN 1 = 2 THEN 1 ELSE 0 END FROM dual;

--CASE语句只能用在PL/SQL中
--第一种CASE语句
CASE selector
  WHEN 表达式1 THEN 
    statements1;
  WHEN 表达式2 THEN 
    statements2;
  ...
  WHEN 表达式n THEN 
    statementsn;
  [ELSE 
    statementsn+1]
END CASE;    --必须以END CASE;结尾

--第二种CASE语句
CASE 
  WHEN 搜索条件1 THEN 
    statements1;
  WHEN 搜索条件2 THEN 
    statements2;
  ...
  WHEN 搜索条件n THEN 
    statementsn;
  [ELSE 
    statementsn+1]
END CASE;    --必须以END CASE;结尾

BEGIN
  --第一种
  CASE 1 
    WHEN 2 THEN
      dbms_output.put_line('1 = 2');
    ELSE
      dbms_output.put_line('1 <> 2');
  END CASE;
  
  --第二种
  CASE  
    WHEN 1 = 2 THEN
      dbms_output.put_line('1 = 2');
    ELSE
      dbms_output.put_line('1 <> 2');
  END CASE;
    
END;
/

LOOP循环

/*
  循环输出1到10
*/
DECLARE
  --定义一个数,初始值为1
  num NUMBER := 1;
BEGIN
  --循环开始
  LOOP
    --输出num
    dbms_output.put_line(num);
    --num+1
    num := num + 1;
    --循环退出条件,当num第一次出现比10大的时候退出
    EXIT WHEN num > 10;
  END LOOP;

END;
/

LOOP循环

WHILE循环

/*
  循环输出1到10
*/
DECLARE
  --定义一个数,初始值为1
  num NUMBER := 1;
BEGIN
   
  --先判断退出循环条件,退出循环条件为 num <= 10
  WHILE num <= 10 
  LOOP
  
    --输出num
    dbms_output.put_line(num);  
    --num++
    num := num + 1;
  
  END LOOP;

END;
/

WHILE循环

FOR循环

/*
  循环输出1到10
*/
BEGIN
 
  FOR i IN 1..10 LOOP
    dbms_output.put_line(i);  
  END LOOP;

END;
/

FOR循环

CONTINUE

--CONTINUE退出当前循环进入下一次循环
/*
  循环输出1到4,6到10,不输出5
*/
--可以这么写
BEGIN
 
  FOR i IN 1..10 LOOP
    CONTINUE WHEN i = 5;
    dbms_output.put_line(i);  
  END LOOP;

END;
/
--也可以这么写
BEGIN
 
  FOR i IN 1..10 LOOP
    IF i = 5 THEN
      CONTINUE;
    END IF;
    dbms_output.put_line(i);  
  END LOOP;

END;
/

CONTINUE

常用的组合数据类型

PL/SQL记录

  1. 创建一个记录类型;
TYPE 数据类型名 IS RECORD (
  字段声明[,字段声明]...
);
  1. 声明一个PL/SQL记录类型的变量;
标识符 数据类型名;
  1. 例:
DECLARE
  --创建一个记录类型
  TYPE exp_record_type IS RECORD (
    example_id example.example_id%TYPE,
    example_name example.example_name%TYPE
  );
  --声明记录类型的变量
  exp_record exp_record_type;
BEGIN

  --将从example表中获取到的一行数据放进已经声明的记录类型的变量exp_record中
  SELECT 
    example_id,
    example_name
  INTO exp_record
  FROM example
  WHERE ROWNUM = 1;
  
  --输出
  dbms_output.put_line(exp_record.example_id);
  dbms_output.put_line(exp_record.example_name);

END;
/

%ROWTYPE

利用%ROWTYPE属性声明一个能够存储一个表或视图中一整行数据的记录(变量)。

DECLARE
  --声明记录类型的变量
  exp_record example%ROWTYPE;
BEGIN

  --将从example表中获取到的一行数据放进已经声明的记录类型的变量exp_record中
  SELECT 
    example_id,
    example_name
  INTO exp_record
  FROM example
  WHERE ROWNUM = 1;
  
  --输出
  dbms_output.put_line(exp_record.example_id);
  dbms_output.put_line(exp_record.example_name);

END;
/

INDEX BY 表 (PL/SQL表)

  1. INDEX BY 表 又称 PL/SQL表,是一组关联的键值对,类似于其他程序设计语言的数组;
  2. 声明一个INDEX BY 表的数据类型;
TYPE 数据类型名 IS TABLE OF
  {列数据类型 | 变量%TYPE
  | 表名.列名%TYPE} [NOT NULL]
  | 表名%ROWTYPE
  [INDEX BY PLS_INTEGER | BINARY_INTEGER | VARCHAR2(<size>)];
  1. 声明一个INDEX BY 表的数据类型的变量;
标识符 数据类型名;
  1. 例:
DECLARE 
  --声明记录类型的变量
  exp_record example%ROWTYPE;
  --声明数组类型
  TYPE exp_table_type IS TABLE OF example.example_name%TYPE
    INDEX BY PLS_INTEGER;
  --声明数组类型的变量
  exp_table exp_table_type;
BEGIN
  
  --将从example表中获取到的一行数据放进已经声明的记录类型的变量exp_record中
  SELECT 
    example_id,
    example_name
  INTO exp_record
  FROM example
  WHERE ROWNUM = 1;
  
  --对数组进行赋值
  exp_table(exp_record.example_id) := exp_record.example_name;
  
  --输出刚赋值的数组
  dbms_output.put_line('exp_table(' || exp_record.example_id || ') = ''' || exp_table(exp_record.example_id) || '''');

END;
/
  1. INDEX BY 表的内置函数
方法 描述
EXISTS(n) 如果第n个元素在PL/SQL表(数组)中存在,返回TRUE
COUNT 返回一个PL/SQL表当前所包含元素的个数
FIRST 返回PL/SQL表中第一个(最小的)下标数字;如果PL/SQL表是空的,返回NULL
LAST 返回PL/SQL表中最后一个(最大的)下标数字;如果PL/SQL表是空的,返回NULL
PRIOR(n) 返回PL/SQL表中当前元素的前n个元素的表值
NEXT(n) 返回PL/SQL表中当前元素的前n个元素的表值
DELETE 删除PL/SQL表的全部元素
DELETE(n) 删除PL/SQL表的第n个元素
DELETE(m,n) 删除PL/SQL表的m到n范围内的全部元素

INDEX BY 记录表

  1. 如果用想用数组来操作一个表中的多列,那么需要基于这个表的每一列都定义一个INDEX BY 表,这样非常不方便;
  2. 声明一个INDEX BY 记录表就是定义一个存放一个表中整行数据(每一列)的INDEX BY表变量;
  3. 例:
DECLARE 
  v_count NUMBER := 3;
  --声明数组类型
  TYPE exp_table_type IS TABLE OF example%ROWTYPE
    INDEX BY PLS_INTEGER;
  --声明数组类型的变量
  exp_table exp_table_type;
BEGIN

  --循环对数组进行赋值
  FOR i IN 1 .. v_count 
  LOOP
    SELECT * 
    INTO exp_table(i)
    FROM example
    WHERE example_id = i;
  END LOOP;
  
  --遍历数组
  FOR i IN 1 .. exp_table.count 
  LOOP
    dbms_output.put_line(exp_table(i).example_id || '|' || exp_table(i).example_name);
  END LOOP;

END;
/

SQL游标(CURSOR)

  1. 声明游标;
--cursor_name :自定义的游标名称
--select_statement :为一个没有INTO子句的	SELECT 语句
CURSOR cursor_name IS
  select_statement;
  1. 打开游标;
OPEN cursor_name;
  1. 循环获取游标的每一行数据;
LOOP
  FETCH cursor_name into var1,var2;
  EXIT WHEN cursor_name%NOTFOUND OR cursor_name%NOTFOUND IS NULL;
  ...
END LOOP;
  1. 关闭游标;
CLOSE cursor_name;
  1. 优化打开游标,先判断游标是否已经被打开,如果没有被打开则打开游标;
IF NOT cursor_name%ISOPEN THEN
  OPEN cursor_name;
END IF;
  1. 如果一个表中的列很多,需要定义与列数相同的变量,实际上可以定义基于游标的记录类型;
DECLARE
  --声明游标
  CURSOR exp_cursor IS 
    SELECT * FROM example;
  --声明基于游标的记录变量
  exp_cursor_record exp_cursor%ROWTYPE;
BEGIN
  
  --如果游标不是打开状态,则打开游标
  IF NOT exp_cursor%ISOPEN THEN
    OPEN exp_cursor;
  END IF;
  
  --循环获取游标的数据到基于游标的记录变量
  LOOP
    FETCH exp_cursor INTO exp_cursor_record;
    EXIT WHEN exp_cursor%NOTFOUND OR exp_cursor%NOTFOUND IS NULL;
    
    dbms_output.put_line(exp_cursor_record.example_id || '|' || exp_cursor_record.example_name);
      
  END LOOP;

  CLOSE exp_cursor;
  
END;
/

常用的4个CURSOR属性

属性 描述
%ISOPEN 如果游标是打开的,返回TRUE
%FOUND 如果最近(刚刚执行过)的SQL语句返回至少一行数据,返回TRUE
%NOTFOUND 如果最近(刚刚执行过)的SQL语句没有返回任何数据,返回TRUE
%ROWCOUNT 返回最近(刚刚执行过)的SQL语句所影响的数据行数(为一个整数)

CURSOR的FOR循环

--record_name :隐含声明的记录名
--cursor_name :前面声明的游标
--select_statement :前面未声明游标时,使用查询语句隐式的声明游标
FOR record_name IN cursor_name | (select_statement) 
LOOP         --隐含打开游标并提取数据行
  语句1;
  语句2;
  ...
END LOOP;    --隐含关闭游标
BEGIN
  
  FOR i IN (
    SELECT * FROM example
  )
  LOOP
    dbms_output.put_line(i.example_id || '|' || i.example_name);
  END LOOP;
  
END;
/

带参数的CURSOR

  1. 参数在DECLARE中CURSOR声明之前声明,直接在CURSOR使用参数(参数名不能与列名相同);
DECLARE
  p_example_id NUMBER := 1;
  --声明游标
  CURSOR exp_cursor IS 
    SELECT * 
    FROM example
    WHERE example_id = p_example_id;
  --声明基于游标的记录变量
  exp_cursor_record exp_cursor%ROWTYPE;
BEGIN
  
  --如果游标不是打开状态,则打开游标
  IF NOT exp_cursor%ISOPEN THEN
    OPEN exp_cursor;
  END IF;
  
  --循环获取游标的数据到基于游标的记录变量
  LOOP
    FETCH exp_cursor INTO exp_cursor_record;
    EXIT WHEN exp_cursor%NOTFOUND OR exp_cursor%NOTFOUND IS NULL;
    
    dbms_output.put_line(exp_cursor_record.example_id || '|' || exp_cursor_record.example_name);
      
  END LOOP;
 
  CLOSE exp_cursor;
  
END;
/
DECLARE
  p_example_id NUMBER := 1;
BEGIN
  
  FOR i IN (
    SELECT * FROM example
    WHERE example_id = p_example_id
  )
  LOOP
    dbms_output.put_line(i.example_id || '|' || i.example_name);
  END LOOP;
  
END;
/
  1. 参数在CURSOR的声明中声明;
DECLARE
  
  --声明游标
  CURSOR exp_cursor (
    p_example_id NUMBER,
    p_example_name VARCHAR2
  ) 
  IS 
    SELECT * 
    FROM example
    WHERE example_id = p_example_id;
  --声明基于游标的记录变量
  exp_cursor_record exp_cursor%ROWTYPE;
BEGIN
  
  --如果游标不是打开状态,则打开游标
  IF NOT exp_cursor%ISOPEN THEN
    OPEN exp_cursor (1,'MERGE后的样例1');
  END IF;
  
  --循环获取游标的数据到基于游标的记录变量
  LOOP
    FETCH exp_cursor INTO exp_cursor_record;
    EXIT WHEN exp_cursor%NOTFOUND OR exp_cursor%NOTFOUND IS NULL;
    
    dbms_output.put_line(exp_cursor_record.example_id || '|' || exp_cursor_record.example_name);
      
  END LOOP;

  CLOSE exp_cursor;
  
END;
/

FOR UPDATE

当游标在处理数据的时候,FOR UPDATE 会给查询表加行级锁(没有其他事物占用时才能加上锁,否则会等待其他事物释放锁),加锁后,其他事物不能对查询表的该行进行更改,直到该事物COMMIT或者ROLLBACK后才会释放锁。

SELECT ...
FROM   ...
FOR UPDATE [OF column_reference] [NOWAIT | WAIT n];

测试:

  1. 先开启一个会话,执行下面的语句,此时,该会话没有提交;
UPDATE example SET example_name = '111' WHERE example_id = 1;
--COMMIT;
  1. 另外开启一个会话,执行下面的语句;
DECLARE
  p_example_id NUMBER := 1;
BEGIN
  
  FOR i IN (
    SELECT * FROM example
    WHERE example_id = p_example_id
    FOR UPDATE WAIT 10
  )
  LOOP
    dbms_output.put_line(i.example_id || '|' || i.example_name);
  END LOOP;
  
END;
/

FOR UPDATE

  1. 此时COMMIT或者ROLLBACK第一个UPDATE的操作,再执行第二个PL/SQL块时,便可以执行了,因为COMMIT和ROLLBACK都会把锁释放。

WHERE CURRENT OF

  1. WHERE CURRENT OF 需要与 FOR UPDATE 配合使用;
  2. 在UPDATE或DELETE中使用 WHERE CURRENT OF,在CURSOR声明中说明 FOR UPDATE ;
  3. WHERE CURRENT OF 引用显示CURSOR的当前行;
DECLARE
  --声明游标,并对example的example_id=1的数据行加锁
  CURSOR exp_cursor IS 
    SELECT * FROM example
    WHERE example_id = 1
    FOR UPDATE OF example_name;
BEGIN
  
  --循环更新example表被锁定的行数据
  FOR i IN exp_cursor
  LOOP
    UPDATE example 
    SET
      example_name = '对游标中的表数据进行更新'
    WHERE CURRENT OF exp_cursor;
  END LOOP;
  
  --若正常执行到这里,则提交
  COMMIT;
  
  --若出现异常,则回滚
  EXCEPTION WHEN OTHERS THEN
    ROLLBACK;
  
END;
/
SELECT * FROM example;

WHERE CURRENT OF

动态CURSOR

  1. 声明的时候不关联SQL查询,在打开游标的时候动态关联SQL查询;
  2. 声明游标类型;
--cursor_type 为自定义的类型名
TYPE cursor_type IS REF CURSOR;
  1. 声明游标类型的变量;
--v_cursor 为自定义的变量名
--cursor_type 为上面声明的引用游标类型
v_cursor cursor_type;
  1. 自定义拼接动态SQL;
--拼接SQL赋值给v_sql变量
v_sql VARCHAR2(32767);
v_sql := 'select_statement';
  1. 打开游标,并关联动态SQL;
OPEN v_cursor FOR v_sql;
  1. 循环获取数据行;
  2. 关闭游标;
CLOSE v_cursor;

例:

CREATE TABLE example_202011 (
  example_id NUMBER,
  example_name VARCHAR2(200)
);

CREATE TABLE example_202010 (
  example_id NUMBER,
  example_name VARCHAR2(200)
);

INSERT INTO example_202011 VALUES (20201101,'样例20201101');
INSERT INTO example_202011 VALUES (20201102,'样例20201102');
INSERT INTO example_202010 VALUES (20201001,'样例20201001');
INSERT INTO example_202010 VALUES (20201002,'样例20201002');
COMMIT;

DECLARE
  --定义动态游标类型
  TYPE cursor_type IS REF CURSOR;
  --声明动态游标类型的变量
  v_cursor cursor_type;
  --当月月份变量   &thismonth表示替代变量
  thismonth VARCHAR2(10) := '&thismonth';
  --动态sql
  v_sql VARCHAR2(32767);
  --example_id变量用来接收游标中的example_id
  example_id NUMBER;
  --example_name变量用来接收游标中的example_name
  example_name VARCHAR2(200);
BEGIN
  
  --动态从指定月份的备份表中查询数据
  v_sql := q'[SELECT * FROM example_]' || thismonth;
  
  --打开动态游标并关联动态sql
  OPEN v_cursor FOR v_sql;
  
  --循环输出游标中的所有数据
  LOOP
    FETCH v_cursor INTO example_id,example_name;
    EXIT WHEN v_cursor%NOTFOUND OR v_cursor%NOTFOUND IS NULL;
    
    dbms_output.put_line(example_id || '|' || example_name);
  
  END LOOP;
  
  --关闭游标
  CLOSE v_cursor;

END;
/

当输入202010时,结果为:
202010
当输入202011时,结果为:
202011

异常处理

EXCEPTION
  WHEN 异常1 [或 异常2 ...] THEN
    语句1;
    语句2;
    ...
  [WHEN 异常3 [或 异常4 ...] THEN
    语句1;
    语句2;
    ...]
  [WHEN OTHERS THEN
    语句1;
    语句2;
    ...]

预定义的ORACLE服务器错误

预定义异常名 ORACLE服务器错误代码 描述
ACCESS_INTO_NULL ORA_06530 试图为一个未初始化的对象的属性赋值
CASE_NOT_FOUND ORA_06592 在选择的CASE语句的WHEN子句中没有选择条件,并且没有ELSE语句
COLLECTION_IS_NULL ORA_06531 试图对一个未初始化嵌套表或VARRAY使用除了EXISTS以外的集合方法
CURSOR_ALREADY_OPEN ORA_06511 试图打开一个已打开的CURSOR
DUP_VAL_ON_INDEX ORA_00001 试图插入一个重复值
INVALID_CURSOR ORA_01001 发生了非法的CURSOR操作
INVALID_NUMBER ORA_01722 将字符串转换成数字失败
LOGIN_DENIED ORA_01017 以一个无效的用户名或密码登录ORACLE服务器
NO_DATA_FOUND ORA_01403 单行查询没有返回任何数据
NOT_LOGIN_ON ORA_01012 在没有连接ORACLE服务器的情况下,PL/SQL程序发出了一个数据库调用
PROGRAM_ERROR ORA_06501 PL/SQL有一个内部问题
ROWTYPE_MISMATCH ORA_06504 在一个赋值语句中涉及的宿主变量与PL/SQL CURSOR的数据类型不匹配
STORAGE_ERROR ORA_06500 PL/SQL耗光了内存或内存崩溃
SUBSCRIPT_BEYOND_COUNT ORA_06533 通过下标引用一个嵌套表或VARRAR元素时,下标数值大于集合中元素的数目
SUBSCRIPT_OUTSIDE_LIMIT ORA_06532 通过下标引用一个嵌套表或VARRAR元素时,下标数值超出合法范围(比如-1)
SYS_INVALID_ROWID ORA_01410 将一个字符串转换成通用(universal)ROWID失败,因为该字符串不能表示为一个有效的ROWID
TIMEOUT_ON_RESOURCE ORA_00051 当ORACLE服务器等待资源期间发生超时
TOO_MANY_ROWS ORA_01422 单行查询返回多行数据
VALUE_ERROR ORA_06502 发生算数、转换、截断或大小限制的错误
ZERO_DIVIDE ORA_01476 试图除以0
/*
  如果不进行异常处理,会直接报错,极其不友好
  进行异常处理后,发生异常时执行异常段中的代码,不会报错
*/
DECLARE
  exp_record example%ROWTYPE;
BEGIN

  --这里有异常, example表中不止一行数据
  SELECT * 
  INTO exp_record
  FROM example;
  
  --异常处理
  EXCEPTION 
    --当碰到单行查询返回多行数据的异常时,执行以下输出
    WHEN TOO_MANY_ROWS THEN
      dbms_output.put_line('单行查询返回多行数据');

END;
/

非预定义的ORACLE服务器错误

  1. 声明异常;
异常名 EXCEPTION;
  1. 将声明的异常与ORACLE服务器的错误号码关联起来;
PRAGMA EXCEPTION_INIT(已经声明的异常名,标准ORACLE错误号码);
/*
  父表与子表通过外键关联,如果直接删除父表中在子表中存在的记录,则会报错,
  因为必须先删除子表的记录才能删除附表的记录
*/
CREATE TABLE example_parent(
  exp_id NUMBER PRIMARY KEY,
  exp_name VARCHAR2(200)
);

CREATE TABLE example_sub(
  exp_id NUMBER REFERENCES example_parent(exp_id),
  exp_name VARCHAR2(200)
);

INSERT INTO example_parent VALUES (1,'样例1');
INSERT INTO example_parent VALUES (2,'样例2');
INSERT INTO example_sub VALUES (1,'样例1');
INSERT INTO example_sub VALUES (2,'样例2');
COMMIT;

--会报错
DELETE FROM example_parent WHERE exp_id = 1;

-- -02292是上述说明的错误的标准ORACLE错误号码
--不会报错,删除也不会执行,会输出错误信息
DECLARE
  e_exp_deleting EXCEPTION;
  PRAGMA EXCEPTION_INIT (e_exp_deleting,-02292);
BEGIN

  DELETE FROM example_parent WHERE exp_id = 1;
  
  COMMIT;
  
  EXCEPTION 
    WHEN e_exp_deleting THEN
      ROLLBACK;
      dbms_output.put_line('删除失败,请先删除子表的记录');

END;
/

SQLCODE和SQLERRM

  1. SQLCODE:为错误代码返回一个数值;
  2. SQLERRM:返回字符串数据,它包含了与错误号相关的错误信息;
  3. SQLCODE的取值和每种值的具体含义:
SQLCODE的值 描述
0 没有遇到异常
1 用户定义的异常
+100 NO_DATA_FOUND异常
负数 其他的ORACLE服务器错误号码
DECLARE
  e_exp_deleting EXCEPTION;
  PRAGMA EXCEPTION_INIT (e_exp_deleting,-02292);
BEGIN

  DELETE FROM example_parent WHERE exp_id = 1;
  
  COMMIT;
  
  EXCEPTION 
    WHEN e_exp_deleting THEN
      ROLLBACK;
      dbms_output.put_line('删除失败,请先删除子表的记录');
      dbms_output.put_line(SQLCODE);
      dbms_output.put_line(SQLERRM);

END;
/

SQLCODE和SQLERRM

用户定义的异常

  1. 在一个PL/SQL程序块的声明段中声明一个用户定义的异常;
  2. 使用RAISE语句显式地跑出这个异常;
  3. 在EXCEPTION段处理这个异常;
/*
  example_parent 表中不存在 exp_id = 3 的数据行,更新时肯定会报错
  我们自定义一个这样的异常,然后捕获
*/
DECLARE
  --声明异常
  e_exp_updating EXCEPTION;
BEGIN

  UPDATE example_parent 
  SET 
    exp_name = '样例3'
  WHERE exp_id = 3;
  
  --抛出异常
  IF SQL%NOTFOUND THEN
    RAISE e_exp_updating;
  END IF;
  
  COMMIT;
  
  --捕获异常
  EXCEPTION 
    WHEN e_exp_updating THEN
      ROLLBACK;
      dbms_output.put_line('该样例不存在!无法更新');
      dbms_output.put_line(SQLCODE);
      dbms_output.put_line(SQLERRM);

END;
/
  1. 异常处理一定是在BEGIN … END;块中的。

RAISE_APPLICATION_ERROR过程

raise_application_error (error_number,message[,TRUE | FALSE]); 
  1. 使用PL/SQL提供的RAISE_APPLICATION_ERROR过程与一种与预定义异常的显示格式一样的方式返回一个非标准的错误代码和错误信息(用户定义的错误代码和错误信息);
  2. error_number:是一个用户说明的异常号码,范围只能是-20000~-20999;
  3. message:用户定义的异常信息,是一个字符串,最大长度是2048个字节;
  4. TRUE | FALSE:是一个可选的布尔参数,如果是TRUE,这个错误被放在之前错误层之上,如果是FALSE,默认为FALSE,这个错误取代之前所有错误;
BEGIN

  UPDATE example_parent 
  SET 
    exp_name = '样例3'
  WHERE exp_id = 3;
  
  --也可以自定义异常,在捕获异常的时候调用 raise_application_error 过程
  --抛出异常
  IF SQL%NOTFOUND THEN
    raise_application_error(-20200,'该样例不存在!无法更新');
  END IF;
  
  COMMIT;

END;
/

RAISE_APPLICATION_ERROR

模块化程序设计

存储过程

CREATE [OR REPLACE] PROCEDURE 过程名 [(
  参数1 [方式] 数据类型1 [DEFAULT 默认值 | := 默认值],
  参数2 [方式] 数据类型2 [DEFAULT 默认值 | := 默认值],
  ...
)]
IS | AS
  [本地声明的变量;...]
BEGIN
  --执行的操作;
END [过程名];
  1. OR REPLACE 只如果过程已经存在,它将被删除,并被由语句创建的新版本所替代。但是REPLACE并不取消任何与该过程相关的权限;
  2. 参数1或参数2表示参数的名字;
  3. 方式包括 IN(默认)、OUT 或 IN OUT;
  4. 数据类型1或数据类型2表示参数的数据类型,但是 没有精度
IN OUT IN OUT
默认方式(mode) 必须说明 必须说明
将值传递给子程序 返回给调用程序 将值传递给子程序;返回给调用程序
形式参数如同一个常量一样 初始化的变量 初始化的变量
实参可以是一个文字、表达式、常量或初始化的变量 必须是一个变量 必须是一个变量
可以赋予一个默认值 不能赋予默认值 不能赋予默认值
  1. 删除存储过程;
DROP PROCEDURE 过程名;
  1. 执行存储过程;
--CALL是PL/SQL的关键字
CALL 过程名([实参1,实参2,...]);

--EXEC和EXECUTE是SQL*Plus的执行命令,不是PL/SQL的关键字
EXEC 过程名([实参1,实参2,...])
EXECUTE 过程名([实参1,实参2,...]);
  1. 在过程中调用过程,直接使用过程名加参数,不需要EXECUTE或者CALL;
  2. 最好能在存储过程中进行异常处理;
--不带参数的存储过程
--创建一个存储过程删除example表中的所有记录
CREATE OR REPLACE PROCEDURE delete_example
AS
BEGIN
  DELETE FROM example;
  COMMIT;
  
  EXCEPTION WHEN OTHERS THEN
    ROLLBACK;
END;
/
--执行delete_example
CALL delete_example();
--表被清空
SELECT * FROM example;

--带输入参数的存储过程
--创建一个存储过程更新example表中的指定列
CREATE OR REPLACE PROCEDURE update_example(
  example_id example.example_id%TYPE,
  example_name example.example_name%TYPE
)
AS
  --声明变量,因为参数与列名相同,避免冲突
  p_example_id example.example_id%TYPE := example_id;
  p_example_name example.example_name%TYPE := example_name;
BEGIN
  
  --做更新操作
  UPDATE example 
  SET
    example_name = p_example_name
  WHERE example_id = p_example_id;
  --正常执行到这里,提交
  COMMIT;
  
  --更新中出现异常,则回滚
  EXCEPTION WHEN OTHERS THEN
    
    ROLLBACK;

END;
/
--执行update_example
CALL update_example(1,'更改后的样例1');
--example表中example_id为1的行的example_name 被更改为了 '更改后的样例1'
SELECT * FROM example;

--带输出参数的存储过程
--创建一个存储过程更新example表中的指定列,并告诉调用环境是否更新成功
CREATE OR REPLACE PROCEDURE update_example(
  example_id example.example_id%TYPE,
  example_name example.example_name%TYPE,
  is_success OUT NUMBER
)
AS
  p_example_id example.example_id%TYPE := example_id;
  p_example_name example.example_name%TYPE := example_name;
BEGIN
  
  UPDATE example 
  SET
    example_name = p_example_name
  WHERE example_id = p_example_id;
  COMMIT;
  is_success := 1;
  
  EXCEPTION WHEN OTHERS THEN
    ROLLBACK;
    is_success := 0;

END;
/
--调试存储过程
DECLARE
  is_success NUMBER;
BEGIN
  update_example(1,'更改后的样例1',is_success);
  dbms_output.put_line(is_success);
END;
/
--最后做了更改,并且在输出区输出了 1
SELECT * FROM example;

--授权给TOOL用户update_example的执行和调用权限
GRANT EXECUTE,DEBUG ON update_example TO TOOL;

函数

CREATE [OR REPLACE] FUNCTION 函数名 [(
  参数1 [模式1] 数据类型1,
  ...
)]
RETURN 数据类型 
IS|AS
[
  本地变量声明;
  ...
]
BEGIN
  --执行的操作;
  RETURN  表达式;
END [函数名];
  1. REPLACE选项表示如果这个函数存在,那么这个函数将被删除,并由这个语句创建的新版本函数取代;
  2. RETURN子句的数据类型一定不能包括数据的大小;
  3. PL/SQL程序块以本地变量声明之后的BEGIN关键字开始,并以END关键字结束,在END后面可以包括该函数名,也可以不包括;
  4. 在函数中至少必须包含一个RETURN语句;
  5. 在存储函数的PL/SQL块中不能引用宿主或绑定变量;
/*
  --最简单的函数
  输入什么,就输出什么
*/
CREATE OR REPLACE FUNCTION print(
  str IN VARCHAR2
) RETURN VARCHAR2
AS 
BEGIN
  RETURN str;
END;
/

--在SQL中使用函数
SELECT example_id,print(example_id) FROM example;

--在PL/SQL中使用函数
BEGIN
  dbms_output.put_line(print('我是print函数返回的值!'));
END;
/

--授权TOOL用户函数的使用权限
GRANT EXECUTE,DEBUG ON print TO TOOL;

--删除函数
DROP FUNCTION print;
过程(PROCEDURE) 函数(FUNCTION)
作为一个PL/SQL语句来执行 作为一个表达式来调用
在头中不包含RETURN子句 在头中必须包含一个RETURN子句
可以使用多个输出参数传递值 必须返回一个单一的值
可以包含一个无值的RETURN语句 必须包含至少一个RETURN语句

PL/SQL软件包

CREATE [OR REPLACE] PACKAGE package_name IS|AS
  public type abd variable declarations
  subprogram specifications
END [package_name];
  1. 创建软件包的说明(包头);
  2. 如果软件包的说明已经存储,OR REPLACE 选项删除并重建该软件包的说明;
  3. 如果需要,在声明中使用常量或公式初始化变量,否则默认初始化为NULL;
  4. package_name 为软件包的名字,在一个用户(模式)中必须唯一(不能与同一用户中的其他对象名重名),END关键字后的软件包名字是可选的;
  5. public type abd variable declarations为声明的公共变量、常量、cursors、异常、用户定义的数据类型和子类型;
  6. subprogram specifications为公共过程或函数的声明;
CREATE [OR REPLACE] PACKAGE BODY package_name IS|AS
  private type and variable declarations
  subprogram bodies
[BEGIN initialization statements]
END [package_name];
  1. 在创建包头后,开始创建包体;
  2. package_name为软件包的名字,该名字必须与软件包说明部分名字相同,在END关键字后的软件包名字是可选的;
  3. private type and variable declarations为声明的私有变量、常量、cursors、异常、用户定义的数据类型和子类型;
  4. subprogram bodies说明所有私有和共有过程或函数的完整实现(即PL/SQL程序代码);
  5. [BEGIN initialization statements] 是一个可选的初始化代码程序块,该块在软件包第一次引用时执行;
--创建包头
CREATE OR REPLACE PACKAGE pkg_example AS
  --声明常量
  exp_id CONSTANT NUMBER := 1;
  
  --声明存储过程delete_example
  PROCEDURE delete_example;
  
  --声明函数print
  FUNCTION print(
    str IN VARCHAR2
  ) RETURN VARCHAR2;
  
END pkg_example;
/

--创建包体
CREATE OR REPLACE PACKAGE BODY pkg_example AS
  
  --声明存储过程delete_example
  PROCEDURE delete_example
  AS
  BEGIN
    DELETE FROM example;
    COMMIT;
    
    EXCEPTION WHEN OTHERS THEN
      ROLLBACK;
  END;
  
  --声明函数print
  FUNCTION print(
    str IN VARCHAR2
  ) RETURN VARCHAR2
  AS
  BEGIN
     RETURN str;
  END;
  
END pkg_example;
/

--执行包中的存储过程
CALL pkg_example.delete_example();

--调用包中的函数
SELECT example_id,pkg_example.print(example_id) FROM example;
--输出包中的常量
BEGIN
  dbms_output.put_line(pkg_example.exp_id);
END;
/

--授权TOOL用户包的执行和调试权限
GRANT EXECUTE,DEBUG ON pkg_example TO TOOL;

--删除包
DROP PACKAGE pkg_example;

批量绑定

--FORALL 批量绑定
FORALL INDEX IN 下限 .. 上限
[SAVE EXCEPTIONS]
  SQL语句;

--BULK COLLECT INTO 批量绑定
... BULK COLLECT INTO
  collection_name[,collection_name ...]
  1. 批量绑定减少了执行时PL/SQL引擎和SQL引擎之间的来回切换,可以提高执行效率;
  2. 使用SAVE EXCEPTIONS关键字后造成失败的操作将被存储在一个名为%BULK_EXCEPTIONS的CURSOR属性中;
  3. 利用 BULK COLLECT INTO 可以快速地获取一个数据行的集合而不再需要使用CURSOR机制了;
  4. returning子句将DML语句的结果直接之装入变量或者如果使用了批量绑定后可以批量装入数组。
--FORALL
DECLARE
  TYPE exp_forall_type IS TABLE OF example.name%TYPE INDEX BY PLS_INTEGER;
  exp_forall exp_forall_type;
BEGIN
  exp_forall(1) := '潘金莲';
  exp_forall(2) := '杨贵妃';
  exp_forall(3) := '武则天';
  exp_forall(4) := '花木兰';
  exp_forall(5) := '苏妲己';
  
  --批量绑定 无需循环
  FORALL i IN exp_forall.first .. exp_forall.last
    INSERT INTO example VALUES (exp_forall(i));
  
  FOR i IN exp_forall.first .. exp_forall.last 
  LOOP
    dbms_output.put_line(
      'Inserted ' || SQL%BULK_ROWCOUNT(i) || ' row(s) on iteration ' || i
    );
  END LOOP;
  
  COMMIT;
  
  EXCEPTION WHEN OTHERS THEN
    ROLLBACK;
END;
/

--BULK COLLECT INTO
DECLARE
  TYPE exp_bulk_type IS TABLE OF example.name%TYPE INDEX BY PLS_INTEGER;
  exp_bulk exp_bulk_type;
BEGIN

  --将example与数组exp_bulk批量绑定
  SELECT *
  BULK COLLECT INTO exp_bulk 
  FROM example;
  
  --循环遍历数组
  FOR i IN exp_bulk.first .. exp_bulk.last
  LOOP
    dbms_output.put_line(exp_bulk(i));
  END LOOP;
  
  EXCEPTION WHEN OTHERS THEN
    NULL;

END;
/
--带有returning的批量绑定
CREATE TABLE example (
  exp_id NUMBER,
  exp_value NUMBER
);

INSERT INTO example VALUES (1,1000);
INSERT INTO example VALUES (2,2000);
INSERT INTO example VALUES (3,3000);
INSERT INTO example VALUES (4,4000);
COMMIT;

DECLARE
  TYPE exp_returning_type IS TABLE OF example.exp_value%TYPE INDEX BY PLS_INTEGER;
  exp_returning exp_returning_type;
  exp_returning_new exp_returning_type;
BEGIN

  exp_returning(1) := 1;
  exp_returning(2) := 2;
  exp_returning(3) := 3;
  exp_returning(4) := 4;
  
  FORALL i IN exp_returning.first .. exp_returning.last
    UPDATE example 
    SET exp_value = exp_value * 1.1
    WHERE exp_id = exp_returning(i)
    RETURNING exp_value BULK COLLECT INTO exp_returning_new;
  
  FOR i IN exp_returning_new.first .. exp_returning_new.last
  LOOP
    dbms_output.put_line(exp_returning(i) || ' ' || exp_returning_new(i));
  END LOOP;
  
  COMMIT;
  
  EXCEPTION WHEN OTHERS THEN
    ROLLBACK;  

END;
/

子程序的重载

在软件包中,存储过程和函数是可以重载的。
PL/SQL编译器解析重载子程序调用的内部操作过程: 编译器试着找到匹配子程序调用的一个声明,编译器首先在当前域(程序块)中进行搜索,如果需要接下来搜索包含该程序块的域(即包含该程序块的程序块)。如果编译器找到一个或多个与调用子程序名字相匹配的子程序声明,编译器就停止搜索。如果在同一级的域中(同一个程序块中)有多个同名的子程序,那么编译器需要在实参和形参之间精确地匹配它们的数量、顺序和数据类型。

CREATE OR REPLACE PACKAGE pkg_example AS

  --声明存储过程delete_example
  PROCEDURE delete_example;
  
  PROCEDURE delete_example(
    exp_id IN NUMBER
  );
  
END pkg_example;
/

CREATE OR REPLACE PACKAGE BODY pkg_example AS
  
  --声明存储过程delete_example
  PROCEDURE delete_example
  AS
  BEGIN
    DELETE FROM example;
    COMMIT;
    
    EXCEPTION WHEN OTHERS THEN
      ROLLBACK;
  END;
  
  PROCEDURE delete_example(
    exp_id IN NUMBER
  )
  AS
  BEGIN
    DELETE FROM example
    WHERE example_id = exp_id;
    COMMIT;
    
    EXCEPTION WHEN OTHERS THEN
      ROLLBACK;
  END;
  
END pkg_example;
/

--分别调用2个重载的存储过程
BEGIN
  pkg_example.delete_example();
  pkg_example.delete_example(1);
END;
/

DML触发器

CREATE [OR REPLACE] TRIGGER trigger_name
  timing
  event1 [OR event2 OR event3]
ON object_name [
[REFERENCING OLD AS OLD | NEW AS NEW]
FOR EACH ROW
[WHEN (condition)]
]
trigger_body
其中:
  timing = BEFORE | AFTER | INSTEAD OF
  event = INSERT | DELETE | UPDATE | UPDATE OF column_list
  1. trigger_name : 触发器名,唯一标识触发器,在同一个模式中的触发器名字必须唯一;
  2. timing:指定触发器在什么时候触发(与触发事件无关),其值为BEFORE、AFTER和INSTEAD OF;
  3. event:标识引发触发器的DML操作,其值为INSERT、UPDATE[OF column]和DELETE;
  4. object_name:指定与触发器相关的表或视图;
  5. 对于行触发器,可能需要说明:
    一个REFERENCING子句以选择引用当前行的旧值和新值的相关的名字 (默认值是OLD和NEW);
    FOR EACH ROW 指定这个触发器是行触发器;
    一个使用括号中条件谓词(表达式)的WHEN子句,该子句要测试每一行以决定是否要执行触发器体(的PL/SQL程序代码);
  6. BEFORE:在触发有个表上的DML事件之前执行触发器体;
  7. AFTER:在触发有个表上的DML事件之后执行触发器体;
  8. INSTEAD OF:代替触发的语句来执行触发体(原DML语句不执行,只执行触发器体),主要用于对视图的修改;
  9. 相同对象上的多个同类型的触发器的触发次序是随机的,可以将多个过程写进同一个触发器中指定执行顺序;
  10. 触发顺序:BEFORE语句触发器、BEFORE行触发器、AFTER行触发器、AFTER语句触发器;
CREATE TABLE example (
  example_id NUMBER,
  example_name VARCHAR2(200)
);

INSERT INTO example VALUES (1,'样例1');
INSERT INTO example VALUES (2,'样例2');
COMMIT;

--创建语句级触发器
/*
  在插入数据之前判断今天是否是1号,不为1号则报错不允许插入
*/
CREATE OR REPLACE TRIGGER secure_exp
BEFORE INSERT
ON example
BEGIN
  IF to_char(SYSDATE,'dd') <> '01' THEN
    raise_application_error(-20200,'今天不是1号,不允许插入数据!');
  END IF;
END;
/

--创建带WHEN子句的行级触发器
/*
  当插入或更新或删除example_id为1到10的数据时报错,不允许插入、更新或删除
*/
CREATE OR REPLACE TRIGGER no_opt_exp 
BEFORE INSERT OR UPDATE OR DELETE
ON example
FOR EACH ROW
WHEN ((new.example_id >= 1 AND new.example_id <= 10) OR (old.example_id >= 1 AND old.example_id <= 10))
BEGIN
  IF inserting THEN 
    raise_application_error(-20331,'不允许插入编号1到10的数');
  ELSIF updating ('example_id') THEN
    raise_application_error(-20332,'不允许更新编号1到10的数');
  ELSIF deleting THEN
    raise_application_error(-20333,'不允许删除编号1到10的数');
  END IF;
END;
/
--如果今天不是1号,直接报错 “今天不是1号,不允许插入数据!”
--如果今天是1号,则报错 “不允许插入编号1到10的数”
INSERT INTO example VALUES (2,'样例2');

--报错 “不允许更新编号1到10的数”
UPDATE example SET example_id = 1;

--报错 “不允许删除编号1到10的数”
DELETE FROM example WHERE example_id = 1;

--如果今天是1号,则成功插入
INSERT INTO example VALUES (11,'样例11');
COMMIT;

--删除触发器
DROP TRIGGER no_opt_exp;
DROP TRIGGER secure_exp; 

--INSTEAD OF 触发器执行触发器体的内容而不执行原来的DML语句,这里不再举例
语句级触发器 行级触发器
是创建触发器时的默认类型 创建触发器时使用 FOR EACH ROW子句
对于触发的事件只触发一次 对受触发事件影响的每行触发一次
没有受影响的行时也要触发一次 触发事件未影响任何数据行就不触发
数据操作 旧(OLD)值 新(NEW)值
INSERT 空值(NULL) 插入的值
UPDATE 修改之前的值 修改之后的值
DELETE 删除之前的值 空值(NULL)
触发器 过程
使用CREATE TRIGGER定义 使用CREATE PROCEDURE定义
源代码包含在user_triggers数据字典中 源代码包含在user_source数据字典中
由DML语句隐含调用 显式调用
不允许使用COMMIT、SAVEPOINT和ROLLBACK(声明自治事务后可以使用) 允许使用COMMIT、SAVEPOINT和ROLLBACK

复合触发器

--基于表的复合触发器
CREATE OR REPLACE TRIGGER 模式.触发器名 
FOR dml事件子句 ON 模式.表名
COMPOUND TRIGGERS
  --initial section (初始段)
  --declaration (声明)
  --subprograms (子程序)
  
  --optional section (可选段)
  BEFORE STATEMENT IS
    ...;
  
  --optional section (可选段)
  AFTER STATEMENT IS
    ...;
  
  --optional section (可选段)
  BEFORE EACH ROW IS
    ...;
  
  --optional section (可选段)
  AFTER EACH ROW IS
    ...;
    
END 模式.触发器名;

--基于视图的符复合触发器
CREATE OR REPLACE TRIGGER 模式.触发器名 
FOR dml事件子句 ON 模式.表名
COMPOUND TRIGGERS
  --initial section (初始段)
  --declaration (声明)
  --subprograms (子程序)
    
  --optional section (独有的)
  INSTEAD OF EACH ROW IS
    ...;
    
END 模式.触发器名;
时机 复合触发器程序段
在触发语句之前执行 BEFORE statement
在触发语句之后执行 AFTER statement
在触发语句影响的第一行之前执行 BEFORE EACH ROW
在触发语句影响的第一行之后执行 AFTER EACH ROW
CREATE TABLE example (
  exp_id NUMBER,
  exp_value NUMBER
);

INSERT INTO example VALUES (1,10);
INSERT INTO example VALUES (1,20);
INSERT INTO example VALUES (1,30);
INSERT INTO example VALUES (2,15);
INSERT INTO example VALUES (2,20);
INSERT INTO example VALUES (3,40);
INSERT INTO example VALUES (3,1);
COMMIT;

/*
  --解决变异表错误
  当插入数据的时候,判断插入的example.exp_value的值是否在同一exp_id的最大值和最小值之间,
  如果在最大值和最小值之间,则允许插入,否则报错“插入的值不在取值范围内!”
*/
CREATE OR REPLACE TRIGGER check_value
FOR INSERT
ON example
WHEN (new.exp_id >= 1 AND new.exp_id <= 3)
compound TRIGGER

  --声明数组类型
  TYPE exp_value_type IS TABLE OF example.exp_value%TYPE INDEX BY PLS_INTEGER;
  TYPE exp_id_type IS TABLE OF example.exp_id%TYPE INDEX BY PLS_INTEGER;
  
  --声明数组 用来批量绑定汇总后的example
  min_value exp_value_type;
  max_value exp_value_type;
  p_exp_id exp_id_type;
  
  --将example_id作为下标与数组绑定
  p_min_value exp_value_type;
  p_max_value exp_value_type;
  
  --在触发之前先汇总example并放进数组中
  BEFORE STATEMENT IS
  BEGIN
    
    --批量绑定
    SELECT 
      exp_id,
      MIN(exp_value),
      MAX(exp_value)
    BULK COLLECT INTO p_exp_id,min_value,max_value
    FROM example
    GROUP BY exp_id;
    
    FOR i IN 1 .. p_exp_id.count()
    LOOP
      p_min_value(p_exp_id(i)) := min_value(i);
      p_max_value(p_exp_id(i)) := max_value(i);
    END LOOP;
    
  END BEFORE STATEMENT;
  
  --在触发语句影响一行之前
  BEFORE EACH ROW IS
  BEGIN
    IF :new.exp_value < p_min_value(:new.exp_id) OR :new.exp_value > p_max_value(:new.exp_id) THEN
      raise_application_error(-20345,'插入的exp_value值不在取值范围内!');
    END IF;
  END BEFORE EACH ROW;
  

END check_value;
/

--插入失败,报错“插入的值不在取值范围内!”
INSERT INTO example VALUES (1,1);

--成功插入
INSERT INTO example VALUES (1,15);
COMMIT;

DDL触发器

CREATE [OR REPLACE] TRIGGER trigger_name
BEFORE | AFTER
DDL | ALTER | DROP | CREATE
ON DATABASE | SCHEMA
trigger_body;
DDL事件 何时触发
CREATE 使用CREATE命令创建任何数据库对象时
ALTER 使用ALTER命令更改任何数据库对象时
DROP 使用DROP命令删除任何数据库对象时
/*
  当当前用户执行DDL语句的时候,像 example 表中插入一条1的记录
*/
CREATE TABLE example (ID NUMBER);

--可以这么写
CREATE OR REPLACE TRIGGER tri_exp_ddl
AFTER CREATE OR DROP OR ALTER
ON SCHEMA 
BEGIN
  INSERT INTO example VALUES(1);
END;
/
--也可以这么写
CREATE OR REPLACE TRIGGER tri_exp_ddl
AFTER DDL
ON SCHEMA 
BEGIN
  INSERT INTO example VALUES(1);
END;
/
--当on后面指定database时,需要sys的权限

基于系统事件的触发器

CREATE [OR REPLACE] TRIGGER trigger_name
BEFORE | AFTER
database_event1 [OR database_event2 OR ...]
ON DATABASE | SCHEMA
trigger_body;
数据库事件 何时触发
AFTER SERVERERROR 一个ORACLE错误被跑出时
AFTER LOGON 一个用户登录数据库时
BEFORE LOGOFF 一个用户退出数据库时
AFTER STARTUP 开启数据库时
BEFORE SHUTDOWN 正常关闭数据库时
/*
  当当前用户登录的时候,想example表中插入一条1的记录
*/
CREATE OR REPLACE TRIGGER tri_exp_sysevent
AFTER logon 
ON SCHEMA
BEGIN
  INSERT INTO example VALUES(1);
END;
/

触发器中的CALL语句

CREATE OR REPLACE PROCEDURE hint_exp
AS
BEGIN
  dbms_output.put_line('正在删除example表中的数据!');
END;
/
CREATE OR REPLACE TRIGGER after_exp_delete
AFTER DELETE
ON example
FOR EACH ROW
CALL hint_exp     --此处没有分号
/

程序的定义者权限和调用者权限

  1. 定义者权限:所有程序都是以创建这一程序的用户的权限执行的;
  2. 调用者权限:一个程序以调用该程序的用户的权限执行的,一个用户以调用者权限运行一个过程时,该用户需要具有这个过程所引用的对象的相应权限;
  3. PL/SQL默认是AUTHID DEFINED,即以程序的定义者权限执行该子程序。出于安全的考虑,大多数ORACLE数据库自带的PL/SQL软件包,如 DBMS_LOB、DBMS_ROWID等都是调用之权限的软件包。
--将一个程序定义成调用者权限来执行
CREATE FUNCTION function_name RETURN type_name AUTHID CURRENT_USER IS ...
CREATE PROCEDURE procedure_name AUTHID CURRENT_USER IS ...
CREATE PACKAGE package_name AUTHID CURRENT_USER IS ...
CREATE TYPE type_name AUTHID CURRENT_USER IS OBJECT ...

自治事务

  1. 一个自治事务是一个由另外一个主事务开启的独立事务;
  2. 自治事务是使用 PRAGMA AUTONOMOUS_TRANSACTION 关键字定义的;
  3. 不能使用关键字 PRAGMA 将一个软件包的子程序标为自治的,只能将独立的子程序标记为自治的;
  4. 自治事务可以被递归调用;
  5. 如果主事务回滚,那么自治事务并不回滚。
CREATE OR REPLACE PROCEDURE exp_insert
IS 
  PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
  INSERT INTO example VALUES (1);
  COMMIT;
END;
/

--结果1被插入进去,2没有没插入进去
BEGIN
  exp_insert();
  INSERT INTO example VALUES (2);
  ROLLBACK;
END;
/

PL/SQL源代码加密

常用的数据字典表

--显示用户下所有表和视图
SELECT * FROM cat;
--显示用户下的表
SELECT * FROM user_tables;
--显示用户下的视图
SELECT * FROM user_views;
--显示所有表
SELECT * FROM all_tables;
--显示多有视图
SELECT * FROM all_views;
--显示用户下的约束
SELECT * FROM user_constraints;
SELECT * FROM user_cons_columns;
--显示所有约束
SELECT * FROM all_constraints;
SELECT * FROM all_cons_columns;
--显示用户下的索引
SELECT * FROM user_indexes;
SELECT * FROM user_ind_columns;
--显示用户下的对象
SELECT * FROM user_objects;
--显示所有对象
SELECT * FROM all_objects;
--显示用户下的源代码
SELECT * FROM user_source;
--显示所有源代码
SELECT * FROM all_source;
--显示用户下的触发器
SELECT * FROM user_triggers;
--显示多有触发器
SELECT * FROM all_triggers;
--显示表中列的信息
SELECT * FROM user_tab_columns;
SELECT * FROM all_tab_columns;

CREATE_WRAPPED 过程

DECLARE
  v_sql VARCHAR2(32767);
BEGIN
  v_sql := q'[
    CREATE OR REPLACE PROCEDURE print
    AS
    BEGIN
      dbms_output.put_line('我源码被加密了,你看不到我内部的源码!');
    END;
  ]';
  
  --加密编译,即写入数据字典中的源码是暗文
  dbms_ddl.create_wrapped(v_sql);
END;
/

猜你喜欢

转载自blog.csdn.net/qq_33445829/article/details/109774295