9 在网格中编辑数据#
Interactive Grid(交互式网格)让最终用户在表格形态中查看、筛选、排序、选择、复制粘贴和直接编辑多行数据。它既继承了 Interactive Report(交互式报表)的许多自助分析能力,又增加了更适合数据录入的网格行为,例如键盘导航、单元格选择、滚动分页、冻结列、行级操作,以及一次保存多行新增、修改和删除。
本章的学习重点不是“把表格变成可编辑”这么简单,而是理解 APEX 在浏览器端如何跟踪网格模型(model)中的行状态、服务器端如何用 Interactive Grid - Automatic Row Processing (DML) 保存变化,以及在集合(Collection)、弹窗编辑、局部刷新等特殊场景下,什么时候需要覆盖默认保存或调用 JavaScript API。
学习前提:建议先准备一个可练习的 APEX 应用,并能访问示例 EMP、DEPT 或等价表。若要复现集合示例,还需要能创建数据库视图和 PL/SQL package。若只阅读概念,可以先跳过建表和包代码,但要保留“网格主键必须稳定”这一核心约束。
9.1 认识交互式网格#
Interactive Grid 区域把查询结果显示为功能完整的数据网格。用户可以搜索、筛选、排序、分组、聚合、高亮、下载、调整列顺序、保存个性化报表;开发者启用编辑后,用户还可以在页面内新增、更新、复制、删除、刷新和还原行。保存方式有两类:用户点击网格自己的 Save 动作,只保存这个网格的待处理变化;或提交整个页面,让网格变化与表单项、校验和其他页面进程一起处理。
9.1.1 从一个可编辑员工网格开始#
最小可复现场景是基于 EMP 表创建一个 Interactive Grid,并在区域的 Attributes 中启用编辑。运行页面后,用户可以用鼠标或键盘进入某个可编辑单元格,例如把员工 KING 的名称改为 KINGSTON,也可以连续新增、更新或删除多行,最后一次性保存。
复现路径:在 Page Designer 中创建或打开 Employees 页面;区域类型选择 Interactive Grid,数据源指向 EMP 或等价查询;在 Attributes 中启用 Editable;确认主键列配置正确,并检查自动生成的 Interactive Grid - Automatic Row Processing (DML) 页面进程。运行页面后修改一个员工名称并保存。验收点是刷新页面后仍能看到修改结果,且浏览器控制台和 APEX 错误栈没有保存异常。
如果启用滚动分页,网格可以按需加载更多行,同时保持表头可见,适合数据量较大的浏览场景。开发者还可以精确控制哪些列可编辑、哪些行可编辑,以及没有数据时是否自动显示一行空白记录供用户录入。
9.1.2 为可编辑列选择合适控件#
可编辑网格中的一行可以理解为“横向表单”。每个可编辑列都有自己的列类型,单元格进入编辑模式时才显示对应控件。常见控件包括 Text Field、Number Field、Date Picker、Select List、Popup LOV、Switch 和 Checkbox。这样既能保持网格浏览效率,也能给用户熟悉的表单输入体验。
复现路径:在 Employees 网格中选择 HIREDATE 列,设置为 Date Picker;选择 JOB 或类似枚举列,设置为 Select List 并配置 LOV。运行页面后分别修改日期和职位。验收点是单元格显示正确控件,保存后数据库值符合预期;若 LOV 有返回值和显示值之分,应确认保存的是返回值而不是显示文本。
9.1.3 最终用户能使用哪些网格能力#
Interactive Grid 的功能入口主要来自默认工具栏、Actions 菜单、列标题菜单、键盘快捷键和直接的网格交互。开发者可以决定开放哪些能力。常见能力包括跨行搜索、限制到某列搜索、选择和重排列、调整列宽、冻结列、排序、筛选、聚合、高亮、控制中断、分组、图表、Flashback、保存报表布局、下载、邮件发送、帮助和键盘快捷键。
启用编辑后,网格有导航模式和编辑模式。导航模式用于移动焦点、选择行或单元格、打开行操作和调用菜单命令;编辑模式用于在当前单元格中显示控件。用户可双击单元格、按 Enter 或点击工具栏 Edit 进入编辑模式,按 Esc 返回导航模式。APEX 同一时间只激活一个单元格编辑器,这对性能、键盘操作和可访问性都更稳定。
网格还区分行操作与选择操作。行操作通常作用于当前行,例如新增、复制、删除、刷新、还原;选择操作作用于被选中的行或单元格范围,例如批量复制、删除、刷新、还原、Copy Down、Fill、Clear 和剪贴板操作。若业务需要批量审批、批量刷新图表、把选中主键传给另一区域或跳转页面,应该优先使用网格选择能力,而不是手写 DOM 遍历。
9.1.4 剪贴板、分页和大数据浏览#
Interactive Grid 支持复制、剪切、粘贴、Paste Insert,以及从电子表格拖放或粘贴表格数据。粘贴时,值会像用户手工输入一样进入单元格,因此动态操作和客户端校验仍可能触发。只读行、只读列或不可编辑单元格会被跳过。对 LOV 列要格外小心:粘贴值应匹配 APEX 实际保存的返回值,而不仅是用户看到的显示值。
剪贴板 API 还受浏览器和部署环境影响。某些浏览器可能要求 HTTPS、用户快捷键触发或明确授权,才允许页面读取剪贴板。正式业务中,批量粘贴前应用小样本验证 LOV、日期格式、数字格式和只读规则。
对较大结果集,网格既可以按页浏览,也可以滚动分页。滚动分页只取当前视口附近需要的行。若关闭 Show Total Count,滚动条只能表达“是否还有更多行”;若开启,APEX 会额外计算总行数并可使用虚拟滚动,滚动条能更准确反映整体数据规模,但代价是多一次计数查询。
复现路径:使用 Employees 示例数据或等价的较大数据集;在网格 Attributes 中配置分页和表头固定方式。Heading > Fixed To 可选择 Page、Region 或 None;若选择 Region,再设置 Fixed Report Height,例如 300px。验收点是滚动时表头行为符合配置,且数据按需加载而不是一次性渲染全部记录。
9.1.5 表单控件、级联行为和性能边界#
网格列控件可以参与许多表单式行为。例如级联 LOV 可依赖同一行的其他列值;动态操作可在列值变化时做校验、计算另一列、启用或禁用相关字段,或刷新依赖 LOV。这样,一行网格就像一个按需展开的小型表单。
注意:如果级联 LOV 或动态操作每改一行都要访问服务器,批量粘贴、Copy Down、Fill 和 Clear 会明显变慢。能在客户端完成的简单计算和格式检查,可以先用 JavaScript 完成;真正的权威校验和重算仍应在保存时由页面进程、包 API 或数据库约束完成。
9.1.6 在网格列中使用 Template Component#
Template Component(模板组件)适合把可复用的 HTML/SVG 片段封装成组件,让其他开发者在 Page Designer 中通过属性传值。官方示例使用 Percent Dot on Bar 模板组件,把员工薪资在全体薪资范围中的相对位置显示成一条横线上的圆点。
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 16" width="100%" height="16">
<title>#LABEL#</title>
<rect x="0" y="6.5" width="100" height="3" rx="1.5" fill="#e8e8e8"/>
<rect x="0" y="6.5" width="#PERCENT#" height="3" rx="1.5" fill="#378ADD"/>
<circle cx="#PERCENT#" cy="8" r="6" fill="white" stroke="#185FA5" stroke-width="2"/>
</svg>
viewBox="0 0 100 16" 建立的是作者使用的逻辑坐标系,width="100%" 和 height="16" 决定浏览器如何把逻辑坐标映射到实际页面。因为圆点半径为 6 且边框为 2,视觉上靠近左右边缘时容易被裁切,所以示例把圆心的有效范围设为 7 到 93,也就是 86 个可绘制单位。
复现路径:先创建 Template Component,并定义 LABEL 与 PERCENT 自定义属性。随后在 Interactive Grid 的 Columns 下新增隐藏列 PERCENT,类型使用 SQL Expression,数据类型设为 NUMBER,SQL 表达式如下:
ROUND(
7 + (
(SAL - MIN(SAL) over ())
/
NULLIF(MAX(SAL) over () - MIN(SAL) over (), 0)
*
86
)
)
这个表达式先计算当前 SAL 在最小薪资和最大薪资之间的相对位置,再乘以 86 映射到可绘制宽度,最后加 7 防止圆点贴边被裁切。若所有薪资都相同,NULLIF(..., 0) 可避免除以零。
再新增一个无数据源列作为显示列,将 Source > Type 设为 None,列类型设为 HTML Expression,表达式如下:
{with/}
LABEL := &SAL.
PERCENT := &PERCENT.
{apply PERCENT_DOT_ON_BAR/}
验收点:网格仍可编辑原始数据列;薪资范围列只负责展示;鼠标悬停时能通过 LABEL 看到传入的薪资值。正式使用时还应补充可访问性属性,并决定复制单元格时应复制显示 HTML、标签,还是底层数据值。
9.2 使用网格编辑集合数据#
APEX Collection 适合在当前会话中临时保存多行值,例如购物车、向导暂存、尚未提交的批量修改。通常 Collection 由 PL/SQL 代码读写,但也可以让用户通过 Interactive Grid 直接编辑集合成员。官方示例是一个购物车:每行包含商品代码、数量和需要日期;商品代码可以用 Select List 或 Popup LOV,日期可以用 Date Picker。
直接维护表或可更新视图时,Interactive Grid 的自动 DML 页面进程通常足够;但 Collection 不是普通表,保存时要调用 APEX_COLLECTION API。因此本节的关键是:给每个集合成员生成一个不会变化的业务主键,再用自定义 PL/SQL 保存新增、修改和删除。
9.2.1 网格主键不能随意变化#
Interactive Grid 在浏览器端维护一个客户端模型,用主键列值唯一识别每行。可编辑模型还会跟踪用户新增、修改和删除的行。用户提交页面或点击网格 Save 后,模型中的变化被发送到服务器,由页面进程逐行处理。
模型假设主键值稳定。只有一种例外:用户新增行时,浏览器先生成临时唯一键;服务器保存成功后,数据库分配的真正主键可以替换这个临时键。除此之外,已经存在的行不应在网格编辑过程中改变主键。
Collection 的 SEQ_ID 是系统分配的整数序号,添加、删除、移动或排序集合成员时都可能变化,因此不能把它当作 Interactive Grid 的稳定主键。示例做法是在集合的额外 VARCHAR2 列中保存 SYS_GUID() 生成的值,并把它暴露为网格的 ID 主键列。
C001映射为ITEM_CODEN001映射为QUANTITYD001映射为NEED_BY_DATEC002映射为稳定主键ID
9.2.2 用视图和包封装 Collection 访问#
先创建一个视图,把 APEX_COLLECTIONS 的通用列名改成业务列名。这样 Interactive Grid 的 SQL 数据源、列属性和后续代码都更容易读懂。
create or replace view shopping_cart_v as
select c001 as item_code,
c002 as id,
n001 as quantity,
d001 as need_by_date
from apex_collections
where collection_name = 'SHOPPING_CART'
再创建一个 PL/SQL 包,把清空购物车、新增项目、更新项目、删除项目这些操作封装起来。页面进程只调用业务含义明确的过程或函数,而不直接散落 APEX_COLLECTION 调用。
create or replace package shopping_cart_api is
procedure clear_cart;
function add_item(
p_item_code in varchar2,
p_quantity in number,
p_need_by_date in date)
return varchar2;
procedure remove_item(
p_item_id in varchar2);
procedure update_item(
p_item_id in varchar2,
p_item_code in varchar2,
p_quantity in number,
p_need_by_date in date);
end shopping_cart_api;
create or replace package body shopping_cart_api is
c_collection_name constant varchar2(40) := 'SHOPPING_CART';
procedure ensure_collection is
begin
if not apex_collection.collection_exists(c_collection_name) then
apex_collection.create_collection(c_collection_name);
end if;
end ensure_collection;
procedure clear_cart is
begin
ensure_collection;
apex_collection.truncate_collection(c_collection_name);
end clear_cart;
function get_seq_id(p_item_id in varchar2)
return number
is
l_ret number;
begin
for j in (
select seq_id as seq
from apex_collections
where collection_name = c_collection_name
and c002 = p_item_id
) loop
l_ret := j.seq;
end loop;
return l_ret;
end;
function add_item(
p_item_code in varchar2,
p_quantity in number,
p_need_by_date in date)
return varchar2
is
l_ret varchar2(255) := sys_guid();
begin
ensure_collection;
apex_collection.add_member(
p_collection_name => c_collection_name,
p_c001 => p_item_code,
p_c002 => l_ret,
p_n001 => p_quantity,
p_d001 => p_need_by_date);
return l_ret;
end add_item;
procedure remove_item(p_item_id in varchar2) is
l_seq_id number;
begin
ensure_collection;
l_seq_id := get_seq_id(p_item_id);
if l_seq_id is not null then
apex_collection.delete_member(
p_collection_name => c_collection_name,
p_seq => l_seq_id);
end if;
end remove_item;
procedure update_item(
p_item_id in varchar2,
p_item_code in varchar2,
p_quantity in number,
p_need_by_date in date)
is
l_seq_id number;
begin
ensure_collection;
l_seq_id := get_seq_id(p_item_id);
if l_seq_id is not null then
apex_collection.update_member(
p_collection_name => c_collection_name,
p_seq => l_seq_id,
p_c001 => p_item_code,
p_c002 => p_item_id,
p_n001 => p_quantity,
p_d001 => p_need_by_date);
end if;
end update_item;
end shopping_cart_api;
复现路径:在 SQL Workshop 或部署脚本中创建视图和包;确认当前会话可以查询 shopping_cart_v;调用 shopping_cart_api.add_item 后再查视图,应看到新行且 ID 有 GUID 值。验收点是同一个集合成员的 ID 在更新前后保持不变。
9.2.3 让网格通过 API 保存集合行#
视图和包准备好后,把 Interactive Grid 的数据源设为 SHOPPING_CART_V,并把 ID 列配置为 Primary Key。启用编辑后,Page Designer 会创建相关保存进程。可以把它重命名为 Save Shopping Cart,类型仍是 Interactive Grid - Automatic Row Processing (DML),Editable Region 指向该网格,但 Target Type 改为 PL/SQL Code。
由于购物车集合数据只属于当前用户会话,不会被其他用户同时锁定或修改,可关闭行锁和 Lost Update Protection。真正业务表的保存仍应保留并发保护。
保存进程会针对每一条新增、修改或删除的网格行执行一次。内置绑定变量 :APEX$ROW_STATUS 表示当前行状态:'C' 是创建,'U' 是更新,'D' 是删除。新增时,应把 add_item 返回的 GUID 写回 :ID,让浏览器模型拿到服务器端确认后的稳定主键。
case :APEX$ROW_STATUS
when 'C' /* Create */ then
:ID := shopping_cart_api.add_item(
p_item_code => :ITEM_CODE,
p_quantity => to_number(:QUANTITY),
p_need_by_date => to_date(:NEED_BY_DATE, 'DD-MON-YYYY'));
when 'U' /* Update */ then
shopping_cart_api.update_item(
p_item_id => :ID,
p_item_code => :ITEM_CODE,
p_quantity => to_number(:QUANTITY),
p_need_by_date => to_date(:NEED_BY_DATE, 'DD-MON-YYYY'));
when 'D' /* Delete */ then
shopping_cart_api.remove_item(
p_item_id => :ID);
end case;
实现提醒:网格中的日期或时间戳列传给 PL/SQL 时通常是 VARCHAR2,应使用与页面格式一致的 to_date 或 to_timestamp 转换;数字列使用 to_number。捕获源代码里数量转换写成 to_number('QUANTITY'),本草稿按可执行意图写为 to_number(:QUANTITY),最终合并前建议对照 Oracle 原文和本地运行结果确认。
验收点:新增一行后保存,页面不应报主键变化错误;更新数量或日期后保存,集合视图返回新值;删除行后保存,视图中对应 ID 不再存在。
9.2.4 声明式调用网格动作#
购物车页面可以关闭网格工具栏和页脚,改用普通页面按钮组织操作。例如 Save 按钮提交页面保存网格;Clear 按钮提交页面并触发一个 Invoke API 进程调用 shopping_cart_api.clear_cart;自定义 Add Item 按钮则不提交页面,而是在浏览器端触发网格内置的 selection-add-row 动作。
声明式按钮配置方式如下:
- 给 Interactive Grid 区域设置 HTML DOM ID,例如
shoppingcart。 - 把按钮 Action 设为 Defined by Dynamic Action。
- 在按钮 Custom Attributes 中写入以下属性。
data-action="[shoppingcart]selection-add-row" data-no-update="true"
方括号中是网格区域的 HTML DOM ID,后面是要调用的动作名。data-no-update="true" 表示不要让运行时动作自动覆盖按钮标签或图标。验收点是点击 Add Item 后网格出现新行,但页面不刷新;点击 Save 后才真正写入集合。
9.2.5 空购物车提示和结账处理#
默认没有行时,网格会显示 No data found。业务页面应把 Attributes 中的 When No Data Found 改成更贴近场景的文案,例如 No items in your cart。
真正结账时,可以遍历视图中的集合成员,再在单一事务中写入订单头、订单行和库存或审批记录。
for j in (
select item_code, quantity, need_by_date
from shopping_cart_v
) loop
-- Reference j.item_code, j.quantity, j.need_by_date
end loop;
复现路径:清空购物车后运行页面,确认空数据提示显示为自定义文案;新增两行并保存,再运行结账处理或测试块读取 shopping_cart_v。验收点是业务代码读取到的列名与页面列一致,不需要知道底层 C001、N001、D001 这些通用列。
9.3 理解并定制网格行为#
Interactive Grid 是一个 model-view 组件:客户端模型保存记录、元数据和变化状态;视图负责展示和编辑;动作、事件和 widget 方法让开发者调用命令或响应用户交互。工具栏按钮和菜单项本质上连接到命名动作,例如保存、刷新、进入编辑模式、添加行或打开内置对话框。
默认配置能覆盖大多数场景。需要定制时,优先使用公开的 APEX JavaScript API 和区域 Static ID,不要依赖内部 DOM 结构。初始化函数用于网格创建前的配置;运行时 API 用于网格已经存在后的选择、刷新、取值、设值和事件处理。
9.3.1 保存处理与 JavaScript 定制入口#
可编辑网格的客户端模型会跟踪 inserted、updated 和 deleted 记录。触发网格自己的 Save 动作时,只保存该网格的待处理变化且不刷新整页;提交页面时,待处理网格变化可以与其他区域、页面项、校验和页面进程一起保存。主表单加一个或多个明细网格时,常用页面提交来保证一个事务内完成整体校验和保存。
JavaScript 定制常见用途包括:调整默认视图选项、改造工具栏、配置内置动作、增加自定义动作、调用 save、refresh、edit、show-filter-dialog 等动作,响应选择变化、模式变化、保存、报表变化和视图变化,或读取模型并刷新指定行。
在 Interactive Grid 区域的 Attributes > JavaScript Initialization Function 中,可以设置创建前必须确定的选项。对嵌套选项对象,例如 defaultGridViewOptions 或 defaultModelOptions,应使用 apex.util.getNestedObject,避免覆盖 APEX 或其他代码已经设置的同级配置。
9.3.2 几个小型初始化示例#
有些需求只需改一个属性:隐藏页脚用 footer = false;允许多行选择用 multiple = true;把选中主键自动维护到隐藏页面项用 selectionStateItem = "P2_SELECTED_EMPNOS";跨分页保持选择用 persistSelection = true。
function( options ) {
const gridOptions = apex.util.getNestedObject(
options,
"defaultGridViewOptions"
);
gridOptions.selectionStateItem = "P2_SELECTED_EMPNOS";
gridOptions.multiple = true;
// gridOptions.footer = false;
// gridOptions.persistSelection = true;
return options;
}
复现路径:给 Employees 网格设置 Static ID,并在 JavaScript Initialization Function 中加入上面代码;页面上创建隐藏项 P2_SELECTED_EMPNOS;运行页面后选择多行。验收点是隐藏项保存选中行主键,且分页切换时是否保留选择与 persistSelection 配置一致。
9.3.3 精确刷新弹窗编辑后的某一行#
Interactive Grid 模型 API 提供 getRecord() 和 fetchRecords()。前者按主键在客户端模型中找记录,后者从服务器重新取回指定记录。组合起来,就能在弹窗编辑返回后只刷新被改动的行;若无法确定主键,例如新增或删除,则刷新整个区域。
function refreshGridRowOrRegion( regionHtmlDomId, primaryKey ) {
const region = apex.region( regionHtmlDomId );
if ( !region || region.type !== "InteractiveGrid" ) {
throw new Error( "The region must be an Interactive Grid" );
}
if ( primaryKey ) {
const gridView = region.call( "getViews", "grid" );
const model = gridView.model;
const record = model.getRecord( primaryKey );
if ( record ) {
model.fetchRecords( [ record ] );
return;
}
}
region.refresh();
}
复现路径:在调用页的 Function and Global Variable Declaration 中加入该函数;Employees 网格设置 Static ID 为 employees;弹窗页负责创建、编辑或删除员工。验收点是编辑已有员工并关闭弹窗后,仅对应行刷新;新增或删除时,整个网格刷新以保持行集合准确。
在调用页添加动态操作:事件选择 Dialog Closed,选择类型用 jQuery selector,目标为 body;动作类型为 Execute JavaScript Code,代码如下:
const editedEmpno = this.data && this.data.P2_EMPNO_EDITED;
refreshGridRowOrRegion( "employees", editedEmpno || null );
9.3.4 让弹窗返回被编辑主键#
为了让调用页知道该刷新哪一行,弹窗页可以维护一个隐藏项 P2_EMPNO_EDITED。编辑已有员工时,在 Pre-Rendering 阶段用 computation 把传入的 P2_EMPNO 复制到 P2_EMPNO_EDITED。
弹窗页关闭时,Close Dialog 页面进程的 Items to Return 配置为返回 P2_EMPNO_EDITED。调用页的 this.data.P2_EMPNO_EDITED 就能读到该值。
删除时不应返回被删除行的主键,因为调用页已经不能只刷新那一行。可以在关闭弹窗前增加一个类型为 Clear Session State 的页面进程,例如 Clear EMPNO_EDITED for Delete,并设置在 DELETE 按钮按下时执行,清空 P2_EMPNO_EDITED。
还可以通过服务器端条件避免用户看到数据库外键错误。例如只有当当前员工没有被其他员工作为经理引用时才显示 Delete 按钮。按钮的 Server-side Condition 可使用 No Rows Returned,SQL 如下:
select empno
from emp
where mgr = :P2_EMPNO
验收点:编辑现有员工后,调用页只刷新该员工行;新增员工或删除员工后,调用页刷新整个网格;当员工仍被其他记录引用为经理时,删除按钮不显示,用户不会走到外键约束报错。