程序包不是新语言特性,是「组织手段」。它把相关的过程、函数、变量、游标装进一个命名空间。 最值钱的是「规范与主体分离」:对外只暴露规范,实现藏在主体里。 改实现不用改调用方,这是接口与实现分离在数据库里的落地方式。
6.1 到 6.3 已经造出了三种对象:过程、函数、触发器。
它们都是「散装」的——名字直接挂在模式(schema)下。
对象一多,命名冲突、权限授予、依赖管理全都会变成麻烦。
包就是解决这个麻烦的。
Oracle 自带的功能包(DBMS_OUTPUT、DBMS_STATS、UTL_FILE)全是用这套机制写的,
所以看懂包,也顺便看懂了那些 DBMS_ 开头的家伙是什么。
一、程序包规范:只写「有什么」
规范(specification,简称 spec)只声明,不实现。
里面能放:变量、常量、类型、游标、过程头、函数头——全都是「声明」,没有一句 BEGIN。
关键点:过程头和函数头后面是分号,没有 IS。
PROCEDURE raise_sal (p_empno IN NUMBER); 就是一句声明,
写了 IS 就变成「要在规范里实现」,那是错的。
规范里声明的每一样东西,都是「公开」的——
包外可以调用,也需要授权(GRANT EXECUTE ON 包名)。
授权是按包授的,不是按过程授的,这一点比散装对象省事得多。
规范里声明的变量是「会话级」的。
同一个会话里第一次访问时初始化,之后一直活着,直到会话结束。
注意它不是「全局变量」——两个会话看到的值可能不一样。
二、程序包主体:写「怎么实现」
主体(body)是 CREATE OR REPLACE PACKAGE BODY 包名,多一个 BODY。
主体里要把规范声明过的东西全部实现一遍,一个都不能漏。
漏了报 PLS-00323(规范里声明的子程序必须在主体里定义)。
只在主体里定义、规范里不声明的,就是「私有」的。
包外调不到,包内随便用——
这和 Java 的 private 是一个意思,但实现更简单:不出现在规范里,外面就看不见。
主体末尾可以有一个初始化块:
BEGIN ... END; 写在所有子程序之后、END 包名; 之前。
它在「本会话第一次使用这个包」时跑一次,适合做「加载配置」这类一次性的活。
它不是构造函数,不会每次调用都跑——这一点常被误解。
包里还支持重载:同名函数,参数类型不同。
fmt(p IN NUMBER) 和 fmt(p IN DATE) 可以共存,调用时按实参类型自动选。
但只靠返回类型不同不算重载——
f(NUMBER) RETURN VARCHAR2 和 f(NUMBER) RETURN DATE 编译不过,因为调用时编译器只看参数,
不看你要什么返回值。
重载只在包里支持,单独的过程和函数不行。
这是包比「一堆散装过程」强的又一处,
也是 Oracle 自带的 DBMS_ 包能做到「一个名字管一堆用法」的原因。
三、删除程序包:规范和主体分开删
两句要分清:
DROP PACKAGE emp_pkg; 连规范带主体一起删;
DROP PACKAGE BODY emp_pkg; 只删实现,规范留着。
反过来是不行的——不能只有主体没有规范。
因为主体是「照着规范写的」,规范没了,主体就失去依据。
只删主体之后,包处于「规范在、实现在」之外的第三种状态:无效。
这时候访问包里任何子程序都会报 ORA-04068(包的现有状态已被丢弃)或 ORA-06508。
这个状态在「刚改完主体、还没编译通过」时很常见,别慌,编译一下就好。
顺带一个实战提醒:重新编译包会让「当前所有会话的包状态」失效。
如果包里有会话级变量(比如缓存),重新编译后第一个访问它的会话
会收到 ORA-04068,然后重新加载。这就是「改包会让应用报一下错」的原因。
四、动手 Demo:用一个 Python 类模拟包:会话级变量 + 重载
沙箱里没有 Oracle,CREATE PACKAGE 一行都执行不了。
但包的两个核心特性——「会话级变量」和「重载」——都能用 Python 比划清楚。
我用一个类模拟这个包,走三步验证「变量跟着会话走」,
再故意写两个同名函数,看 Python 和 Oracle 的处理方式差在哪。
class Pkg(object):
u"""模拟一个程序包:规范里的变量 + 主体里的过程。"""
def __init__(self):
self.g_counter = 0 # 会话级变量,首次访问时初始化
def bump(self):
self.g_counter += 1
return self.g_counter
s = Pkg()
print('(1) 首次读 g_counter =', s.g_counter)
s.bump()
s.bump()
print('(2) 调两次 bump 之后 =', s.g_counter)
print('(3) 换一个会话再读 =', Pkg().g_counter)
# 重载:Python 里同名函数是「后一个覆盖前一个」,只有包才支持同名不同参
def fmt(p):
return 'NUMBER 版'
def fmt(p): # 前一个被覆盖了
return 'DATE 版'
print('fmt(1) ->', fmt(1))
实跑输出:
--------------------------------------------------------------------
第二十九课:程序包——把一堆过程函数打成一套
--------------------------------------------------------------------
**程序包(package)不是一个新语言特性,是一个「组织手段」。**
**它把相关的变量、过程、函数、游标打包成一个命名空间。**
**好处有三个:**
**① 名字不打架(都挂在包名下);② 有「私有」和「公开」之分;③ 整个包一次编译、一次加载,比一个个过程调快。**
规范与主体:一个包,两个文件
--------------------------------------------------------------------
[Oracle 原码 · 本沙箱不执行]
| -- 规范(spec):只写「有什么」,不写「怎么实现」
| CREATE OR REPLACE PACKAGE emp_pkg IS
| g_min_sal NUMBER := 800;
| PROCEDURE raise_sal (p_empno IN NUMBER, p_pct IN NUMBER DEFAULT 10);
| FUNCTION get_grade (p_sal IN NUMBER) RETURN VARCHAR2;
| END emp_pkg;
| /
|
| -- 主体(body):写「怎么实现」
| CREATE OR REPLACE PACKAGE BODY emp_pkg IS
| PROCEDURE raise_sal (p_empno IN NUMBER, p_pct IN NUMBER DEFAULT 10) IS
| BEGIN
| UPDATE emp SET sal = sal * (1 + p_pct / 100) WHERE empno = p_empno;
| END;
|
| FUNCTION get_grade (p_sal IN NUMBER) RETURN VARCHAR2 IS
| BEGIN
| RETURN CASE WHEN p_sal < 3000 THEN '低' ELSE '高' END;
| END;
| END emp_pkg;
规范里声明过的东西,主体里必须全部实现。
**为什么要分成两个?** **因为规范是「对外合同」,主体是「内部实现」。** **调用方只需要规范**——
编译时只查规范,运行时才加载主体。
**这带来一个重要特性:改主体不用重新编译调用方。**
**只要规范不变(参数没变),你改函数体,调用方的代码一个字都不用动。**
**这就是「接口与实现分离」在数据库里的落地方式。**
包里可以藏东西
--------------------------------------------------------------------
**只在主体里定义、规范里不声明的东西,就是「私有」的。** **包外调不到,包里随便用。**
**这一点和 Java 的 `private` 是一个意思**,
**但实现方式更简单:不出现在规范里,外面就看不见。**
**上面那段主体里我留了一手:`get_grade` 的实现和 6.2 节的不一样。**
**规范里写的是「返回档位」,主体里只分了两档——**
**这不是错,是故意演示「调用方不需要知道实现细节」。**
**当然真写代码别这么干,行为不一致会出事。**
包里的变量:整个会话共用一份
--------------------------------------------------------------------
**包里声明的变量(`g_min_sal` 这种)是「会话级」的:**
**同一个会话里,第一次访问时初始化,之后一直活着,直到会话结束。**
我用一个 Python 模块模拟这个包,走三步:
**① 读一次 `g_counter`:0**
**② 调两次 `bump()` 之后:2**
**③ 换一个「会话」(新建对象)再读:0**
**第 ③ 步是关键:新会话看到的是初始值,不是 2。** **因为包变量跟着会话走,不是跟着数据库走。**
**这一点经常被误解成「全局变量」——它不是,它是「会话全局」。**
**这个特性有用也有坑:**
**可以用它做「会话级缓存」(比如缓存一次配置查询的结果),但也意味着两个会话看到的值可能不一样。**
包的重载:同名函数,参数不同
--------------------------------------------------------------------
[Oracle 原码 · 本沙箱不执行]
| CREATE OR REPLACE PACKAGE util_pkg IS
| FUNCTION fmt (p IN NUMBER) RETURN VARCHAR2;
| FUNCTION fmt (p IN DATE) RETURN VARCHAR2;
| END util_pkg;
同名不同参,叫重载。
**重载在包里才支持,单独的过程/函数不行。** **这是包比「一堆散装过程」强的又一处。**
**但要小心:只靠返回类型不同,不算重载。**
**`f(NUMBER) RETURN VARCHAR2` 和 `f(NUMBER) RETURN DATE` 会报「重复定义」**——
**因为调用时编译器只看参数,不看你要什么返回值。**
删除:DROP PACKAGE
--------------------------------------------------------------------
[Oracle 原码 · 本沙箱不执行]
| DROP PACKAGE emp_pkg;
| DROP PACKAGE BODY emp_pkg;
只删主体、保留规范是可以的,反过来不行。
**两句的顺序有讲究:**
**`DROP PACKAGE` 会连主体一起删;`DROP PACKAGE BODY` 只删实现、保留规范。**
**保留规范的状态下,包是「无效」的**——
**调用会报错,但编译能过(因为规范还在)。**
**这个状态在排查「改了主体之后调用方报错」时很容易遇到。**
三步输出是 0、2、0。第三步那个 0 是重点。
新建一个对象(相当于开一个新会话)再读,看到的是初始值 0,不是 2。
这说明包变量跟着「会话」走,不跟着「数据库」走。
这个特性有用也有坑。
有用:可以做「会话级缓存」——一次配置查询的结果存进包变量,同一个会话里反复用,省掉重复查询。
坑:两个会话看到的值可能不一样,谁要是拿它当「全局计数器」,一定会算错。
重载那段是反例教材:Python 里两个同名 fmt,后一个直接把前一个覆盖了。
fmt(1) 返回「DATE 版」,不是「NUMBER 版」——Python 没有重载,只有「后来的盖掉前面的」。
Oracle 的包能分清这两个 fmt,因为它在编译时记录了「参数类型」这个签名。
fmt(1) 传的是 NUMBER,调 NUMBER 版;传 SYSDATE 就调 DATE 版。
这就是「重载」和「覆盖」的本质区别:一个并存,一个替换。
顺带说一句 Python 的等价做法:用 functools.singledispatch 能按类型分派,
机制和重载很像,但要自己注册。
这说明「按类型选同名函数」这个需求不是 Oracle 独有的,只是各家实现方式不同。
30 秒实验
- 把
Pkg()改成模块级的一个全局对象,重跑。 第三步会变成 2 而不是 0——因为「会话」这个概念消失了,所有调用共享同一个实例。 这就是「全局变量」和「会话级变量」的分界线。 - 给
Pkg加一个__init__里的print('包初始化'),重跑。只会打一次。 对应 Oracle 里主体末尾那个BEGIN ... END;初始化块:每个会话只跑一次。 - 把第二个
fmt改名为fmt_date,重跑。 两个函数就能共存了——但这就不是重载,而是「起了两个名字」。 Oracle 里也能这么干,只是调用方要多记一个名字。
常见误区
- 把包变量当全局变量用。 它是「会话级」的,两个会话看到的值可能不一样。 拿它做计数器、做开关,迟早出错。
- 在规范里写实现。 规范和主体分工明确:
规范只声明,主体才写
BEGIN。规范里出现IS加BEGIN,编译直接不过。 - 以为只靠返回类型不同就能重载。 不能。 调用时编译器只看参数类型,不看你要什么返回值,所以会报重复定义。
- 忘了主体里要全部实现规范声明的子程序。 漏一个就报
PLS-00323,而且提示往往只指到包名, 不容易看出漏的是哪个。
自测三问(点开对答案)
1. 规范(spec)和主体(body)各放什么? —— **规范只放声明**:变量、常量、类型、游标、过程头、函数头。 **主体放实现**,而且规范里声明的每一个子程序都必须在主体里实现。 2. 包里声明的变量是全局的吗? —— **不是,是「会话级」的。** **同一会话内一直活着,换一个会话就是初始值**——这是它和全局变量最关键的区别。 3. 为什么只能 `DROP PACKAGE BODY`,不能反过来? —— **因为主体是照着规范写的。** **规范没了,主体就失去依据**,所以「只有主体没有规范」这个状态不允许存在。第 6 章到此为止。到这里,PL/SQL 的四层壳——匿名块、过程、函数、触发器、包——全齐了。 下一篇进第 7 章:控制文件、日志文件、归档模式。 话题从「写代码的人」转到「管库的人」。
- ☐ (本篇完,下一篇 7.1 管理控制文件)
相关笔记
- 存储过程
- 触发器
- PL/SQL 简介
-
- 🐬 MySQL必知必会/00-索引
- 🏺 Oracle从入门到精通 · 索引