包头是对外契约,只声明需被外部调用的函数、过程、类型、游标和常量;包体实现功能并可声明私有元素;必须先建包头再建包体,变量应尽量局部化以避免会话间状态污染。

包头只放真正需要对外暴露的接口
包头不是“所有声明都往里塞”的垃圾桶。它本质是契约——告诉调用方“你能用什么”,而不是“我内部怎么写”。把 FUNCTION、PROCEDURE、TYPE、CURSOR 或 CONSTANT 放进包头,前提是它们会被包外代码直接引用。比如一个用于计算奖金的 get_year_sal 函数必须在包头声明;但校验逻辑用的私有函数 validate_comm 就不该出现。
常见错误是把包体里用到的所有变量、类型、游标全挪到包头,结果导致:外部误用私有结构、包头臃肿难读、修改私有实现时被迫重编译包头(触发依赖它的所有对象失效)。
- 包头中避免声明仅包内使用的
VARIABLE(如临时计数器、中间缓存) - 自定义
RECORD或REF CURSOR类型,只在包头声明那些会被OUT参数或返回值暴露出去的 - 常量若只服务于包内逻辑,移到包体声明更安全
包体里用私有子程序拆解复杂逻辑
包体才是你写“脏活累活”的地方。把一段长过程(比如插入用户并同步发通知再更新统计)硬塞进一个 PROCEDURE 里,等于放弃可读性和单元测试能力。应该用私有子程序切分职责:比如 insert_user、send_notification、update_stats 都只做一件事,且只在包体内可见。
这样做的好处不只是好看:调试时能单步进具体步骤;修改发信逻辑不影响插入逻辑;后续加事务控制也只在调用处加 SAVEPOINT 和异常回滚点,不污染主流程。
- 私有子程序无需在包头声明,也不用加
PUBLIC注释——Oracle 本身就不允许外部访问 - 参数命名保持一致风格,比如输入统一用
p_前缀(p_user_id),避免和包级变量名冲突 - 不要为了“复用”把私有过程改造成通用接口——真需要复用,应另建新包,而非膨胀现有包
避免包级变量状态污染,尤其跨会话场景
std_comm NUMBER := 0.10 这类包级变量看着方便,但它是会话级(session-level)的。一旦你在包头里声明了可变的全局变量,就等于把状态耦合进了调用上下文——A 用户改了值,B 用户下一次调用可能拿到意外值;更糟的是,在连接池环境下,会话复用会让这个“全局”变成不可预测的共享内存。
除非明确需要会话内状态保持(比如分页游标缓存),否则默认应把变量移入子程序局部作用域。若真要配置化,改用参数传入或查配置表,比依赖包变量可靠得多。
- 包头中初始化为
NULL的变量(如v_counter NUMBER)极易被误认为“安全默认”,实际首次使用前未赋值就会出错 - 包体中用
BEGIN ... END;块包裹初始化语句,只在包加载时执行一次,但无法解决多会话并发写同一变量的问题 - 用
PRAGMA SERIALLY_REUSABLE可强制包状态不跨会话保留,但会禁用包级变量缓存,适合无状态工具包
包头与包体编译顺序和依赖检查不能跳过
先建包头,再建包体,不是仪式感,而是 Oracle 的硬性依赖机制。如果包体里引用了包头没声明的 PROCEDURE,编译直接报 PLS-00302: component 'XXX' must be declared;反过来,若包头删了一个函数但包体没同步删掉对应实现,编译包体时报 PLS-00323: subprogram or cursor 'XXX' is declared in a package specification and must be defined in the package body。
开发中容易忽略的是依赖传播:包A调用了包B的函数,而包B包头改了签名,包A不会自动失效,但运行时才报错。所以每次改包头后,最好手动检查哪些对象依赖它(查 ALL_DEPENDENCIES),尤其涉及生产环境升级。
- 用
CREATE OR REPLACE PACKAGE和CREATE OR REPLACE PACKAGE BODY是安全的,但替换包头会令所有依赖它的包/过程失效,需重新编译 - SQL*Plus 或 SQL Developer 中执行包体脚本前,确保当前会话已成功创建或替换过包头
- CI/CD 流程中,应将包头脚本放在包体脚本之前执行,并加入编译验证步骤(如
SELECT * FROM USER_ERRORS WHERE TYPE IN ('PACKAGE','PACKAGE BODY');)
包最难维护的地方,往往不在语法对不对,而在于“谁在什么时候改了哪一行,又影响了谁”。把包头当接口协议来守,把包体当私有实现来封,比堆砌功能更能扛住时间。


















