搜索数据是企业应用的基础能力。Oracle APEX 提供多种原生组件,可以按任务场景组合出合适的搜索体验:需要按多个分类条件逐步缩小结果时,用分面搜索(Faceted Search);需要把筛选能力压缩到一个更小的界面里时,用智能筛选器(Smart Filters);需要跨多个来源统一呈现结果时,用搜索区域(Search Region)和搜索配置(Search Configuration);如果要完全控制用户输入条件的方式,则可以用页面项构建查询示例(Query by Example,QBE)。

6 增强最终用户搜索#

本章围绕“用户如何找到正确数据”展开。APEX 的搜索能力不是单一控件,而是一组互补模式:分面搜索适合把候选集逐步收窄;智能筛选器适合空间紧张、结果区需要更大宽度的页面;搜索区域适合把多个业务对象的结果合并成统一列表;页面项过滤则适合开发者自己定义每一个查询条件。

  • 分面搜索:用户从一组组分面(Facet)中选择条件,结果区域自动刷新。
  • 智能筛选器:用一个搜索框承载文本搜索、筛选器选择和筛选 Chip,适合移动端或顶部区域。
  • 统一搜索:通过共享组件中的搜索配置描述各数据源,再由搜索区域统一呈现。
  • 查询示例:用普通页面项收集条件,并在其他区域的 SQL 查询中引用这些值。

6.1 使用分面筛选缩小搜索范围#

分面搜索区域(Faceted Search Region)让用户通过多个分面组选择条件。每个分面代表一个值得筛选的业务特征,例如职位、部门、薪资范围、日期区间或类别。熟悉电商筛选的人会很容易理解这种交互:就像按内存、屏幕尺寸、价格区间筛选笔记本电脑一样,APEX 应用也可以用同样方式筛选任何业务数据。

典型布局是使用 Left Side Column 页面模板,把分面搜索区域放在 Left Column 插槽,把被过滤的结果区域放在 Body 中。结果区域可以是经典报表(Classic Report)、卡片(Cards)、地图(Map)、内容行(Content Row)、支持分面的模板组件或自定义插件。用户每次选择或取消条件时,结果会自动刷新。

图 6-1 左侧分面搜索过滤正文中的经典报表

6.1.1 定义分面#

每个分面至少要配置 LabelType 和用于过滤的 Database Column。在页面设计器中,分面看起来像页面项,命名也通常类似 P23_JOB;运行时它的值就是最终用户在对应分面组里选择的条件。

对于 Checkbox GroupRadio GroupSelect List 类型的分面,需要提供值列表(List of Values)。配置方式与普通列表型页面项相同,但多了一个实用选项:Distinct Values。启用后,APEX 会根据当前筛选条件下仍然匹配的行,从底层列中计算不同值作为可选项。

图 6-2 页面设计器中的搜索分面

6.1.2 配置范围分面#

范围分面(Range Facet)要求源列的数据类型是 NUMBERDATETIMESTAMP。每个范围选项的返回值用竖线 | 分隔下界和上界,格式为 LowValue|HighValue。如果省略其中一端,就表示该方向不设边界。例如显示值 <900 可以对应返回值 |900,表示没有下界、上界为 900。

显示值可以使用替换字符串 #G# 表示千位分隔符,使用 #D# 表示小数分隔符。日期和时间戳范围要使用规范格式 YYYYMMDDHH24MISS。例如“Before 2020”可以写成 |20191231235959;“2020 to 2024”可以写成 20200101000000|20241231235959

图 6-3 指定范围分面的区间上下界

6.1.3 多选条件的行为#

当用户在 Checkbox GroupRange 分面中选择多个值时,APEX 会返回匹配任意一个所选值的行。换句话说,同一个多选分面内部通常是 OR 关系。官方示例中,薪资范围同时选中 900-13002000-2500,结果会包含薪资落在任一范围内的员工。

图 6-4 多选分面返回匹配任一选项的行

6.1.4 计数、依赖关系和图表#

分面可以显示每个选项当前会匹配多少行,并可按匹配数量排序。对于数量为零的选项,可以选择隐藏,以免用户继续选择没有结果的条件。对于 Checkbox GroupRadio Group 分面,还可以把选项按匹配数量从高到低排列。

分面之间也可以建立依赖关系:一个分面只有在用户先选择另一个分面的值之后才显示。对于适合观察分布的分面,还可以启用图表视图,让用户通过饼图或条形图理解当前结果在各个分面值上的分布。

图 6-5 用饼图或条形图比较分面值分布

6.1.5 处理多值列#

如果某个被过滤的列在一行中可能存储多个值,可以把该分面的 Multiple Values 属性设置为 DelimiterJSON Array。启用后会出现额外设置,例如 Trim Whitespace,用于移除值开头和结尾的空格、制表符或换行符。

Filter Combination 决定多值列如何与用户选择的筛选项匹配:OR (Union) 表示包含任意一个所选值即可匹配;AND (Intersect) 表示必须同时包含所有所选值才匹配。假设 EMP.INTERESTS 用冒号保存多个兴趣:

TEXT
Art:Animation:Urban Sketching:Math

如果 P23_INTERESTS 分面使用冒号作为分隔符,并把组合方式设为 AND (Intersect),那么当用户同时选择 AnimationMath 时,这一行会匹配;只包含 Art 或只包含 Math 的员工则不会匹配。

6.1.6 在“添加筛选器”对话框中显示分面#

配置分面时,可以在属性编辑器的 Appearance 区域决定它是直接内联显示,还是放进 Add Filter 对话框。页面上有大量可选分面时,对话框更适合承载低频筛选条件,避免左侧区域过长。

运行时,用户点击 Add Filter 按钮打开模态对话框,通过顶部的 Select One 项查找要使用的分面,选择筛选值后点击 Apply 应用条件并关闭对话框。

图 6-6 从“添加筛选器”对话框应用筛选条件

6.1.7 跨列查找文本#

每个分面搜索区域默认都有一个 Search 分面,用于跨多个列查找文本。它可以显示在分面区域内部,也可以引用页面其他位置的页面项。要控制搜索范围,在该 Search 分面的 Source 组中设置 Database Columns,用逗号分隔列名。

用户输入一个或多个词并按 Enter 后,APEX 会查找配置列中包含任一搜索词的行。如果页面上还应用了其他分面条件,则结果必须同时满足这些筛选条件。官方示例中,用户输入 acccl,匹配结果包括员工名 CLARK、部门列 ACCOUNTING,以及职位列 CLERK 等。

图 6-7 用搜索分面跨列查找文本

6.1.8 显示筛选 Chip 和总行数#

当页面上有很多内联分面,或者部分分面藏在 Add Filter 对话框中时,用户需要快速知道当前哪些条件已经生效。把分面搜索区域的 Show Current Facets 设置为 Yes,即可在分面区域顶部显示筛选 Chip。每个 Chip 会显示分面标签、所选显示值,并带有可移除单个条件的 x 图标;Clear All 链接可一次清空全部条件。

如果左侧栏太窄,更常见的做法是在正文中放一个静态内容区域,把它的 HTML DOM ID 设为例如 active_facets,再把 Show Current Facets 设为 Selector,并在 Current Facets Selector 中填写 #active_facets。同理,Show Total Row Count 可以设为 Yes,也可以设为 Selector 并填写类似 #total_count_area 的选择器,把总行数显示到指定静态区域中。

图 6-8 经典报表上方显示行数和筛选 Chip

6.2 用智能筛选器为结果区腾出空间#

智能筛选器区域(Smart Filters Region)提供与分面搜索类似的筛选能力,但界面更紧凑。它适合屏幕空间有限,或者搜索结果需要占据整页宽度的页面。智能筛选器可以过滤经典报表、卡片、地图、内容行、支持分面的模板组件和自定义插件。默认外观接近一个单独搜索框。

图 6-9 紧凑的智能筛选器区域是一个搜索字段

6.2.1 定义筛选器#

每个智能筛选器也要配置 TypeLabel 和用于过滤的 Database Column。筛选器名称看起来像页面项名称,运行时值也反映用户的选择。对于 Checkbox GroupRadio Group 类型,需要定义值列表;同样可以使用 Distinct Values,由 APEX 基于当前匹配行计算可选值。

范围筛选、计数、依赖关系和多值列的配置方式与前一节分面搜索中的分面一致。计算计数时,Checkbox GroupRadio Group 筛选器也可以按匹配数量从高到低展示。

图 6-10 在页面设计器中定义多个筛选器

6.2.2 智能筛选器的用户体验#

用户进入智能筛选器搜索框时,下拉列表会显示已定义筛选器的显示名称和图标。用户选择某个筛选器后,搜索框中会出现对应 Chip,下拉列表改为显示该筛选器的可选值。如果该筛选器允许多选,用户可以继续选择多个值;每次选择后,被过滤的结果区域都会立即刷新。

图 6-11 筛选器显示名称和图标出现在搜索下拉列表中
图 6-12 从某个筛选器中选择一个或多个选项

一旦某个筛选器投入使用,它的 Chip 会留在搜索框中表示已应用条件,下拉列表则显示剩余可用筛选器。用户可以回到该 Chip 修改选择,也可以点击 Chip 上的 x 移除它。多个筛选器可以组合使用,例如再选择 Manager 并选中 BLAKE 后,结果会刷新为 BLAKE 管理且薪资位于已选范围内的员工。

图 6-13 一个筛选器已使用,其他筛选器仍可选择
图 6-14 使用智能筛选器应用多个筛选条件

用户也可以直接在搜索框中输入一个或多个词,在配置的搜索列中查找文本。搜索列由 Search 筛选器的 Database Columns 属性控制。输入过程中,下拉列表还会显示匹配的筛选选项;例如输入 cl 时,可能出现 Job: CLERKManager: CLARK。用户可以继续输入并按 Enter 搜索,也可以直接选择建议的筛选选项。

图 6-15 输入文本以跨列搜索或查找筛选器

6.2.3 配置建议 Chip#

当用户还没有应用任何筛选条件时,智能筛选器可以显示建议 Chip,引导用户从常用或热门条件开始。除必需的 Search 筛选器以外,每个额外筛选器都可以通过 Suggestions > Type 控制建议行为。

  • Dynamic:显示第一个可见筛选值;如果该筛选器计算计数,则显示出现频率最高的选项,否则显示排序后的第一个选项。
  • Static Values:使用硬编码的建议值。
  • SQL Query:用自定义查询决定建议值,例如根据当前用户 :APP_USER 或其他数据规则生成建议。
  • None:该筛选器不显示建议。

无论哪种方式,建议值都必须是该筛选器值列表中定义的返回值。区域级 Maximum Suggestion Chips 属性可以限制建议 Chip 数量;留空时最多显示 5 个,每个启用建议的筛选器一个。若某个场景中建议没有价值,可以把该属性设为 0 完全关闭建议。

图 6-16 初始可应用筛选器的建议 Chip

6.2.4 为搜索结果腾出更多空间#

如果希望把页面主体尽量留给搜索结果,可以把智能筛选器移动到导航栏。做法是把智能筛选器区域的 Parent Region 设置为 No Parent,选择 Before Navigation Bar 插槽,并把 CSS Classes 设为 w100p,让搜索框占据导航栏中的空白空间。这个布局通常搭配 Maximum Suggestion Chips0 使用效果更好。

图 6-17 导航栏中的智能筛选器区域

6.3 跨来源统一搜索结果#

用户在 Web 上搜索时,通常是在搜索框里输入几个词,查看顶部结果,再点击有希望的链接。APEX 可以用搜索区域和搜索配置复刻这种体验。搜索配置负责描述每一种可搜索数据源,搜索区域负责把一个或多个搜索配置的结果合并成统一列表。

6.3.1 定义搜索配置#

每一种需要搜索的来源都应定义一个搜索配置共享组件(Search Configuration)。它标识本地或远程数据源、可搜索列,以及哪些列承担主键、标题、描述、图标等标准角色。为了让用户点击结果后进入详情页,还要在搜索配置上设置链接目标。

根据搜索配置类型,APEX 可以对本地表、查询、APEX 列表项或 REST 数据源执行标准 LIKE 过滤,也可以使用 Oracle Text、Oracle AI Database 26ai Ubiquitous Search 做全文搜索,或使用 Oracle AI Database 26ai Vector Search 做语义相似度搜索。官方 Employees 示例使用如下 SQL 作为来源,并把 ENAMEJOBDNAMEEMPNOWORKS_FOR_ENAME 作为可搜索列:

SQL
select  e.empno,
        e.ename,
        m.ename as works_for_ename,
        d.dname,
        e.job,
        case e.job
           when 'PRESIDENT' then 'fa-badgerine'
           when 'ANALYST'   then 'fa-line-chart'
           when 'CLERK'     then 'fa-user-headset'
           when 'SALESMAN'  then 'fa-badge-dollar'
           when 'MANAGER'   then 'fa-user'
        end as icon
  from  emp e
  join  dept d
    on  e.deptno = d.deptno
  left join emp m
         on m.empno = e.mgr

配置中把 EMPNO 映射为主键,把 ENAME 映射为标题,把 JOB 映射为副标题,把 DNAME 映射为描述,并用 EMPNO 显示徽章。随后在 Icon and Display 选项卡指定图标名来自 ICON 列,再配置链接信息:用户点击 Employees 来源的搜索结果时,导航到第 4 页的员工编辑模态页,并把结果行的 EMPNO 传给 P4_EMPNO

图 6-18 把搜索配置列映射到标准插槽
图 6-19 定义搜索配置的链接目标页

同样方式也可以为 Departments 定义搜索配置:直接基于 DEPT 表,用 fa-building-o 作为每行图标,映射相关列,并设置跳转到部门编辑页的链接。

6.3.2 使用搜索配置#

在页面中添加搜索区域后,可以把一个或多个搜索配置作为搜索来源加入该区域。默认情况下,搜索区域会在结果区域上方显示内置搜索框。为了获得更灵活的布局,也可以自己创建一个页面项,例如 P27_SEARCH,把它放在希望的位置,然后在搜索区域的 Search Page Item 属性中引用它。

搜索区域还可以控制结果排序、分页行为,以及是否使用自定义布局。启用 Custom Layout 后,结果模板可以使用条件模板指令,也可以引用 TITLESUBTITLEDESCRIPTIONICONLINK 等替换值。完整可用列表以页面设计器中 Result Row Template 属性的帮助页为准。

图 6-20 使用搜索配置配置搜索区域

运行时,用户输入例如 acc 的搜索词后,Departments 和 Employees 两个来源中的匹配结果会同时出现在统一结果列表中。点击像 CLARK 这样来自员工来源的结果时,会打开相应员工详情页。

图 6-21 查找并下钻搜索结果

6.3.3 搜索结果格式化选项#

统一搜索结果的外观可以用三种方式调整:为默认搜索结果编写自定义 CSS;在搜索区域上提供自定义 Result Row Template;或者在部分或全部搜索配置上配置 Default Result Row Template。CSS 适合调整间距、字体和颜色;模板方式则提供更完整的结构控制。

如果在搜索配置上定义默认结果行模板,该模板可以引用数据源列名作为替换值,并且会自动用于该搜索配置的结果。若搜索区域自身也配置了结果行模板,则区域级模板会覆盖配置级模板,以便把所有来源格式化为统一外观。

6.3.3.1 自定义搜索区域结果行模板#

默认情况下,如果搜索配置没有指定自己的模板,搜索区域会使用内置结果行模板来格式化结果。这个模板把 HTML 标签、&NAME. 替换引用和用于条件输出的 HTML 模板指令组合在一起。例如搜索配置提供了徽章列时,渲染结果会包含显示徽章的标记;如果没有提供,相关标记就会被省略。

要改变所有搜索结果的展示方式,可以在搜索区域上指定 Result Row Template。启用 Custom Layout 后,属性帮助页会列出可以用 &NAME. 语法引用的标准替换字符串,并能查看内置结果行模板作为参考。

图 6-22 查看搜索区域结果行模板的帮助页

6.3.3.2 自定义搜索配置模板#

如果只想为某个搜索配置的结果使用特殊格式,可以在该共享组件编辑页的 Icon and Display 区域设置 Default Result Row Template。模板中可以使用 HTML 标签、数据源列的 &COLUMNNAME. 替换,以及用于条件格式化的 HTML 模板指令。

APEX 会使用搜索配置自己的默认结果行模板,除非所在搜索区域配置了覆盖所有结果的结果行模板。官方截图与上一节使用同一张帮助页示例,强调模板可用替换字符串和示例模板的位置。

图 6-23 查看搜索配置模板的帮助页

6.4 在其他区域中显示搜索结果#

如果使用搜索配置,可以通过专用的 SEARCH API 在任意区域中呈现搜索结果。但当用户通过分面搜索、智能筛选器、交互式报表或交互式网格过滤数据时,想把同一批过滤结果显示到另一个区域,就需要另一种方式:取得源区域当前过滤后的主键,再让目标区域使用这些主键查询。

6.4.1 自定义统一搜索体验#

搜索区域本身会管理搜索框,并用结果行模板显示结果。如果它的界面不符合需求,可以用 APEX_SEARCH 包中的 SEARCH 函数构建自定义搜索体验。该函数接收搜索词和搜索配置静态 ID 列表,返回一个可在 SQL FROM 子句中使用的搜索结果表;使用时用 table() 包住函数调用即可。

下面示例用页面项 P31_SEARCH 中的搜索词,同时搜索静态 ID 为 employeesdepartments 的搜索配置:

SQL
select *
  from table(
         apex_search.search(
           p_search_expression => :P31_SEARCH,
           p_search_static_ids => apex_t_varchar2('employees',
                                                  'departments')))

如果外层查询要自己加 ORDER BY,可以额外传入 p_apply_order_bys => 'N',避免函数内部做不必要的排序:

SQL
select *
  from table(
         apex_search.search(
           p_search_expression => :P31_SEARCH,
           p_search_static_ids => apex_t_varchar2('employees',
                                                  'departments'),
           p_apply_order_bys   => 'N'))
 order by title

这种方式可以用任意方式收集搜索词,并把结果显示在卡片、地图、图表、经典报表、模板组件或其他 APEX 区域中。返回结果包含与搜索区域 Result Row Template 可用替换值对应的标准列,例如 PRIMARY_KEY_1PRIMARY_KEY_2TITLESUBTITLEDESCRIPTIONBADGE。搜索配置还可以映射 3 个自定义列,结果中分别以 CUSTOM_01CUSTOM_02CUSTOM_03 引用;例如把经纬度映射到 Custom Column 1 和 2 后,可以在地图区域中使用。

6.4.2 在另一个区域中显示过滤后的数据#

要把一个区域的过滤结果显示到另一个区域,可以在目标区域的查询或 WHERE 子句中引用源区域当前结果的主键。每当源区域结果变化时,用动态操作自动刷新目标区域,两个区域就能保持同步。

官方示例中,导航栏中的智能筛选器过滤 EMP 数据,正文左侧是卡片区域,右侧六列放置地图和图表。随着用户缩小卡片结果,地图和图表也展示同一批员工数据。

图 6-24 在图表和地图中显示智能筛选后的卡片区域数据

6.4.2.1 获取过滤后数据的主键#

可以创建一个辅助函数,返回某个过滤区域当前结果的主键列表。下面的 region_pks 函数接收源区域的静态 ID;如果主键列名不是默认的 ID,则通过可选参数 p_pk_column_name 传入列名。函数返回 apex_t_number 管道化列表。

函数内部用 APEX_REGION.OPEN_QUERY_CONTEXT() 打开源区域当前过滤后的查询上下文,再用 APEX_EXEC.NEXT_ROW() 遍历结果,用 APEX_EXEC.GET_NUMBER() 取出主键并逐行管道返回。

PLSQL
create or replace function region_pks(
    p_region_static_id in varchar2,
    p_pk_column_name   in varchar2 default 'ID')
return apex_t_number
pipelined
is
    c_page_id constant number := V('APP_PAGE_ID');
    l_context          apex_exec.t_context;
begin
    l_context := apex_region.open_query_context(
                    p_page_id   => c_page_id,
                    p_region_id => apex_region.get_id(
                                     p_page_id       => c_page_id,
                                     p_dom_static_id => p_region_static_id));
    while apex_exec.next_row(l_context) loop
        pipe row (apex_exec.get_number(l_context, p_pk_column_name));
    end loop;
    apex_exec.close(l_context);
    return;
exception
    when no_data_needed then
        apex_exec.close(l_context);
        return;
end region_pks;

6.4.2.2 使用过滤后数据的主键#

在任意 APEX 区域的数据源查询中,可以使用 SQL 的 table() 操作符读取管道化函数返回的行。下面示例用 WITH 子句把 region_pks() 返回的源区域主键命名为 result_pks,再与 emp 表按 empno 连接。因为左右两边列名相同,using (empno)on emp.empno = result_pks.empno 更简洁。

SQL
with result_pks as (
    select column_value as empno
      from table(region_pks('employees','EMPNO'))
)
select ename,
       sal
from result_pks
join emp e using (empno)

如果只需要在目标区域的 WHERE 条件中引用主键列表,也可以写成:

SQL
empno in (select column_value
            from table(region_pks('employees','EMPNO')))

6.4.2.3 刷新目标区域#

为了让源区域和目标区域保持同步,需要在源区域上定义动态操作,在源区域结果刷新时触发。对于经典报表区域,事件是 After Refresh;对于卡片区域,使用 Page Change (Cards)。在 True Actions 列表中添加 Refresh 动作,目标指向要联动刷新的地图、图表或其他区域。

6.5 使用页面项实现查询示例#

页面项不仅用于数据录入,也适合做查询示例(Query by Example,QBE)条件。用户输入值或从列表中选择值后,同一页面上的另一个区域可以在 WHERE 子句中引用这些页面项值,从而过滤数据。由于 QBE 页面项会频繁改变值,还应考虑关闭默认的未保存更改警告,以免用户误以为自己修改了业务数据。

6.5.1 使用空值显示文本#

对于 Select ListRadio GroupPopup LOV,可以选择在列表中加入一个代表空值的选项,并为它设置自定义标签。这样能帮助用户理解“不选择具体值”在查询条件中的含义。

例如,一个用于按部门过滤员工的可选 Select List 页面项,可以把 Null Display Value 设为 <All>。用户看到这个标签时,就能理解如果不选择具体部门,结果会显示所有部门的员工。

图 6-25 在选择列表中用自定义标签表示空值

6.5.2 按页面项的值过滤#

任何页面项的值都可以作为同页另一个区域的查询过滤条件。多数页面项类型,包括单选的列表型页面项,只保存一个值,也可能为空。假设 QBE 页面项是名为 P5_SELECTED_DEPARTMENT 的 Select List,可以在另一个区域的 WHERE 子句中直接引用它:

SQL
DEPTNO = :P5_SELECTED_DEPARTMENT

如果该页面项可以为空,过滤条件需要显式处理空值:

SQL
/*  Either no department is selected,
 or match the selected department */
(   :P5_SELECTED_DEPARTMENT IS NULL
 OR DEPTNO = :P5_SELECTED_DEPARTMENT)

注意:当区域数据源把页面项作为绑定变量引用时,必须把该页面项列入区域的 Page Items to Submit 属性。填写时不要带冒号,多个页面项用逗号分隔。如果忘记设置,区域刷新时绑定变量会被当作空值,查询可能返回空结果。

6.5.3 按多选页面项过滤#

当 QBE 页面项允许多选时,过滤通常要用带子查询的 IN 条件。默认情况下,多选页面项的值会把用户所选返回值用冒号 : 拼接保存。假设页面项名为 P5_SELECTED_DEPARTMENTS,可使用 APEX_STRING.SPLIT() 拆分后过滤:

SQL
DEPTNO IN (SELECT COLUMN_VALUE
             FROM APEX_STRING.SPLIT(:P5_SELECTED_DEPARTMENTS,':'))

如果使用了不同分隔符,把它作为 APEX_STRING.SPLIT() 的第二个参数传入即可。如果该多选页面项是可选条件,也要处理空值:

SQL
/* Either no department is selected,
or match the selected departments */
(   :P5_SELECTED_DEPARTMENTS IS NULL
 OR DEPTNO IN (SELECT COLUMN_VALUE
                 FROM APEX_STRING.SPLIT(:P5_SELECTED_DEPARTMENTS,':')))

注意:这里同样要把 P5_SELECTED_DEPARTMENTS 加入目标区域的 Page Items to Submit,否则刷新时绑定变量可能为空。

6.5.4 查询示例的最佳方式#

QBE 的 WHERE 条件有两种常见写法。一种更容易理解,另一种性能更好。简单写法是先判断页面项值是否为空,再用 OR 条件决定是否按列过滤:

SQL
/* Either no department is selected,
or match the selected department */
(   :P5_SELECTED_DEPARTMENT IS NULL
 OR DEPTNO = :P5_SELECTED_DEPARTMENT)

数据库首次处理查询时会参考绑定变量值来选择索引和执行计划,并在后续执行同一 SQL 语句时复用该计划,即使绑定变量值已经变化。因此,包含大量这种 OR 条件的通用 SQL 不一定总能使用最适合当前过滤条件的索引。

性能更好的策略是:只有当对应页面项不为空时,才把该过滤条件加入 SQL;为空时则不加入该谓词。APEX 的区域数据源可以选择 Function Body Returning SQL,用 PL/SQL 动态拼接并返回最终 SQL。下面示例只在 P5_SELECTED_DEPARTMENT 不为空时加入部门过滤:

PLSQL
return q'~
SELECT EMPNO, ENAME, SAL, DEPTNO, HIREDATE
  FROM EMP
 WHERE 1=1
~'
|| CASE WHEN :P5_SELECTED_DEPARTMENT IS NOT NULL THEN
q'~
AND DEPTNO = :P5_SELECTED_DEPARTMENT
~' END;

这里使用 PL/SQL 的多行字符串语法 q'~ ... ~',可读性更好,也避免手工转义单引号。双竖线 || 是 Oracle SQL 的字符串连接运算符。CASE 表达式在没有满足条件时返回空值,而字符串连接空值不会改变原字符串。

运行时,APEX 引擎会用一个外层函数包住你在属性编辑器中输入的函数体:

PLSQL
function x
return varchar2
is
begin
   /* Your Function Body Here */
end x;

P5_SELECTED_DEPARTMENT 为空时,数据库实际看到的查询是:

SQL
SELECT EMPNO, ENAME, SAL, DEPTNO, HIREDATE
  FROM EMP
 WHERE 1=1

当它有值时,数据库执行的查询是:

SQL
SELECT EMPNO, ENAME, SAL, DEPTNO, HIREDATE
  FROM EMP
 WHERE 1=1
AND DEPTNO = :P5_SELECTED_DEPARTMENT

同样方式可以按多个页面项有选择地添加多个条件。使用恒真的 WHERE 1=1 可以保证无论追加多少条件,SQL 语法都成立,并且每个条件都能统一以 AND 开头:

PLSQL
return q'~
SELECT EMPNO, ENAME, SAL, DEPTNO, HIREDATE
  FROM EMP
 WHERE 1=1
~'
|| CASE WHEN :P5_SELECTED_DEPARTMENT IS NOT NULL THEN
q'~
AND DEPTNO = :P5_SELECTED_DEPARTMENT
~' END
|| CASE WHEN :P5_ENAME_CONTAINS IS NOT NULL THEN
q'~
AND INSTR(UPPER(ENAME), UPPER(:P5_ENAME_CONTAINS)) > 0
~' END
|| CASE WHEN :P5_MAX_SALARY IS NOT NULL THEN
q'~
AND SAL <= :P5_MAX_SALARY
~' END;

为了提升可维护性,可以把 SQL 构建逻辑放进 PL/SQL 包函数。假设有一个 UTILS 包,其中定义了 p5_employees_query,区域的 Function Body Returning SQL 就可以简化为:

PLSQL
return utils.p5_employees_query(:P5_SELECTED_DEPARTMENT,
                                :P5_ENAME_CONTAINS,
                                :P5_MAX_SALARY);

包函数本身可以写成:

PLSQL
function p5_employees_query(
   p_selected_deptno in number,
   p_ename_contains  in varchar2,
   p_max_salary      in number)
   return               varchar2
is
begin
   return q'~
   SELECT EMPNO, ENAME, SAL, DEPTNO, HIREDATE
     FROM EMP
    WHERE 1=1
   ~'
   || CASE WHEN p_selected_deptno IS NOT NULL THEN
   q'~
   AND DEPTNO = :P5_SELECTED_DEPARTMENT
   ~' END
   || CASE WHEN p_ename_contains IS NOT NULL THEN
   q'~
   AND INSTR(UPPER(ENAME), UPPER(:P5_ENAME_CONTAINS)) > 0
   ~' END
   || CASE WHEN p_max_salary IS NOT NULL THEN
   q'~
   AND SAL <= :P5_MAX_SALARY
   ~' END;
end p5_employees_query;

注意:只要函数体返回的 SQL 或 WHERE 子句中可能以绑定变量形式引用页面项,就要把这些页面项列入区域的 Page Items to Submit。另外,Page Designer 检查 Function Body Returning SQL 的语法时,所有绑定变量都会按空值求值,因此函数在这种情况下也必须返回语法有效的 SELECT 语句。

6.5.5 关闭未保存更改警告#

QBE 页面项本来就会随着用户筛选而改变值。如果用户只是筛选结果,却在离开页面时看到“有未保存更改”的警告,容易误以为自己修改了需要保存的业务数据。对于用作查询条件的页面项,应根据场景关闭 Warn on Unsaved Changes

该设置既可以在页面级统一配置,也可以按具体页面项单独配置。判断原则是:如果页面项代表的是临时筛选条件,而不是需要提交保存的数据,就不应让它触发离页未保存警告。

图 6-26 查询示例场景中的未保存更改警告可能让用户困惑