🏺 第6章 过程、函数、触发器和包
📄 一句话概括

程序包不是新语言特性,是「组织手段」。它把相关的过程、函数、变量、游标装进一个命名空间。 最值钱的是「规范与主体分离」:对外只暴露规范,实现藏在主体里。 改实现不用改调用方,这是接口与实现分离在数据库里的落地方式。

📝 本章和前面几章的关系

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 秒实验

  1. 把 Pkg() 改成模块级的一个全局对象,重跑。 第三步会变成 2 而不是 0——因为「会话」这个概念消失了,所有调用共享同一个实例。 这就是「全局变量」和「会话级变量」的分界线。
  2. 给 Pkg 加一个 __init__ 里的 print('包初始化'),重跑。只会打一次。 对应 Oracle 里主体末尾那个 BEGIN ... END; 初始化块:每个会话只跑一次。
  3. 把第二个 fmt 改名为 fmt_date,重跑。 两个函数就能共存了——但这就不是重载,而是「起了两个名字」。 Oracle 里也能这么干,只是调用方要多记一个名字。

常见误区

  1. 把包变量当全局变量用。 它是「会话级」的,两个会话看到的值可能不一样。 拿它做计数器、做开关,迟早出错。
  2. 在规范里写实现。 规范和主体分工明确: 规范只声明,主体才写 BEGIN。规范里出现 IS 加 BEGIN,编译直接不过。
  3. 以为只靠返回类型不同就能重载。 不能。 调用时编译器只看参数类型,不看你要什么返回值,所以会报重复定义。
  4. 忘了主体里要全部实现规范声明的子程序。 漏一个就报 PLS-00323,而且提示往往只指到包名, 不容易看出漏的是哪个。
自测三问(点开对答案) 1. 规范(spec)和主体(body)各放什么? —— **规范只放声明**:变量、常量、类型、游标、过程头、函数头。 **主体放实现**,而且规范里声明的每一个子程序都必须在主体里实现。 2. 包里声明的变量是全局的吗? —— **不是,是「会话级」的。** **同一会话内一直活着,换一个会话就是初始值**——这是它和全局变量最关键的区别。 3. 为什么只能 `DROP PACKAGE BODY`,不能反过来? —— **因为主体是照着规范写的。** **规范没了,主体就失去依据**,所以「只有主体没有规范」这个状态不允许存在。

第 6 章到此为止。到这里,PL/SQL 的四层壳——匿名块、过程、函数、触发器、包——全齐了。 下一篇进第 7 章:控制文件、日志文件、归档模式。 话题从「写代码的人」转到「管库的人」。

相关笔记