包和嵌套子程序

本章节主要介绍在 DM 数据库中包和嵌套子程序的处理方式。

适用场景

软件 版本
操作系统 Redhat 7 及以上版本
DM 数据库 DM 8.0 及以上版本
CPU 架构 x86、ARM、龙芯、飞腾等国内外主流 CPU

概述

  • 当用户执行包中的一个程序时,该程序运行在与其所有者相同的权限下。
  • 将逻辑上相关联的 DMSQL 类型、变量、常量、子程序、游标、异常处理等封装在一起。
  • 每个包是以独立的小型应用程序。具备模块性,可复用性。简化应用开发,隐藏实现细节,公开的变量在整个会话期间为所有的子程序所共享。
  • 模块中有 100 个函数和过程,对外只公开必要需要提供几个函数。对使用者更加的友好。
  • 定义清楚模块和模块之间的调用接口,减弱模块与模块之间的耦合性。
  • 常见的程序开发语言中都有封装、打包的概念。DMSQL 是对常规 SQL 的扩展,自然也存在包。理念上和其他高级语言是一致的。
  • 可以按包为单位授予相应的权限给用户,而不是给包中的每个对象逐个授权。

定义

  • 包分包头和包体两个部分,定义的时候先定义包头,后定义包体,其他的应用程序或者模块调用你定义的包的时候,看到的都是包头中的内容,实现功能的细节是被隐藏的,外面调用的程序看不到。包头中不要定义不想暴露在外面的东西。
  • 改变包头中的变量,其他的程序也能够看的到。变量在几个程序或模块之间传递更加方便。
  • 匿名块每次使用都需要编译。包定义后,第一次调用后被加载到内存,第二次调用可以直接去内存中拿到。
  • 包头:只有 declare 部分,只需要定义接口。具体实现部分交给包体可以只定义常量和变量,如果定义了游标、子程序、则一定要在包体中实现。所以只有 declare 部分。可实现子程序的重载。普通的 shecma 级别的函数是无法实现重载的。
  • 包体:包含包头中未定义的常量和变量、游标和子程序,但是在包外无法调用它们。包头里面没有的 declare 和 define,实现私有性。

语法

包头:

CREATE OR REPLACE PACKAGE <[schema].package_name> IS <declare_section> END [package_name];

包体:

CREATE OR REPLACE PACKAGE BODY  <[schema].package_name> IS <declare_section> 
[initialize_section] END [package_name];

编译:

包头被修改后重新编译,对应的包体会变成 invalid,需要对应修改后重新编译,才能变成 valid. 达梦不会自动比对包体是否符合包头的改变

ALTER PACKAGE <[schema].package_name> COMPILE PACKAGE;

ALTER PACKAGE <[schema].package_name> COMPILE SPECIFICATION; --ORACLE

ALTER PACKAGE <[schema].package_name> COMPILE BODY; --ORACLE

如果只是改变了包体,包头并没有变,则使用 BODY 关键字。 

删除包:

DROP PACKAGE [IF EXISTS] <[schema].package_name>;

示例

实现一个求三角形面积和周长的包。包定义如下:

CREATE OR REPLACE PACKAGE triangle AS
FUNCTION calcAreaByAllSides(sidelist VARCHAR(24)) RETURN NUMBER;
FUNCTION calcAreaByAllSides(a NUMBER, b NUMBER, c NUMBER) RETURN NUMBER;
FUNCTION calcAreaByTwoSides_Ang(a NUMBER, b NUMBER, ang NUMBER) RETURN NUMBER;
FUNCTION calcGirthByTwoSides_Ang(a NUMBER, b NUMBER, ang NUMBER) RETURN NUMBER;
PROCEDURE calcTriangleAreaGirth(a NUMBER, b NUMBER, ang NUMBER);
END;
/

包实现如下:

CREATE OR REPLACE PACKAGE BODY triangle IS
FUNCTION calcAreaByAllSides(a NUMBER, b NUMBER, c NUMBER) RETURN NUMBER
IS
p NUMBER := (a+b+c)/2;
s NUMBER;
BEGIN
  s := ROUND(SQRT(p*(p-a)*(p-b)*(p-c)), 2);
  RETURN s;
END;

FUNCTION calcAreaByTwoSides_Ang(a NUMBER, b NUMBER, ang NUMBER) RETURN NUMBER
IS
rad NUMBER; --radian
s NUMBER; --Area
BEGIN
  rad := ang/180*(asin(1)*2); --pi=arcsin(1)*2
  s := ROUND(a*b*sin(rad)/2, 2);
  RETURN s;
END;

FUNCTION calcGirthByTwoSides_Ang(a NUMBER, b NUMBER, ang NUMBER) RETURN NUMBER
IS
rad NUMBER := 0;
c NUMBER := 0;
g NUMBER := 0;
BEGIN
  rad := ang/180*(asin(1)*2); 
  c := SQRT(POWER(a,2)+ POWER(b,2)-2*a*b*cos(rad));
  g := ROUND(a+b+c,2);
  RETURN g;
END;

PROCEDURE calcTriangleAreaGirth(a NUMBER, b NUMBER, ang NUMBER)
IS
BEGIN
  dbms_output.put_line('Area:' || to_char(calcAreaByTwoSides_Ang(a, b, ang)) || ' 
  Girth:' || to_char(calcGirthByTwoSides_Ang(a, b, ang)));
END;

FUNCTION calcAreaByAllSides(sidelist VARCHAR(24)) RETURN NUMBER
IS
a NUMBER := TO_NUMBER(regexp_substr(sidelist, '\d+', 1, 1));
b NUMBER := TO_NUMBER(regexp_substr(sidelist, '\d+', 1, 2));
c NUMBER := TO_NUMBER(regexp_substr(sidelist, '\d+', 1, 3));
BEGIN
  RETURN calcAreaByAllSides(a, b, c);
END;

END;
/

包的实例化和初始化

当一个回话引用到一个包时,数据库会为会话实例化一个包,每一个引用包的会话都拥有自己的包实例。数据库实例化一个包时,会对其进行初始化操作。

  • 包的公共的常量将被赋予一个初始值
  • 分配初始化值给那些在包头中声明的公共变量
  • 执行包体的初始化部分
  • 声明中指定的公共变量赋值
CREATE OR REPLACE PACKAGE pkg01 
AS
  cur_dt DATETIME;
  ccc CONSTANT VARCHAR(12) := 'Hello!';
  FUNCTION GetLocalDT() RETURN NUMBER;
END;
/

CREATE OR REPLACE PACKAGE BODY pkg01 
IS

  
FUNCTION GetLocalDT() RETURN NUMBER
IS
BEGIN
  RETURN 90;
END;

BEGIN
  SELECT SYSDATE() INTO cur_dt FROM DUAL;
END;
/


CREATE OR REPLACE PACKAGE pkg02 AS
FUNCTION GetVar() RETURN VARCHAR(120);
END;
/

CREATE OR REPLACE PACKAGE BODY pkg02 IS

FUNCTION GetVar() RETURN VARCHAR(128)
IS
BEGIN
  RETURN 'Public var is ' || to_char(pkg01.cur_dt, 'yyyy-mm-dd hh24:mi:ss') || ',Public constant is ' || pkg01.ccc;
END;

END;
/

--每个不同的会话执行下面的过程,可以到不同的时间,并这个等到的时间在整个会话存在期间保持不变
BEGIN
  PRINT pkg02.GetVar();
END;
/

编写指南

  • 熟悉并使用达梦提供的包,勿重复造车轮。例如达梦提供的随机数生成函数包,性能不如 DM 提供的原生包。
  • 尽可能复用现有代码,编写包中的函数尽可能通用。
  • 先设计包头,再开发包体。
  • 包头只定义外部必须要调用的变量、子程序等,该隐藏的细节尽量隐藏,只暴露应该暴露出来的。这样做的好处可以防止其他的开发者基于你的实现细节开发出不安全的代码。

可以减少包需要重新编译的次数。假如一个包的包头部分发生改变,那么你必须重新编译其他的包中的子程序,因为他们都调用了该包中公共子程序。

  • 在包头声明公开游标后,最好在包体中定义。这样可以对包用户隐藏游标对应的查询,并且改变这这些查询,不会改变游标的定义。
CREATE PACKAGE emp_manage AS
  CURSOR c1 RETURN employees%ROWTYPE;  -- Declare cursor
END emp_manage;
/
CREATE PACKAGE BODY emp_manage AS
  CURSOR c1 RETURN employees%ROWTYPE IS
    SELECT * FROM employees WHERE salary > 2500;  -- Define cursor
END emp_manage;
/
  • 在包体的初始化部分为变量赋予初值,而不是在包头声明的时候赋予初值。

初始值可以通过更加复杂的计算得到、处理起来更加方便和规范。如果计算得到初始值的过程发生了异常,可以被异常处理器捕获并处理,使得代码更加的健壮。

嵌套子程序

  • 举例

将一个子程序定义在一个 PL/SQL 块中,称之为嵌套子程序。函数和过程是 schema 级别的子程序,定义中包中的函数和过程是包级别的子程序,都不是嵌套子程序。

下面是一个简单的嵌套子程序并实现重载的例子:

--写在声明部分的嵌套子程序
DECLARE
  FUNCTION test(v1 NUMBER) RETURN NUMBER
  IS 
  BEGIN
    RETURN v1*10;
  END;
  
  FUNCTION test(v1 varchar(12)) RETURN NUMBER
  IS 
  BEGIN
    RETURN to_number(v1) + 10;
  END;
BEGIN
  DBMS_OUTPUT.PUT_LINE(test(10));
  DBMS_OUTPUT.PUT_LINE(test('10'));
END;
/

当在一个函数或者过程中需要反复调用某个功能,则可以把这个功能封装到一个嵌套子程序中。只需要修改一出代码,就可以应用到所有调用的地方。没有必要把定义成一个独立的存储过程和函数,也没有必要将其保存到数据字典中。

  • 前置声明
DECLARE
--先声明 pro1,否则 pro2 无法编译通过
PROCEDURE pro1(n1 NUMBER);

PROCEDURE pro2(n2 NUMBER) IS
BEGIN
  pro1(n2);
END;

PROCEDURE pro1(n1 NUMBER) IS
BEGIN
  pro2(n1);
END;

BEGIN
  NULL;
END;
/

参考文档

更多 SQL 语言使用说明,请参考《DM_SQL 语言使用手册》,手册位于数据库安装路径 /dmdbms/doc 文件夹下。如有其他问题,请在社区内咨询。

微信扫码
分享文档
扫一扫
联系客服