从设计到实践全面了解 Oracle ADF 应用程序
作者:Chris Muir
和 Penny Cookson
第 3 章 —“应该具备”需求:创建一个大众化应用程序
年 5 月发表
单击此处查看“从设计到实践全面了解 Oracle ADF 应用程序”描述和目录。
在本章中,您将了解如何构建 应该具备 需求。我们将向应用程序添加一些额外特性,包括在用户接受我们的条款和条件后记录用户 IP 地址以及增强搜索应用程序的功能。
我们的需求可以总结为以下几点:
记录用户的 IP 地址以及用户接受还是拒绝条款和条件。
在用户接受条款和条件后仅启用搜索字段。
如返回多条记录,隐藏结果。
将搜索条件转换为大写并删除运货单编号中的连字符。
我们将反向进行解决,从最简单的要求开始。
将搜索条件转换为大写并删除运货单编号中的连字符。
在数据库中,数据存储的格式通常与用户输入的格式不同。通常,人名以大写形式存储在数据库中,然而,我们允许客户以大小写混合的名称进行搜索。
Oracle ADF 业务组件提供了一系列机制,用以在搜索条件(我们在 Oracle ADF 业务组件中称其为 绑定变量)到达数据库前对其进行拦截。最简单的机制是修改视图对象查询以进行转换。例如,考虑我们原有视图对象中的已修改查询,如下所示:
与往常一样,修改 Oracle ADF 业务组件时,最好在 Business Component Browser 中测试更改。从测试中我们可以看到,现在输入的是小写的数据以及带有连字符的运货单编号,而应用程序仍然返回了一个结果。
还有一种修改绑定变量的机制,即,底层 ViewObjectImpl 中的 bindParametersForCollection() 方法,它将充当阻塞点方法,用于在针对视图对象应用绑定变量前查看并修改视图对象绑定变量。由于我们之前有一个简单的解决方案,因此我们将继续使用该方法。稍后,我们将介绍 bindParametersForCollection() 解决方案,因为在我们需要将搜索条件记录到数据库时会证明该解决方案很有用。
如返回多条记录,隐藏结果。
该要求非常奇怪;您可能以为我们希望显示页面中视图对象的所有结果。在本示例中,基础表没有基于搜索条件允许重复记录的主键或唯一键。如果显示多条记录,则会有一个隐私要求,客户不应能够看到其他客户的数据。作为 Oracle ADF 编程人员,我们不总是拥有修复底层数据问题的权限或控制力。我们只需使用现有的条件进行工作。
通过查看底层数据,可以看出尚未考虑对这些需求:
Lodged With
First Name
Last Name
Waybill #
Sender
Location
Status
Update
WLYP
CHRIS
MUIR
ABC1234
M.ALAN
PERTH
TRANSIT
23/02/
ACME
JOE
DOE
DDD4433
A.SMITH
SYDNEY
DELIVERED
21/02/
ACME
JOE
DOE
DDD4433
A.SMITH
SYDNEY
TRANSIT
20/02/
WLYP
CHRIS
MUIR
ABC1234
K.BAKER
PERTH
DELIVERED
07/01/
在我们的搜索字段中,如果输入 ACME-JOE-DOE-DDD4433,系统实际上将返回两条记录(上表中红色高亮显示)— 一个显示包裹运输中,另一个显示已运抵。这种情况下,我们的安全性约束(如果返回多条记录,用户不应看到结果)会产生问题。我们发现了一个隐藏要求,即,针对相同的 Lodged With、First Name、Last Name、Waybill # 和 Sender 的搜索,我们应返回最新记录。
那么,我们如何解决这个问题并确保仅显示特定包裹的最新状态记录?同样,由于 SQL 查询的强大功能,我们可以使用子查询修改视图对象,从而仅返回每个包裹的最新记录,如下所示:
同样,在 Business Component Browser 中进行测试,我们看到对 ACME-JOE-DOE-DDD4433 的查询仅返回了一条记录,但是对 WLYP-CHRIS-MUIR-ABC1234 的查询返回了两条记录,这正是我们所需的结果。
如果对 WLYP-CHRIS-MUIR-ABC1234 的搜索返回两条记录,我们不能对该数据进行任何整理操作,所以当我们的 Web 页面上出现这种情况时,我们只能不显示结果,而请客户给我们打电话进行咨询。
对于 SearchPage.jspx,在视图对象中将其从 Component Palette 拖到我们的页面中,结果是 Oracle JDeveloper 在后台为我们创建了一个包含多个绑定的页面定义文件:
其中一个绑定是迭代器 ParcelsView1Iterator,它包含一个属性 estimateRowCount,我们可以通过 JavaServer Faces 表达式语言 (EL) 表达式在 Web 页面中访问该属性。该属性将返回记录的数量返回到迭代器并且非常适用于本例,因为我们希望知道返回记录的数量。对于页面上的每个结果字段,我们可以按照如下所示设置其 rendered 属性:
为进行调试,我们将在结果字段前放置一个 outputText,以文本形式输出返回记录的数量:
value="Numbers of rows: #{bindings.ParcelsView1Iterator.estimatedRowCount}"/>
您将看到现在运行 Web 页面,结果字段将不会在初始时显示:
在我们输入搜索条件 WLYP-CHRIS-MUIR-ABC1234 后,我们应该看到 outputText 显示已返回两条记录并且结果字段依然保持隐藏状态:
然而,如果我们输入搜索条件 ACME-JOE-DOE-DDD4433,我们将得到:
在用户接受条款和条件后仅启用搜索字段。
返回到我们的查询屏幕原始的故事板,我们将看到我们希望选择 Accept 或 Reject 按钮都可以显示用户必须同意的条款和条件。在向屏幕进行输入时,查询条件域和 Enquiry 按钮只有在用户按下 Accept 按钮后才会启用。如果用户按下 Reject 按钮,这些域将保持只读状态。用户按下任一按钮后,按钮即变为不可用,这样用户将无法更改其选择,除非启动一个 新会话。
注意上一段对会话的强调:对于单一会话,我们将强制执行该规则,即用户接受或拒绝整个会话的条款和条件。如果接受,在用户离开前都将接受整个会话的条款和条件。然而,返回时,将再次提示用户接受或拒绝条款和条件。这是现在许多站点的典型要求。
记录用户的 IP 地址以及用户接受还是拒绝条款和条件。
除了用户接受或拒绝条款和条件以外,记录用户的 IP 地址及其所选择的按钮也是必要的。这是出于稍后进行审核的目的:如果客户投诉侵犯其隐私,我们可以进行追溯并说明特定的 IP 地址接受或拒绝了我们的隐私免责声明中包含的条款和条件。
啊,我们差点错过了一个需求。我们还需要记录用户是包裹的发件人还是收件人、IP 地址以及用户接受还是拒绝了条款和条件。为此,我们希望在条款和条件文本前显示一个弹出列表,显式强制用户选择并指定用户所属类型:即,包裹的发件人还是收件人。
为满足以上需求,我们必须实施以下操作:
在屏幕上加上我们的条款和条件的批注,并添加 Accept 和 Reject 按钮。
创建一个按钮映射到的会话级 bean。
按下 Accept 按钮后,启用搜索字段和 Enquiry 按钮。
添加发件人/收件人弹出列表。
在 bean 中添加代码以捕获用户的 IP 地址。
向数据库写入用户的 IP 地址和选择。
在屏幕上加上我们的条款和条件的批注,并添加 Accept 和 Reject 按钮。
这是最简单的要求,结果如下:
理想状况下,条款和条件将源自数据库,我们通过弹出菜单显示完整的法律术语。在我们的律师决定要添加的内容之前,我们将保持其简单性。
以下为新增的代码:
创建一个按钮映射到的会话级 bean。
接下来,当用户按 Accept 或 Reject 按钮时,我们需要将结果存储在会话 bean 中。我们使用 ViewController 项目创建一个 Java 类 view.beans.EnquiryBean,如下所示:
package view.beans;
import javax.faces.event.ActionEvent;
public class EnquiryBean {
String termsAndConditions = "unset";
public void setTermsAndConditions(String termsAndConditions) {
this.termsAndConditions = termsAndConditions;
}
public String getTermsAndConditions() {
return termsAndConditions;
}
// Intended for the "Accept" button for the terms and conditions
public void acceptTermsAndConditions(ActionEvent event) {
setTermsAndConditions("accepted");
}
// Intended for the "Reject" button for the terms and conditions
public void rejectTermsAndConditions(ActionEvent event) {
setTermsAndConditions("rejected");
}
}
注意,我们具有实例属性 termsAndConditions,因此对该类进行实例化后,该属性的值为“unset”,这表示用户尚未接受或拒绝条款和条件。在此之下,接下来的两个方法提供了用于读取和写入该属性的 getter 或 setter 访问器。
此外,我们提供了两个 ActionEvent 方法,映射到各自命令按钮的 actionListener 属性。acceptTermsAndConditions 按钮将 EnquiryBean termsAndConditions 属性设置为“accepted”,rejectTermsAndConditions 按钮将 termsAndConditions 属性设置为“rejected”。
在我们修改 commandButtons 以调用这两个方法之前,需要将 EnquiryBean 配置为 Oracle ADF Controller 的一个会话 bean。我们可以通过打开 Application Navigator 中的 ViewController adfc-config.xml 文件来完成此操作:
我们在 Overview 选项卡 Managed Beans 部分下输入 EnquiryBean 详细信息,如下所示:
完成 bean 配置后,重新映射 commandButtons 以通过 actionListener 属性调用 EnquiryBean 中的 ActionEvent 方法:
actionListener="#{enquiryBean.acceptTermsAndCondition}"/>
actionListener="#{enquiryBean.rejectTermsAndConditions}"/>
按下 Accept 按钮以后,启用搜索字段和 Enquiry 按钮。
要在按下 Accept 或 Reject 按钮后禁用这两个按钮,再次修改含有已禁用 EL 表达式的 commandButtons,如下所示:
actionListener="#{enquiryBean.acceptTermsAndConditions}"
disabled="#{enquiryBean.termsAndConditions != 'unset'}"/>
actionListener="#{enquiryBean.rejectTermsAndConditions}"
disabled="#{enquiryBean.termsAndConditions != 'unset'}"/>
为禁用 enquiry 域,我们将以下已禁用 EL 表达式添加到每个域和 Enquiry 按钮:
label="#{bindings.pLodgedWith.hints.label}"
required="#{bindings.pLodgedWith.hints.mandatory}"
columns="#{bindings.pLodgedWith.hints.displayWidth}"
maximumLength="#{bindings.pLodgedWith.hints.precision}"
shortDesc="#{bindings.pLodgedWith.hints.tooltip}"
rendered="true"
disabled="#{enquiryBean.termsAndConditions != 'accepted'}">
现在进入 Web 页面,我们看到 Accept 和 Reject 按钮已启用,但是 enquiry 域和 Enquiry 按钮已禁用:
如果我们按 Accept 按钮, Accept 和 Reject 按钮都将禁用,并且 enquiry 域和 Enquiry 按钮已启用:
如果我们启动一个新的浏览器会话,按 Reject 按钮而非 Accept 按钮,那么 Accept 和 Reject 按钮也都会禁用,而 enquiry 域和 Enquiry 按钮仍然保持禁用状态:
添加发件人/收件人弹出列表。
故事板显示我们还必须记录用户是包裹的发件人还是收件人。这将为我们提供使用我们系统的用户的有价值信息。
现在,可以将值“sender”和“recipient”硬编码到 Oracle ADF Faces RC 弹出列表绑定中,但是以我们的经验,我们知道老板经常在这之后对要求进行扩展。例如,当老板认识到我们的呼叫中心也将使用该应用程序时,肯定要添加值“call center”。理想情况下,相比对这些值进行硬编码,我们更倾向于从数据库表中获取这些值。在这个阶段,我们不能接触或更改数据库,但是在 Oracle JDeveloper 11g中,我们可以使用静态视图对象设置 Oracle ADF 业务组件结构以接受该数据(就好像它是来自数据库一样)。
文档提示: 不知道如何创建一个带有绑定变量的视图对象吗?请参阅 Oracle Fusion Middleware Fusion Developer's Guide for Oracle Application Development Framework 11g Release 1 的第 5 章 5.3 节“ Populating View Object Rows with Static Data 以获取说明。
在 Model 项目下,我们通过选择“Rows populated at design time (Static List)”选项创建了一个新的视图对象 EnquirySourceView。
我们创建了两个临时属性 Code 和 Description:
并且,我们已经预填充了静态列表的两行,如下所示:
稍后,我们可以根据需要添加更多行。或者,如果我们最后能够创建数据库表来存储这些值,我们也可以删除静态列表视图对象,并使用一个只读视图对象来替换它,而不会影响 SearchPage 的弹出列表。
获取该视图对象后,将通过应用程序模块将其公开,现在,我们的 ViewController 的 SearchPage 可以使用它。
要在本例中将 Select One Choice 添加到我们的页面,我们不能只是从 Data Control Palette 中拖放 EnquirySourceView1。一个映射到 Oracle ADF 业务组件的典型数据绑定控件希望读取其值 并 将其值写回到模型层。在本例中,我们仅希望 读取 值,获取选定值,然后放入我们的 Java 会话 bean 中的一个实例变量中。
为此,我们将手动创建自己的列表绑定。首先,我们切换到 SearchPage 的绑定页面,并按 Bindings 部分下的加号按钮:
从 Insert Item 对话框中,选取列表绑定:
在 Select List Binding Type 对话框中,第四个选项和我们的要求相匹配:我们希望从基础数据源中读取该值到一个独立的 select one choice 中,但是我们不希望该选择从读取的值中更新基础数据源:
在 Create List Binding 对话框中,我们需要创建一个基础数据源以针对 EnquirySourceView 对象进行映射。实质上,这将在 SearchPage 的绑定中创建一个迭代器以从 EnquirySourceView 中提取其数据。我们可以通过选择 Add 按钮完成此操作:
然后,在 Add Data Source 对话框中,选择 EnquirySourceView1 数据源.....
该数据源视图对象在对话框底部创建指定的迭代器,按 OK 按钮并返回到 Create List Binding 对话框,将已选视图对象放入 Base Data Source 弹出下拉列表中,如下所示:
其他选项使我们能够选择将哪个属性(Code 或 Description)从 EnquirySourceView 显示给用户。我们更希望显示更有意义的 Description;如果应显示空白标签项目,我们希望在列表的开始显示空白选项以强制用户选择一个值而不是接受一个默认值。我们将忽略最近使用的工具,因为我们仅在列表中显示几个值。
绑定页面中的结果列表条目名为“Description”,这并不理想。我们将选择 Description 绑定,并通过 Property Inspector 将其重命名为“EnquirySourceList”:
如果我们打开 SearchPage 页面定义文件的 XML 源代码(以上示例中为 view/pageDefs/SearchPagePageDef.xml),我们应看到 EnquirySourceList 绑定的以下条目:
DTSupportsMRU="true"
StaticList="false"
ListIter="EnquirySourceView1Iterator"
id="EnquirySourceList"
NullValueFlag="start"
ListOperMode="navigation">
(注:Oracle JDeveloper 11g build 5188 在创建列表绑定时会产生一些错误,因此以上的 XML 值可能会有所不同。可以相应地调整本例中的条目)。
我们创建绑定后,在 Web 页面中从呈现 Insert Select One Choice 的 Component Palette 对话框中拖拽一个 Select One Choice,如下所示。我们希望从 EnquirySourceList 绑定中检索 Select One Choice 的值,因此我们使用以下 EL 表达式:
在 Common Properties 中,将标签设置为“Who are you?”这最后将生成以下代码:
运行我们的页面,我们将看到以下内容:
现在,Select One Choice 已正确地从底层视图对象获取其值。然而,当用户选择一个选项后,我们需要将该值写入我们可以检索到的位置。通常,我们将该值写回到 Oracle ADF 业务组件模型层,但是在这种情况下,我们不希望将选择永久保存到实体对象或视图对象中,而希望临时存储该值,以便我们可以在 EnquiryBean 中提取该值。
存储该值的最佳位置实际上是在 EnquiryBean 中。我们将添加以下实例变量和访问器:
String enquirySourceIndex;
public void setEnquirySourceIndex(String enquirySourceIndex) {
this.enquirySourceIndex = enquirySourceIndex;
}
public String getEnquirySourceIndex() {
return enquirySourceIndex;
}
然后,我们可以在 selectOneChoice 值属性中使用它:
label="Who are you?"
value="#{enquiryBean.enquirySourceIndex}"
required="true"
disabled="#{enquiryBean.termsAndConditions != 'unset'}">
注意,我们还添加了 Disabled 属性以将该域设置为只读 — 当用户接受条款和条件后。此外,我们还通过所需属性将该域设置为必填域。
本节接下来的内容中,除了用户的 IP 地址和接受或拒绝条款和条件,我们还需要存储 Select One Choice 值。
由于我们刚刚创建了绑定,此操作将相对简单。在 EnquiryBean 中,我们将添加以下方法以通过绑定检索 Select One Choice 值,如下所示:
private String getEnquirySourceCode() {
DCBindingContainer dcBinding =
(DCBindingContainer)BindingContext.getCurrent().getCurrentBindingsEntry();
DCControlBinding listControlBinding =
dcBinding.findCtrlBinding("EnquirySourceList");
FacesCtrlListBinding listBinding = (FacesCtrlListBinding)listControlBinding;
Row row =
listBinding.getRowAtRangeIndex(Integer.valueOf(enquirySourceIndex));
String enquiryListCode = row.getAttribute("Code").toString();
return enquiryListCode;
}
注意我们如何首先在粗体显示的行中检索 EnquirySourceList 绑定,该绑定是映射到虚拟迭代器变量(我们已创建用来存储所选的 Select One Choice 值)的属性值绑定。默认情况下,Select One Choice 存储用户在弹出列表中选择的选项的索引值。因此,我们将提取 EnquirySourceList 绑定并使其在该行中检索索引值,然后检索该已检索行的 Code 属性值。
在 bean 中添加代码以捕获用户的 IP 地址。
在这个阶段,我们已经在屏幕上添加了 Accept 和 Reject 按钮,还添加了发件人/收件人弹出列表。此外,我们在后台具有会话 bean,用其中的方法来处理用户按钮选择,最后,还添加了函数 getEnquirySourceCode() 来检索发件人/收件人弹出列表的值。
在 acceptTermsAndConditions 和 rejectTermsAndConditions 方法中,我们希望收集用户的详细信息,即用户 IP 地址。这可以在 EnquiryBean 会话 bean 中通过以下代码轻松完成:
public String getRemoteAddr() {
String remoteAddr =
((HttpServletRequest)FacesContext.getCurrentInstance(). (cont next line)
getExternalContext().getRequest()).getRemoteAddr();
return remoteAddr;
}
向数据库写入用户的 IP 地址和选择。
最后这个任务实际上又回到了我们的第一个“应该具备”需求,即:记录用户的 IP 地址以及他们是接受还是拒绝了条款和条件(还有用户是发件人还是收件人)。在这个阶段,我们希望实施当前要求;其解决方案将与下一个要求结合。
与以上其他问题一样,解决此问题的最简单解决方法是将其分解为若干可解决的部分。该特定问题分为以下几个部分:
寻找适合记录该信息的事件。记录用户详细信息时,我们需要考虑记录该信息的原因。在我们的示例中,原因是要记录用户接受还是拒绝了条款和条件。记录用户信息的最佳时间是在用户实际按下 Accept 或 Reject 按钮时。
寻找适合的代码阻塞点以收集我们所需要的数据。 假设我们现在知道我们希望在用户按下 Accept 或 Reject 按钮时记录该用户信息,在其中放置下一步逻辑的代码阻塞点明显是我们添加到 EnquiryBean 会话 bean 中的 acceptTermsAndConditions 和 rejectTermsAndConditions 方法。此时,我们的 bean 将如下所示:
public class EnquiryBean {
String enquirySourceIndex;
String termsAndConditions = "unset";
public void setEnquirySourceIndex(String enquirySourceIndex) {
this.enquirySourceIndex = enquirySourceIndex;
}
public String getEnquirySourceIndex() {
return enquirySourceIndex;
}
public void setTermsAndConditions(String termsAndConditions) {
this.termsAndConditions = termsAndConditions;
}
public String getTermsAndConditions() {
return termsAndConditions;
}
// Intended for the "Accept" button for the terms and conditions
public void acceptTermsAndConditions(ActionEvent event) {
setTermsAndConditions("accepted");
}
// Intended for the "Reject" button for the terms and conditions
public void rejectTermsAndConditions(ActionEvent event) {
setTermsAndConditions("rejected");
}
public String getRemoteAddr() {
String remoteAddr =
((HttpServletRequest)FacesContext.getCurrentInstance(). (cont next line)
getExternalContext().getRequest()).getRemoteAddr();
return remoteAddr;
}
private String getEnquirySourceCode() {
DCBindingContainer dcBinding = (DCBindingContainer)BindingContext.getCurrent().getCurrentBindingsEntry();
DCControlBinding listControlBinding = dcBinding.findCtrlBinding("EnquirySourceList");
FacesCtrlListBinding listBinding = (FacesCtrlListBinding)listControlBinding;
Row row = listBinding.getRowAtRangeIndex(Integer.valueOf(enquirySourceIndex));
Object[] enquiryListValues = row.getAttributeValues();
String enquiryListCode = row.getAttribute("Code").toString();
return enquiryListCode;
}
}
收集我们希望记录的数据,包括用户的 IP 地址、用户接受还是拒绝条款和条件,以及用户是发件人还是收件人。 假设我们现在知道收集数据的方法,由于我们之前创建了 getRemoteAddr()、getTermsAndConditions() 和 getEnquirySourceCode() 方法,我们可以增强 acceptTermsAndConditions() 和 rejectTermsAndConditions() 方法来轻松检索用户 IP 地址、按钮选择以及弹出列表选择。
我们可以创建一个从 acceptTermsAndConditions() 和 rejectTermsAndConditions() 调用的新方法 storeUserInformation(),如下所示:
// Intended for the "Accept" button for the terms and conditions
public void acceptTermsAndConditions(ActionEvent event) {
setTermsAndConditions("accepted");
storeUserInformation();
}
// Intended for the "Reject" button for the terms and conditions
public void rejectTermsAndConditions(ActionEvent event) {
setTermsAndConditions("rejected");
storeUserInformation();
}
public void storeUserInformation() {
String userIPAddr = getRemoteAddr();
String userTermsAndConditions = getTermsAndConditions();
String userEnquirySource = getEnquirySourceCode();
}
如您所见,storeUserInformation() 方法检索我们希望永久存储在数据库的所有值。此时,最好在该方法中放置一个断点并在调试会话下运行该应用程序,以查看 storeUserInformation() 方法检索到的值。在我们将数据写入数据库前,我们应该确保我们确实已获取要写入的数据并且没有出错。
如果一切顺利,这将把我们带到最后一个任务:
将用户信息写入数据库。 此时,我们希望记录的用户信息有三类:用户的 IP 地址、用户接受还是拒绝条款和条件以及用户是包裹的发件人还是收件人。
我们需要再次考虑应如何完成此任务。为将该数据写入数据库,我们首先需要一个表,因此我们请 DBA 创建以下表:
CREATE TABLE log_session
(log_session_id NUMBER(10) NOT NULL
,user_ip VARCHAR2(15) NOT NULL
,terms_conditions VARCHAR2(8) NOT NULL
,enquiry_source VARCHAR2(4) NOT NULL
,datetime DATE NOT NULL
,CONSTRAINT log_session_pkg PRIMARY KEY (log_session_id));
我们还需要请 DBA 创建以下序列来填充 log_session_id:
CREATE SEQUENCE log_session_seq;
然后,我们需要选择如何将该数据写入表。从我们的会话 bean 中,我们仅能够将原始 JDBC 写入日志条目。然而,这并不理想,因为我们已经使整个 Oracle ADF 业务组件模型层非常适合写入到数据库,并且无需我们编写容易出错的 JDBC 代码。
通过使用现有的 Oracle ADF 业务组件应用程序模块,我们可以基于 log_session 表创建一个 Oracle ADF 业务组件实体对象或视图对象,将值写入数据库。然而,登录到数据库有一个非常特殊的特性,它使得重用我们在应用程序中创建的现有应用程序模块成为一个固有的问题。
正如您所意识到的,Oracle ADF 业务组件中的应用程序模块,更准确地说是 root 应用程序模块,负责数据库的事务控制;应用程序模块执行提交和回滚,最终将底层视图对象和实体对象数据保存到数据库。因此,对于标准的 Oracle ADF 业务组件,CReated-Update-Delete (CRUD) 希望我们的 root 应用程序模块处理的应用程序允许用户对底层数据做出多处更改然后保存该结果。
然而在记录时,无论用户在进行何种操作,我们都必须在记录条目生成时就将结果保存到数据库。记录事件要求其自身事务与 SearchPage 所基于的 CRUD 应用程序的应用程序模块事务相分离。PL/SQL 类似这样的特性称为“自主事务”。
解决该问题的一个方法是创建一个单独的记录 root 应用程序模块来控制其自身事务,它具有其自身的视图对象和实体对象,我们可以通过它们进行记录。但是,还有一种更加简单的方法:利用 Oracle ADF 业务组件框架的功能,您可以对数据库进行 JDBC 调用,利用当前应用程序模块的连接,但在 autonomous_transaction pragma 中包装 JDBC 匿名 PL/SQL 块。
为此,我们将在 AppModuleImpl 中实现一个名为 logSession() 的方法:
public void logSession(String userIp,
String termsConditions,
String enquirySource) {
DBTransaction trans = getDBTransaction();
SequenceImpl seq = new SequenceImpl("LOG_SESSION_SEQ", trans); setLogSessionId(seq.getSequenceNumber());
CallableStatement statement = null;
String plsql =
"DECLARE "
+ "PRAGMA AUTONOMOUS_TRANSACTION;"
+ "BEGIN "
+ "INSERT INTO log_session "
+ "(log_session_id, user_ip, terms_conditions, enquiry_source, datetime) "
+ "VALUES "
+ "(?,?,?,?,sysdate);"
+ "COMMIT;"
+ "END;";
statement = trans.createCallableStatement(plsql, 4);
try {
statement.setInt(1, getLogSessionId().intValue());
statement.setString(2, userIp);
statement.setString(3, termsConditions);
statement.setString(4, enquirySource);
int rows = statement.executeUpdate();
} catch (SQLException s) {
throw new JboException(s);
} finally {
try {
if (statement != null)
statement.close();
} catch (SQLException s) { /* ignore */ }
}
}
注意我们如何检索 log_session_seq 并通过调用 setLogSessionId 将其存储到 AppModuleImpl 的一个变量中。稍后将用到已检索的 LogSessionId 序列号。为将 LogSessionId 存储到 AppModuleImpl 中,我们还定义了以下实例变量和访问器方法:
private Number logSessionId;
public void setLogSessionId(Number logSessionId) {
this.logSessionId = logSessionId;
}
public Number getLogSessionId() {
return logSessionId;
}
为在应用程序模块中存储将在多个用户请求中持久保存的实例变量,我们需要覆盖 AppModuleImpl 中的 activateState 和 passivateState() 方法,如下所示:
private static final String LOG_SESSION_ID = "jbo.logSessionId";
@Override
protected void passivateState(Document document, Element element) {
Number logSessionIdNum = getLogSessionId();
if (logSessionIdNum != null) {
int logSessionIdInt = logSessionIdNum.intValue();
Node node = document.createElement(LOG_SESSION_ID);
Node cNode = document.createTextNode(Integer.toString(logSessionIdInt));
node.appendChild(cNode);
element.appendChild(node);
}
}
@Override
protected void activateState(Element element) {
super.activateState(element);
if (element != null) {
NodeList nodeList = element.getElementsByTagName(LOG_SESSION_ID);
if (nodeList != null) {
for (int i = 0, length = nodeList.getLength(); i < length; i++) {
Node child = nodeList.item(i).getFirstChild();
if (child != null) {
try {
setLogSessionId(new Number(child.getNodeValue()));
break;
} catch (SQLException ex) {
new JboException(ex.getMessage());
}
}
}
}
}
}
然后,我们将修改 EnquiryBean 的 storeUserInformation() 方法以调用应用程序模块的 logSession() 方法。
public void storeUserInformation() {
String userIPAddr = getRemoteAddr();
String userTermsAndConditions = getTermsAndConditions();
String userEnquirySource = getEnquirySourceCode();
DCDataControl dc = BindingContext.getCurrent().getDefaultDataControl();
ApplicationModule am = (ApplicationModule)dc.getDataProvider();
AppModuleImpl service = (AppModuleImpl)am;
service.logSession(userIPAddr, userTermsAndConditions, userEnquirySource);
}
注意,在 Oracle JDeveloper 源编辑器中看到,可能有必要重新编译(重新构建)Model 项目以及 ViewController 项目,从而使 Oracle JDeveloper 识别 Model 项目中 AppModuleImpl 的新 logSession() 方法。否则,可能出现语法错误,无法导入要使用的类。重新构建完这两个项目后,再导入 model.AppModuleImpl,语法错误将消失。
如果我们运行 SearchPage,无论接受还是拒绝条款和条件,用户信息都将写入到记录表中。
总结
在本章中,我们添加了代码以满足我们应用程序的“应该具备”需求,包括记录用户的 IP 地址、用户是否接受我们的条款和条件以及增强搜索功能。在 下一章中,我们将添加功能来记录用户输入的所有搜索条件以及返回记录的数量。
单击此处查看“从设计到实践全面了解 Oracle ADF 应用程序”描述和目录。