将代码封装在机灵的包中

http://www.oracle.com/technetwork/issue-archive/2013/13-jan/o13plsql-1872456.html

绝大多数基于PL/SQL的应用都是由成千上万甚至上百万行代码组成,这里面包括了具体多变的用户需求。

商业逻辑的实现最初是由存储过程和函数完毕,可是开发人员须要考虑将这些过程和函数放在包中维护。

何为包?

包是一组PL/SQL代码元素(游标、类型、变量、过程、函数)集合的程序单元。

通常由包声明(对象声明)和包体(具体实现)组成。

为什么要使用包?

1)组织和维护一组功能相关的对象;

2)对外隐藏具体实现;

3)提升性能。这一点要说一下:

当你第一次调用包时。整个包被载入入内存。接下来对同一包元素进行调用无需额外的磁盘I/O。

另外,包级别变量能够再会话级别(session-level)缓存起来。从而减少数据读取时间。

4)最小化程序单元重编译

外部程序(未定义在包中)仅能调用包声明中的子程序。假设你改变并又一次编译了包体。那些外部程序

将不会失效。

以下展示一下包的魅力:

1 一个简单的包:

假设我的employees表定义例如以下:

SQL> desc employees

Name             Type
———————————— —————————————
EMPLOYEE_ID NUMBER(38)
FIRST_NAME VARCHAR2(30)
LAST_NAME VARCHAR2(50)

以下我须要定义一个process_employee的过程。返回员工全名(last_name, first_name)以供其它

程序调用。

Code Listing 1: The process_employee procedure

CREATE OR REPLACE PROCEDURE process_employee (
employee_id_in IN employees.employee_id%TYPE)
IS
l_fullname VARCHAR2(100);
BEGIN
SELECT last_name || ',' || first_name
INTO l_fullname
FROM employees
WHERE employee_id = employee_id_in;
...
END;

细致看。这个过程有几个问题:

1)l_fullname 长度固定为100?

2)l_fullname的表达式固定为 last_name || ‘,’ || first_name?万一哪天客户改变主意:

我们想在全部报告和信息中显示:first_name【空格】last_name咋办?假设你在N个过程中都已经

使用了这样的结构。那你是不是去一一找出来改动掉?

3)最后一点,我们非常有可能在不同的过程中编写一些反复SQL,这样会大大减少效率和性能

这个时间,我们须要将这样的通用逻辑藏在包中,保证一处维护处处受益:

CREATE OR REPLACE PACKAGE employee_pkg
2 AS
3 SUBTYPE fullname_t IS VARCHAR2 (100);
4
5 FUNCTION fullname (
6 last_in employees.last_name%TYPE,
7 first_in employees.first_name%TYPE)
8 RETURN fullname_t;
9
10 FUNCTION fullname (
11 employee_id_in IN employees.employee_id%TYPE)
12 RETURN fullname_t;
13 END employee_pkg;

回头再改写过程,能够这样:

CREATE OR REPLACE PROCEDURE process_employee (
employee_id_in IN employees.employee_id%TYPE)
IS
l_name employee_pkg.fullname_t;
employee_id_in employees.employee_id%TYPE := 1;
BEGIN
l_name := employee_pkg.fullname (employee_id_in);
...
END;

代码变整洁了,还有你压根不须要关心employee_pkg.fullname 怎样实现!多省心。

来看下包体是怎样实现的:

CREATE OR REPLACE PACKAGE BODY employee_pkg
2 AS
3 FUNCTION fullname (
4 last_in employees.last_name%TYPE,
5 first_in employees.first_name%TYPE
6 )
7 RETURN fullname_t
8 IS
9 BEGIN
10 RETURN last_in || ', ' || first_in;
11 END;
12
13 FUNCTION fullname (employee_id_in IN employee.employee_id%TYPE)
14 RETURN fullname_t
15 IS
16 l_fullname fullname_t;
17 BEGIN
18 SELECT fullname (last_name, first_name) INTO l_fullname
19 FROM employees
20 WHERE employee_id = employee_id_in;
21
22 RETURN l_fullname;
23 END;
24 END employee_pkg;

这里用到了函数重载。使得外部过程仅仅须要传入不同參数就可以调用不同版本号的函数。

终于都会返回fullname。

2 包级别数据

此类数据由包声明和包体中全局的variables 和 constants组成。

比如:

CREATE OR REPLACE PACKAGE plsql_limits
IS
c_varchar2_length CONSTANT
PLS_INTEGER := 32767;
g_start_time PLS_INTEGER;
END;

当你在一个子程序或匿名块中声明一个变量。称为本地变量。其声明周期限制在一次子程序调用或匿名块运行。

而包级别数据是在整个会话期间都会存活。

假设你在包体中定义包数据(变量和常量),该数据相同在会话期间存活,可是这类数据仅仅能被包中程序使用。即为私有数据。

还有一方面。假设是在包声明中定义包数据则对全部具有运行包权限的程序都可使用。

来看一个样例:

DBMS_UTILITY包中GET_CPU_TIME函数可用来计算你的程序耗时

Code Listing 5: DBMS_UTILITY.GET_CPU_TIME measures

DECLARE
l_start PLS_INTEGER;
BEGIN
/* Get and save the starting time. */
l_start := DBMS_UTILITY.get_cpu_time; /* Run your code. */
FOR indx IN 1 .. 10000
LOOP
NULL;
END LOOP; /* Subtract starting time from current time. */
DBMS_OUTPUT.put_line (
DBMS_UTILITY.get_cpu_time - l_start);
END;
/

看着足够简单了吧,可是你还是须要声明一个本地变量来存放耗时!

so,我们有更快捷的方式,使用自己定义包timer_pkg!!!

Code Listing 6: The timer_pkg package

CREATE OR REPLACE PACKAGE timer_pkg
IS
PROCEDURE start_timer; PROCEDURE show_elapsed (message_in IN VARCHAR2 := NULL);
END timer_pkg;
/ CREATE OR REPLACE PACKAGE BODY timer_pkg
IS
g_start_time NUMBER := NULL; PROCEDURE start_timer
IS
BEGIN
g_start_time := DBMS_UTILITY.get_cpu_time;
END; PROCEDURE show_elapsed (message_in IN VARCHAR2 := NULL)
IS
BEGIN
DBMS_OUTPUT.put_line (
message_in
|| ': '
|| TO_CHAR (DBMS_UTILITY.get_cpu_time - g_start_time)); start_timer;
END;
END timer_pkg;
/

改写之前的匿名块,例如以下:

BEGIN
timer_pkg.start_timer;
FOR indx IN 1 .. 10000
LOOP
NULL;
END LOOP;
timer_pkg.show_elapsed ('10000 Nothings');
END;
/

哇哦!

good job!

不再须要声明本地变量,不再须要理解GET_CPU_TIME function 怎样工作!

3 子程序重载

我们都知道DBMS_OUTPUT.PUT_LINE用于往控制台打印字符数据。

BEGIN
DBMS_OUTPUT.PUT_LINE (100);
END;

其有一个弊端,仅仅能输出字符类型!

SQL> BEGIN
2 DBMS_OUTPUT.PUT_LINE (TRUE);
3 END;
4 /
DBMS_OUTPUT.PUT_LINE (TRUE);
*
ERROR at line 2:
ORA-06550: line 2, column 4:
PLS-00306: wrong number or types of
arguments in call to ‘PUT_LINE’

多尴尬。 比較BOOLEAN类型无法转成字符类型!

非常多开发人员不得不这么搞:

IF l_student_is_registered
THEN
DBMS_OUTPUT.PUT_LINE ('TRUE');
ELSE
DBMS_OUTPUT.PUT_LINE ('FALSE');
END IF;

不得不说精神可嘉!

可是。我们有更好的方式:

Code Listing 7: The my_output package without overloading

CREATE OR REPLACE PACKAGE my_output
IS
PROCEDURE put_line (value_in IN VARCHAR2); PROCEDURE put_line (value_in IN BOOLEAN); PROCEDURE put_line (
value_in IN DATE,
mask_in IN VARCHAR2 DEFAULT 'YYYY-MM-DD HH24:MI:SS');
END my_output;
/

这就充分发挥了重载的价值!

4 包状态及ORA-04068错误

这个问题是不论什么开发包的人都无法回避的。

包有状态?

当一个包有至少一个常量或变量声明在包级别,该包就有了状态!

当一个会话调用有状态包,PGA将包全部包级别数据存储起来。

假设一个状态包又一次编译,全部使用该包的会话在下次调用时都会抛出:ORA-04068错误。

由于存储在PGA中包级别数据已经过期了(out of date)!所以包必须再次初始化。

此外。一旦ORA-04068抛出。会话中全部状态包,比如,DBMS_OUTPUT都将标识为未初始化。

这通常意味着用户

必须断开会话又一次连接。

这个潜在的错误意味着当IT部门须要升级应用,他们须要确保全部用户已注销。

可是在7*24的互联网世界这是

不能容忍的。

所以在Oracle 11g r2中,oracle提供了基于版本号的重定义功能(Edition-Based Redefinition feature)。

具体请參考:oracle.com/technetwork/database/features/availability/edition-based-redefinition-1-133045.pdf and docs.oracle.com/cd/E11882_01/appdev.112/e10471/adfns_editions.htm

05-11 03:50