1 PL/SQL入门
2 PL/SQL-常量和变量
3 PL/SQL-控制语句
4 PL/SQL-游标
5 PL/SQL-异常
6 PL/SQL-存储过程
7 PL/SQL-存储函数
8 PL/SQL-触发器
1 PL/SQL-入门
1.1 PL/SQL是什么
PL/SQL:(Procedure Language/SQL)Oracle数据库基于SQL语言的一门编程语言。
1.2 PL/SQL的作用
PL/SQL是一门编程语言,作用是在Oracle数据库中实现编程逻辑。
1.3 数据库编程的使用场景
使用场景:引用场景
Java使用JDBC操作数据库数据的缺点:
- SQL语句如果要经过网络传输到数据库,可能会被拦截(安全性不高)。
- SQL语句每次都要先解释再执行,效率相比在数据库中编程要低。
- 逻辑写在Java中,其他语言不能重用Java的代码。
数据库编程的缺点:
- 维护性差。
- 调试难。
1.4 PL/SQL的基础语法
[declare]
--声明部分
--注意:变量常量的声明必须要放在声明部分
begin
--逻辑处理部分
end;
1.5 示例代码
-- 需求:编写一段PL/SQL,打印HelloWorld
-- 注意:Oracle数据库默认是没有打开dbms_output输出的,需要使用set打开
set serveroutput on;
begin
-- 打印结果到控制台
dbms_output.put_line('HelloWorld');
end;
2 PL/SQL-变量和常量
2.1 常量的声明
-- 需求:编写一段PL/SQL,先声明一个常量存储HelloWorld,再打印HelloWorld
declare
-- 声明常量
-- 声明的语法:
-- <常量名> constant <数据类型>[(大小)]:=初始值;
-- 常量的注意事项:常量是必须要有初始值的。
-- := 是PL/SQL的赋值符号
v_say constant varchar2(10):='HelloWorld';
begin
-- 打印结果到控制台
dbms_output.put_line(v_say);
end;
2.2 变量的声明
-- 需求:编写一个PL/SQL,输入一个员工的编号,打印该员工的名字。
declare
-- 声明一个员工的编号,先定义为常量,写死
-- v_empno constant number(4):=7788;
-- 声明员工编号为变量,输入值。&后面是随便写的。注意:&在注释中也是起作用的,所有不要在注释中出现&
v_empno number(4):=&empno;
-- 声明一个变量
-- <变量名> <数据类型>[(大小)][:=初始值];
v_ename varchar2(10);
begin
-- 通过一个员工的编号查询员工的名字
-- 注意:查询语句的返回字段,只在当前SQL有效
-- 如果其他的语句需要使用SQL的返回字段,必须通过INTO赋予给一个变量
select ename into v_ename from emp where emp.empno = v_empno;
-- 输出
dbms_output.put_line(v_ename);
end;
-- 变量和常量以v开头仅仅是为了和数据库的字段名区分
2.3 %type
-- 需求:编写一个PL/SQL,输入一个员工的编号,打印该员工的名字。
-- 想法:既然v_empno和v_ename的变量的数据类型是和表的字段是一样的。那么能直接引用表字段的声明吗?
-- 答:可以的。
-- 引用表字段定义的数据类型的格式:
-- 表名.字段名%type
declare
v_empno emp.empno%type := &empno;
v_ename emp.ename%type;
begin
select ename into v_ename from emp where emp.empno = v_empno;
-- 输出
dbms_output.put_line(v_ename);
end;
2.4 %rowtype
表的行类型(%rowtype)作用是:使用一个表的结构变量来接收表的所有字段。
-- 需求:编写一个PL/SQL,输入一个员工的编号,打印该员工的名字、工资。
declare
v_empno emp.empno%type := &empno;
v_ename emp.ename%type;
v_sal emp.sal%type;
begin
select ename, sal into v_ename, v_sal from emp where emp.empno = v_empno;
dbms_output.put_line('员工姓名:'||v_ename||',员工工资:'||v_sal);
end;
-- 以上代码中一个字段需要一个变量,那么100字段就需要100变量。
-- 想法:可不可以声明一个变量接收所有的字段。类似JavaBean。
-- 答:可以通过表的行类型(%rowtype)实现。
-- %rowtype的声明的格式: 表名%rowtype
declare
v_empno emp.empno%type := &empno;
v_emp emp%rowtype;
begin
select ename, sal into v_emp.ename, v_emp.sal from emp where emp.empno = v_empno;
dbms_output.put_line('员工姓名:'||v_emp.ename||',员工工资:'||v_emp.sal);
end;
-- 行类型接收表的所有字段(*)
declare
v_empno emp.empno%type := &empno;
v_emp emp%rowtype;
begin
-- 通过一个员工的编号查询员工的名字
-- 行类型,如果不指定字段名,可以接收一个*的字段。(所有的字段)
select * into v_emp from emp where emp.empno = v_empno;
-- 输出
dbms_output.put_line('员工姓名:'||v_emp.ename||',员工工资:'||v_emp.sal);
end;
3 PL/SQL-控制语句
3.1 判断
-- 需求:指定员工的编号,判断员工的工资,如果大于等于5000:A级,如果大于等于4000:B级,如果大于等于3000:C级,否则D级。
declare
--声明一个常量
v_empno emp.empno%type := 7788;
--声明一个变量接收工资
v_sal emp.sal%type;
begin
--查询工资
select sal into v_sal from emp where empno = v_empno;
--输出工资
dbms_output.put_line('工资为:'||v_sal);
--判断输出
if v_sal>=5000 then
dbms_output.put_line('A级'); -- >=5000
elsif v_sal>=4000 then
dbms_output.put_line('B级'); -- >=4000 and <=4999
elsif v_sal>=3000 then
dbms_output.put_line('C级'); -- >=3000 and <=3999
else
dbms_output.put_line('D级'); -- <3000
end if;
end;
3.2 循环
3.2.1 loop循环
--控制语句-循环
/*
-- java do-while循环
do{
//循环体
}while(循环条件); -- 如果为true,表示继续循环
-- pl/sql loop循环
loop
//循环体
-- 判断退出循环的条件
exit when <退出循环的条件> -- 如果为true,表示退出循环
end loop;
pl/sql的loop循环和do-while的异同
相同之处:
1.它们都可以实现先循环,后判断
不同之处:
1.loop的条件是退出循环的条件,为true退出循环,而do-while循环是循环条件,为true继续循环。
2.loop循环的判断条件可以在循环体的任何位置!
*/
-- 需求:计算输出1-10累加的值。
declare
-- 声明一个变量接收总值
-- 非字符串的类型可以不指定大小
v_sum number := 0;
-- 声明一个变量接收计数
v_num number := 1;
begin
-- 循环累加
loop
-- 累加
v_sum := v_sum + v_num;
-- 计数每次循环+1
v_num := v_num + 1;
-- 判断退出循环的条件
exit when v_num=11;
end loop;
-- 输出
dbms_output.put_line(v_sum); --55
end;
-- 注意,退出循环的条件可以在loop循环体内的任何位置
3.2.2 while循环
-- while循环
/*
-- java while循环
while(<循环条件>){
-- 循环体
}
-- pl/sql while循环
-- 功能等同Java的while循环
while <循环条件>
loop
-- 循环体
end loop;
*/
-- 需求:计算输出1-10的值。
declare
v_sum number := 0;
v_num number := 1;
begin
-- 循环累加
-- 注意,while的条件是循环条件,为true,继续循环
while v_num <= 10
loop
-- 累加
v_sum := v_sum + v_num;
-- 计数+1
v_num := v_num + 1;
end loop;
dbms_output.put_line(v_sum);
end;
3.2.3 for循环
-- for循环
/*
-- java for-each循环
for (<变量> : <数组|集合>){
}
-- pl/sql
for (<变量> in [reverse] <范围|游标>){
loop
end loop;
}
*/
-- 需求:输出1-10的值
begin
-- 注意:for循环的变量可以不声明直接使用,当然也可以声明后使用
-- '..'表示一个范围,包括上下限,步长为1
for i in 1..10
loop
dbms_output.put_line(i);
end loop;
end;
-- 需求:输出10-1的值
begin
-- 注意:范围必须是从小到大的.如果需要从大到小,使用reverse
for i in reverse 1..10
loop
dbms_output.put_line(i);
end loop;
end;
4 PL/SQL-游标
4.1 游标是什么
游标是查询数据时的一个指向数据的指针。作用是用于读取表的数据。
数据库是没有Java里面的集合的概念。如果需要返回多条数据,必须通过游标去表里面直接读取。
Oracle中,如果需要读取多条数据时,必须使用游标。
4.2 游标的语法
-- 创建一个游标
cursor <游标名> is select语句
-- 打开游标
open <游标名>
-- 读取数据
fetch <游标名> into <变量列表>..
-- 关闭游标
close <游标名>
-- 游标变量
%notfound -- 如果找不到数据,返回true
%found -- 如果读到数据了,返回true
%rowcount -- 返回当前读取的记录数
%isopen -- 判断游标是否是打开状态
-- 使用格式
游标名%notfound
4.3 示例代码
-- 需求:输出emp表的所有的员工信息
-- 分析:查询出来的结果是一组数据。
declare
-- 创建一个游标
cursor v_cur_emp is select * from emp;
-- 创建一个行类型接收
v_emp emp%rowtype;
begin
-- 打开游标
open v_cur_emp;
-- 循环
loop
-- 读取
fetch v_cur_emp into v_emp;
-- 结束条件
exit when v_cur_emp%notfound;
-- 输出
dbms_output.put_line('当前记录数:'||v_cur_emp%rowcount||',员工编号:'||v_emp.empno||',姓名:'||v_emp.ename||',工资:'||v_emp.sal);
end loop;
-- 关闭
close v_cur_emp;
end;
4.4 游标引用(重点)
/*
Java里面
String s; -- s是引用
String s = new String("aaa"); -- s是对象。
引用就是没有对象的变量。
所谓的游标引用就是声明时没有select语句的游标。
游标引用的作用:将在声明部分的SQL语句移到逻辑处理部分。这样做可以实现SQL复杂的处理功能。
-- 游标引用的声明
<游标名> sys_refcursor;
-- 打开游标引用
open <游标名> for select 语句
-- 读取
fetch <游标名> into <变量>
close 游标名>
*/
-- 需求:输入一个部门,输出emp表该部门的员工信息。通过游标引用来实现
declare
-- 声明一个游标引用
v_cur_emp sys_refcursor;
-- 声明一个部门编号
v_deptno emp.deptno%type := &deptno;
-- 声明一个行类型
v_emp emp%rowtype;
begin
-- 打开游标引用
open v_cur_emp for select empno,ename,sal,deptno from emp where deptno=v_deptno;
-- 循环
loop
-- 读取
-- 注意:读取游标返回的变量和查询语句的返回字段要一一对应
fetch v_cur_emp into v_emp.empno,v_emp.ename,v_emp.sal,v_emp.deptno;
exit when v_cur_emp%notfound;
dbms_output.put_line('员工编号:'||v_emp.empno||',姓名:'||v_emp.ename||',工资:'||v_emp.sal||',部门编号:'||v_emp.deptno);
end loop;
-- 关闭
close v_cur_emp;
end;
5 PL/SQL-异常(了解)
5.1 异常是什么
异常是已经预定义的错误。
5.2 常见的预定义异常
预定义异常 | 说明 |
---|---|
CURSOR_ALREADY_OPEN | 试图”OPEN”一个已经打开的游标 |
DUP_VAL_ON_INDEX | 试图向有”UNIQUE”中插入重复的值 |
INVALID_CURSOR | 试图对以关闭的游标进行操作 |
INVALID_NUMBER | 在SQL语句中将字符转换成数字失败 |
LOGIN_DENIED | 使用无效用户登陆 |
NO_DATA_FOUND | 没有找到数据时 |
NOT_LOGIN_ON | 没有登陆Oracle就发出命令时 |
PROGRAM_ERROR PL/SQL | 存在诸如某个函数没有”RETURN”语句等内部问题 |
STORAGE_ERROR PL/SQL | 耗尽内存或内存严重不足 |
TIMEOUT_ON_RESOURCE | 等待资源期间发生超时 |
TOO_MANY_ROWS | “SELECT INTO”返回多行时 |
VALUE_ERROR | 当出现赋值错误 |
ZERO_DIVIDE | 除数为零 |
others | 捕捉所有的异常 |
5.3 异常的语法
-- 声明语法
[declare]
-- 声明部分
begin
-- 逻辑处理部分
-- 必须在逻辑处理部分的最后
[exception
when <异常名> then
-- 处理
when others then --others表示捕捉所有的异常
-- 处理
]
end;
-- 异常有两个异常变量(只能用于异常处理部分)
1.sqlcode : 错误码
2.sqlerrm : 错误信息
5.4 示例代码
-- 除数不能为零
declare
v_num number := 0;
begin
-- 除数为,0报错
v_num := 10/0;
-- 异常处理
exception
-- when ZERO_DIVIDE then
when others then
-- dbms_output.put_line('除数不可以为0,错误码:'||sqlcode ||',错误信息:' ||sqlerrm);
-- 异常可以自定义信息抛出。
raise_application_error(-20004, '除数为0了!不可以哟');
end;
6 PL/SQL-存储过程
6.1 存储过程是什么
存储过程是一个命名的PL/SQL语句块。它使用一个数据库对象封装了PL/SQL语句,实现功能的重用。类似Java里面的方法。
6.2 存储过程的作用
实现PL/SQL语句的重用。
6.3 存储过程的语法
-- 创建的语法
create [or replace] procedure <存储过程名>[(参数列表)]
as
-- 声明部分
begin
-- 逻辑处理部分
[exception]
-- 异常处理部分
end;
-- 参数列表
输入参数 : <参数名> [in] <数据类型>
输出参数 : <参数名> out <数据类型>
6.4 存储过程入门-无参数
-- 编写一个存储过程,输出HelloWorld
create or replace procedure pro_say_hello
as
-- 声明一个常量
v_hello constant varchar2(10) := 'HelloWorld';
begin
-- 输出
dbms_output.put_line(v_hello);
end;
-- 查看
select * from SYS.user_procedures;
-- 调用
-- 方式1:使用命令调用
exec pro_say_hello;
-- 方式2;使用pl/sql调用
begin
pro_say_hello;
end;
-- 移除存储过程
drop procedure pro_say_hello;
6.5 存储过程-有参数
-- 需求:传入一个员工的编号,输出该员工的姓名
create or replace procedure pro_find_by_empno(p_empno in emp.empno%type,p_ename out emp.ename%type)
as
begin
-- 查询
select ename into p_ename from emp where emp.empno=p_empno;
end;
-- pl/sql调用
declare
-- 声明一个变量
v_ename emp.ename%type;
begin
-- 注意:输出参数必须需要一个变量接收输出的值
pro_find_by_empno(7788,v_ename);
-- 输出变量
dbms_output.put_line(v_ename);
end;
6.6 存储过程-有游标
-- 需求:传入一个员工的部门编号,输出该员工的信息
-- 输出一组数据,必须使用游标
create or replace procedure pro_find_by_deptno(p_dpetno in emp.deptno%type,p_cur_emp out sys_refcursor)
as
begin
-- 将查询结果返回给游标
open p_cur_emp for select * from emp where deptno=p_dpetno;
end;
-- 使用pl/sql调用
declare
-- 声明一个游标引用接收输出的值
v_cur_emp sys_refcursor;
-- 使用一个行类型接收
v_emp emp%rowtype;
begin
-- 执行存储过程
pro_find_by_deptno(10,v_cur_emp);
-- 循环
loop
fetch v_cur_emp into v_emp;
-- 读不到数据退出
exit when v_cur_emp%notfound;
-- 打印结果
dbms_output.put_line('员工名:'||v_emp.ename||',部门编号:'||v_emp.deptno);
end loop;
-- 关闭
close v_cur_emp;
end;
6.7 使用Java调用存储过程
/**
* 获得连接对象的工具类
*/
public class DbUtils {
public static Connection getConnection() {
// 四要素
// 1.驱动
String driver = "oracle.jdbc.OracleDriver";
// 2.连接字符串
String url = "jdbc:oracle:thin:@localhost:1521:orcl";
// 3.用户名
String user = "xust";
// 4.密码
String password = "xust";
try {
// 加载驱动
Class.forName(driver);
// 获得连接对象
Connection conn = DriverManager.getConnection(url, user, password);
return conn;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 调用Oracle数据库中的存储过程pro_find_by_deptno
*/
@Test
public void findByDeptno() throws SQLException {
// 存储过程的操作命令,格式为{call <存储过程名> (?,?)}
String sql = "{call pro_find_by_deptno(?,?)}";
// 1.获得连接
Connection conn = DbUtils.getConnection();
// 2.获得操作对象
CallableStatement call = conn.prepareCall(sql);
// (1)设置参数1,部门编号为10,参数的下标是从1开始
call.setInt(1, 10);
// (2)设置参数2,参数2是一个输出的参数,需要指定输出参数的类型
call.registerOutParameter(2, OracleTypes.CURSOR);
// 3.操作
call.execute();
// 获得存储过程返回的数据
// 游标对应的Java类为ResultSet
ResultSet resultSet = (ResultSet) call.getObject(2);
while(resultSet.next()) {
System.out.println("员工编号:" + resultSet.getInt("empno") + "部门编号:" + resultSet.getInt("deptno") + "员工姓名:" + resultSet.getString("ename"));
}
// 4.关闭
resultSet.close();
call.close();
conn.close();
}
7 PL/SQL-存储函数(了解)
存储函数也是一个命名的PL/SQL语句块。它也是使用一个数据库对象封装了PL/SQL的语句,实现功能的重用。存储函数就是一个自定义函数,类似Java里面的方法。
存储函数和存储过程的区别:
- 存储函数是可以被SQL语句调用(DML/DQL),而存储过程是不可以。
- 存储函数是必须有返回值,而存储过程是没有返回值的。
由于存储函数可以被SQL语句调用,而存储过程不可以。通常习惯性地,存储函数用于处理复杂的SQL语句,而存储过程用于被第三方程序调用。
7.1 存储函数的语法
-- 创建语法
create [or repalce] function <函数名>(参数名 [in]/out 类型) return <数据类型>
as
begin
return <返回值>;
end;
7.2 示例代码
-- 需求:传入一个员工的编号,返回该员工的工资
create or replace function fn_find_sal_by_empno(p_empno in emp.empno%type) return emp.sal%type
as
-- 声明一个变量接收工资
v_sal emp.sal%type;
begin
select sal into v_sal from emp where empno=p_empno;
-- 返回工资
return v_sal;
end;
-- 查看
-- 查看所有用户级别的数据库对象user_objects
select * from user_objects where object_type='FUNCTION';
-- 调用
-- 方式1:SQL调用
select fn_find_sal_by_empno(7788) from dual;
-- 方式2:PL/SQL调用
declare
v_sal emp.sal%type;
begin
v_sal := fn_find_sal_by_empno(7788);
dbms_output.put_line(v_sal);
end;
7.3 使用Java调用存储函数
/**
* 调用Oracle数据库中的存储函数fn_find_sal_by_empno
*/
@Test
public void fn_find_sal_by_empno() throws SQLException {
// 调用fn_find_sal_by_empno
// 命令格式:{? = call <存储函数名> {?,?}}
String sql = "{? = call fn_find_sal_by_empno(?)}";
// 1.获得连接对象
Connection conn = DbUtils.getConnection();
// 2.获得操作对象
CallableStatement call = conn.prepareCall(sql);
// 参数一,返回输出。需要指定输出类型返回工资
call.registerOutParameter(1, OracleTypes.DOUBLE);
// 参数二,输入的值。员工编号
call.setInt(2, 7788);
// 3.操作
call.execute();
// 获得返回值
Double sal = call.getDouble(1);
System.out.println(sal);
// 4.关闭
call.close();
conn.close();
}
8 PL/SQL-触发器
8.1 触发器是什么
触发器是在操作数据表时(增删改),会自动触发一段PL/SQL 的数据库对象。
8.2 触发器的作用
理论上,它可以实现在操作数据时,执行任何的PL/SQL。但是实际上,我们一般都是用于一些不操作其他数据的功能。如:
- 数据备份
- 隐藏ID自增长列
- 数据统计
8.3 触发器的语法
-- 创建触发器
create [or replace] trigger <触发器名>
before|alter -- 在操作之前还是之后触发,二选一
insert|delete|update -- 设置触发器的操作类型,三选一
[of <字段名>] -- 如果指定的字段发生了修改,就触发。用于update触发器
on <表名> --指定是哪个表的触发器
[for each row] --行级触发器还是表级触发器
------------------------pl/sql-----------------------------
[declare]
--声明部分
begin
--逻辑处理部分
[exception]
end;
-- 有 [for each row]:行级触发器,每影响一行记录,就触发一次。
-- 没有[for each row]:表级触发器,每次操作,触发一次。
-- 触发器变量
:new.新记录,insert、update操作才有新的数据,所以:new只能用户更新和插入触发器
:old.旧记录,旧记录表示表里面已经有的数据。update、delete操作时有旧记录,而插入时是没有旧记录的。所以:old只能用于更新和删除
8.4 示例代码
-- 需求:每次插入emp表记录之前,就触发打印HelloWorld
create or replace trigger tri_emp_insert_sayhello
before -- 操作之前触发
insert -- 触发器器类型是插入
on emp -- 指定表名
for each row -- 每插入一条就打印一个HelloWorld,使用行级触发器
declare
-- 声明一个常量
v_hello constant varchar(10) := 'HelloWorld';
begin
-- 输出
dbms_output.put_line(v_hello);
end;
-- 查看
select * from user_triggers;
-- 测试
insert into emp(empno,ename) values(4001,'tri1');
8.5 触发器-隐藏ID自增长列
隐藏ID自增列:MySQL中,ID主键实现auto-increment后,插入数据可以不写该主键,MySQL会自动补全。但在Oracle中没有auto-increment,要想隐藏ID自增长列,就要应用序列+触发器。
-- 前提:必须创建一个序列。
-- 通过触发器实现将序列值,自动填入ID列。
create or replace trigger tri_emp_insert_autoincrement
before -- 由于主键字段是不可以为null,所以必须要在插入之前填入值
insert
on emp
for each row
begin
-- 如何获得当前插入的记录填入值
-- 通过触发器变量(:new)获得正在插入的新记录
:new.empno := seq_emp.nextval;
end;
--测试
insert into emp(ename) values('tri11');
select seq_emp.currval from dual;
8.6 触发器-数据备份
-- 创建一个触发器实现备份
create or replace trigger tri_emp_delete_backup
before --备份数据一定在删除之前
delete
on emp
for each row
begin
-- 每删除一条数据就备份该数据到emp_bak
insert into
emp_bak(empno,ename,job,mgr,hiredate,sal,comm,deptno) values(:old.empno,:old.ename,:old.job,:old.mgr, :old.hiredate,:old.sal,:old.comm,:old.deptno);
end;
-- 删除一条数据
delete from emp where empno = 1;
-- 删除所有emp表的数据
delete from emp;
-- 删除触发器
drop trigger tri_emp_insert_sayhello;