背景描述
在开发Oracle forms的过程中,唯一性验证及重复性验证时是最基本的form验证功能之一了。通常情况下,通过为基表创建唯一性索引的方式来达到唯一性验证已经可以满足日常开发的要求。但是,不免会有特殊的行为和需求唯一性索引的方式还是搞不定的,比如一条记录中的两个字段的值取自相同的数据来源,但是他们彼此之间以及记录与记录之间都不能允许有重复。
幸运的是,对于特殊的情况,开发者们也有很多解决方案加以应对,比如利用PL/SQL的内建数组来存储每一次录入的需要做验证的值,在验证时迭代所有数组中的值去做重复性验证。甚至你也可以利用字符串来存取单一的一个值的集合来做同样的事情,虽然我本人并不推荐使用这种方式;) 。
总结起来大概有这么几种:
- 使用PL/SQL数组 (缺点:与form结合性不好)
- 使用字符串 (操作起来复杂,不宜使用,验证多列时尤为明显)
- 使用FIRST_RECORD, LOOP, 循环界面记录 (记录行多时速度慢,效率低,用户视觉体验不佳:因为光标一直在动。。。)
如果还有请帮我补充;)
记录组(Record Group)
在开发实践中我们发现,在Oracle forms内部已经提供了一种内建的类似于数组的对象Record Group,我想提起他来大家肯定都不陌生~ 是啊,这有什么好惊讶的?我们在创建LOV的时候不是经常用到它吗。。。但是,Oracle也提供了一系列的内建API来帮助我们动态创建、删除和操作记录组以及记录组中的元素,而且可以以二维的形式存储多行数据。因此这个记录组对象就可以以编程的方式为我们所用。
相比其他的方式,我认为使用记录组的方式有如下优点:
- Form内建支持,可以与Oracle forms有机结合
- 提供简洁明了的API,便于使用
- 记录组的行号的运作机制与数据块上的 record number时完全相同的,便于同步界面上的记录
- 相比其他内建方法,速度快,效率也比较高
如果还有请帮我补充;)
API简介
下面就简要介绍一下能够对我们的验证所用的API函数定义,详细的说明和文档请参考Oracle Forms Builder的帮助文档。
-- FUNCTION
CREATE_GROUP();
-- 根据给定的名称在内存中创建一个非查询类型的记录组,函数返回创建的记录组的句柄。(句柄类型为RecordGroup)
-- PROCEDURE
DELETE_GROUP();
-- 从内存中删除已创建的记录组。
-- FUNCTION
ADD_GROUP_COLUMN();
-- 为给定的记录组创建制定类型的列(类似于创建数据库表时初始化的字段属性一样),共有CHAR_COLUMN, DATE_COLUMN和NUMBER_COLUMN三种列类型
-- PROCEDURE
ADD_GROUP_ROW();
DELETE_GROUP_ROW();
-- 为指定的记录组新建/删除一个记录行
-- FUNCTION / PROCEDURE
SET_GROUP_<XXX>_CELL();
GET_GROUP_<XXX>_CELL();
-- 设置/获取指定记录组的指定记录行上的某一个字段值。<XXX>一共有三种: CHAR, DATE和NUMBER
-- FUNCTION
GET_GROUP_ROW_COUNT();
-- 返回给定记录组中所拥有的行数
-- FUNCTION
GET_GROUP_RECORD_NUMBER();
-- 返回包含特定值的记录行中的第一个行号,如果没有在任何记录行上找到该值,则返回0
一个简单的例子
下面就举一个简单的例子来说明这些API如何使用,图我就不上了,默认你们对form开发已经很熟悉啦,:)
假设form中有一个多行数据块REPLIST,现在我们要对该数据块的REPORT_ID字段做界面唯一性验证。
1. 向FORM级别的触发器WHEN-NEW-FORM-INSTANCE加入如下代码:
DECLARE
rg_name VARCHAR2(30) :='RG_REPORT_ID';
rg_id RecordGroup;
rc_id GroupColumn;
BEGIN
-- 查找内存中是否已创建具有相同名称的记录组
rg_id := Find_Group(rg_name);
-- 如果有,从内存中删除
IFNOT id_null(rg_id) THEN
Delete_Group(rg_name);
ENDIF;
-- 创建记录组
rg_id := Create_Group('RG_REPORT_ID');
-- 为记录组添加列
rc_id := Add_Group_Column(rg_id, 'REPORT_ID', CHAR_COLUMN, 100);
END;
-- 转向目标数据块
GO_BLOCK('REPLIST');
-- 执行查询
EXECUTE_QUERY;
2. 向数据块REPLIST添加块级触发器PRE-QUERY,添加代码如下:
DECLARE
rg_name VARCHAR2(30) :='RG_REPORT_ID';
BEGIN
-- 删除记录组中的所有行
Delete_Group_Row(rg_name, ALL_ROWS);
END;
3. 向数据块REPLIST添加块级触发器WHEN-REMOVE-RECORD,添加代码如下:
DECLARE
rg_name VARCHAR2(30) :='RG_REPORT_ID';
BEGIN
-- 当数据块中某一条记录被删除后,其对应记录组中的记录也需要被删除
Delete_Group_Row(rg_name, :system.trigger_record);
END
4. 向数据块REPLIST添加块级触发器WHEN-CREATE-RECORD,添加代码如下:
DECLARE
rg_name VARCHAR2(30) :='RG_REPORT_ID';
BEGIN
-- 当用户创建记录时,也想记录组中添加相应一行
-- 注意:该触发器会在form创建初期调用两次,显然那时
-- 并不是我们创建记录的时候,故使用:system.block_status != 'NEW'这一句来过滤掉那两次触发
IF :system.block_status !='NEW'THEN
Add_Group_Row(rg_name, :system.trigger_record);
ENDIF;
END;
5. 向数据块REPLIST添加块级触发器POST-QUERY,添加代码如下:
DECLARE
rg_name VARCHAR2(30) :='RG_REPORT_ID';
BEGIN
-- 创建新行
Add_Group_Row(rg_name, :system.trigger_record);
-- 为该行更新字段值
Set_Group_Char_Cell(rg_name ||'.REPORT_ID', :system.trigger_record, :replist.report_id);
END;
6. 向数据块REPLIST的REPORT_ID项添加ITEM级触发器WHEN-VALIDATE-ITEM,添加代码如下:
DECLARE
rg_name VARCHAR2(30) :='RG_REPORT_ID';
n_row_number pls_integer;
BEGIN
IF ( :REPLIST.report_id ISNOTNULL ) THEN
-- 检查当前值是否有重复,如果有,返回找到的第一条记录的行号
n_row_number := Get_Group_Record_Number(rg_name ||'.REPORT_ID', :replist.report_id);
-- 如果有重复,且不是本身,提示信息,并终止当前操作
IF n_row_number >0AND n_row_number != :system.trigger_record THEN
FND_MESSAGE.DEBUG('Duplicated!');
RAISE form_trigger_failure;
ELSE
-- 若未发现重复,则像记录组中更新值
Set_Group_Char_Cell(rg_name ||'.REPORT_ID', :system.triiger_record, :replist.report_id);
ENDIF;
ENDIF;
END;
OK,大功告成了,界面上的验证部分就此告一段落,另外我们还要结合界面和数据库两个层面进行充分的验证,因为并不是所有的记录都显示在了界面上。
程序包zz_record_group
虽然内建的API已经很简洁了,但是某些操作为了简化API和验证需要在每次调用api之前做,很麻烦,故而本人干脆直接写了一个方便同学们(包括我自己)易于使用的通用程序包ZZ_RECORD_GROUP,该程序包有如下特点:
- 对记录行的创建和删除做了封装,简化了操作
- 对重复值查找提供了更方便的VALUE_EXISTS函数,分别为字符类型、数字类型和日期类型重载了三次
- 对单元格(cell)的修改操作改为set_value方便使用
- 以library形式发布,使用时直接attach到本机form上即可
我已经将它发布到了github上了,有兴趣的同学可以去瞧瞧;)
传送门:https://github.com/eliu/zz_record_group
感谢以下文章对笔者的启发,Thanks!
http://sites.google.com/site/craigsoraclestuff/oracle-forms---how-to-s/oracle-forms
Enjoy!