Oracle
Consulting asked the BC4J development team to review a
sample JClient/BC4J application from one of their customers. We were
specifically asked to study the number of thin-client-to-appserver roundtrips
made by their sample application, and provide guidance on how to minimize these
round-trips.
This paper documents the issues that we found in the
example that influence round-trips and documents the rebuilt "best practices"
sample application that we have produced to illustrate the theoretical minimum
number of round-trips when the BC4J and JClient frameworks are used in the
optimal way.
We illustrate that an equivalently-functional form
can startup with 15-20 network roundtrips instead of the more than 100 round
trips that the customer's analogous application was incurring.
NOTE:
The observations herein were made using the JDeveloper 9.0.3.2
maintenance release, as well as the upcoming 9.0.3.3 maintenance
release.
Brief Overview of the 3-Tier BC4J Architecture
Why Swing and JClient?
Developers who demand an application with a cross-platform, rich user
experience typically turn to Java and its built-in
Java Foundation Classes.
This library, also known as "Swing" from its early project code name, provides
a powerful and portable set of UI building blocks so your application can run
on any machine with a Java VM. The fact that the entire
JDeveloper IDE is built using Swing is a solid testament to the richness of the
user experience that a Swing application can offer end-users.
When
a Swing application needs to present and interact with a corporate database --
a basic requirement of any kind of business application -- developers need to
combine Swing with additional layers of code to handle interacting with the
back-end business services that expose business-tier information, enforce
business rules, and coordinate database access. While numerous books exist that
explain the basic Swing libraries, virtually no books explain how to use Swing
to build a rich-client business application on the J2EE platform.
This lack of reading material on the subject is not by accident. The
simple explanation is that the code required to do a good job of coupling Swing
with backend business services in a network-efficient way is simply too complex
if you were to have to develop it yourself. While doing the job in a
network-inefficient way is fairly straightforward, doing it properly requires
clever coordination of a thin-client data cache and the middle-tier business
services that provide data and allow that data to be modified.
J2EE
developers choose the Oracle BC4J framework for simplifying the task since it
includes all of the "plumbing" code to make building these kinds of rich-client
business applications much simpler. Due to its unique design, your rich-client
application can work either in a two-tier or three-tier deployment
architecture, as well. The JClient data binding framework for Swing works
together with BC4J and allows you to easily bind any Swing user interfaces to
your back-end business services. It is architected to support anything that you
can build using Swing, however custom, so for example you could use open source
Swing-based libraries like JGoodies to build even nicer looking
forms and panels with less page layout effort and without becoming masters of
the powerful, yet complicated Swing layout mangers.
The three-tier
deployment architecture is especially compelling due to the introduction of
Java
WebStart, which allows clients to download the latest version of your
application client interface from a web server with the simplicity of a central
point of update. However with this power, comes the responsibility to
understand the nature of the three-tier application regarding network traffic.
We'll see in this paper that by properly exploiting the features of the BC4J
framework, your thin-client applications can work in a highly-optimized way
over the network. However, as might be expected, not all of the optimizations
can happen for you automatically...Read on to learn more.
General Comments About Three Tier Architectures
When the
code that implements the user interface is physically separated from the
application code that implements the business service, the two layers must
communicate using some IPC mechanism. For thin-client Swing UI's working with
remote EJB Session Beans, this is the RMI-over-IIOP protocol.
The
frequency with which the thin client communicates with the remote business
components is a determined by a combination of two primary factors:
The number of server-side API calls performed by
the client
The amount of local data-caching
and data-transfer optimizations that thin-client layer is able to
perform
The BC4J framework contains a
thin-client cache and a sophisticated mechanism to keep this thin-client cache
in sync with middle-tier data that the user interface is displaying and
interacting with. This design pattern, unique to BC4J, is our "Value Messenger"
design pattern implementation. If developers exploit this feature correctly,
they can keep network roundtrips to a minimum by pushing as much logic as
possible into their middle-tier components.
BC4J framework
components like Entity Objects, Associations, View Objects, View Links, and
Application Modules already are designed to automate a large amount of common
application infrastructure code for your middle-tier business components. By
making maximal and correct use of these built-in framework features, you not
only can achieve fewer network round trips, but also have much less
hand-written "plumbing" code to write yourself.
Using the Value Messenger Pattern to Minimize Round Trips
The
Simplifying
J2EE and EJB Development with BC4J whitepaper on Oracle Technet
describes the BC4J architecture and functionality in detail. In particular, it
explains the BC4J implementation of the "Value Messenger" pattern in the
section entitled "Understanding the BC4J Value Messenger Pattern
Implementation". The Value Messenger design pattern allows the developer to
place a maximal amount of application logic into the
middle tier components (instead of writing it inside the
thin client classes), since it automatically coordinates keeping the
thin-client cache in sync with any changes that this middle-tier code causes to
occur.
For example, if you are building a form that edits a
"master" record and some of its "detail" information -- perhaps assisted by
data-driven poplists as well -- you need to coordinate a lot of data sources
before you can display the form to the user. You can achieve all of this data
model "setup" on the server side using a custom application module method
combined with view links between master and detail views. In a single remote
method call from the thin client -- perhaps passing in a key ID number of the
master "thing" you want to edit -- all of the data needed by the form is
retrieved on the server side. Thanks to the value messenger implementation,
when control returns to your thin client from the server-side custom method,
all of the changes in the data being observed by your Form are brought back in
the same round-trip response.
Using BC4J, you want to push as much
application code as possible into the middle-tier business components, leaving
only the bare minimum of client-side code in the thin client. This not only
keeps the thin client thin, it also makes sure that the network round trips are
the minimum necessary.
We'll see in this article that by correctly
exploiting the features of the BC4J framework, we were able to achieve a
dramatic reduction in the amount of client-side code and network round trips,
when compared with the example application provided to us from the customer
(via Oracle Consulting) at the outset.
Measuring the Round Trips Your JClient/BC4J App Performs
JDeveloper comes
with three built-in performance profiling tools:
The Execution Profiler
This lets you find "hot spots" in your
Java code on the client and server side to know where to spend time
optimizing.
The Memory Profiler
This helps you find objects that you might have inadvertently kept
references to such that the VM's garbage collector cannot free them as you
expected.
The Event Profiler
This
helps get a higher-level picture of where time is being spent in your
application doing things like executing queries, issuing remote method calls,
loading metadata, or any number of developer-defined, application-specific
events.
The BC4J framework is
instrumented already with a number of diagnostic events that the JDeveloper
profiler can show you. Figure 1 shows what
project-level profiling filter options can be set for BC4J. In the screen shot
shown here, we've configured the Event Profiler to only show JVM garbage
collection events ("GC") as well as BC4J remote method calls. For this
exercise, we were more interested in the number of remote method call round
trips and no so interested in the timing information, so we've unchecked all of
the detail information columns except for the "Comment" that will tell us which
remote methods are getting invoked.
Figure 1: Project-Level Profiler Filters to See BC4J Remote Method Calls
Given our event
profiler settings above, the output of using the profiler on a JClient
application running against a remotely deployed application module looks like
what you see in Figure 2.
Figure 2: Display of the JDeveloper Event Profiler
The App We Received
Oracle Consulting supplied us with the
TarExample application that has the user interface shown in
Figure 3. They said this application represented one of the
forms that the customer has built.
Figure 3: Style Card Form from the Customer's TarExample Application
The Application Functionality
As we understood it, the "Style Card"
form is used to edit information about the styles related to an apparel
collection for a given department. When the form is run, a particular
"collection ID" is passed in, and the style information for that collection is
edited. It appeared from the data model that a Collection is related to exactly
one Department.
The main panel at the
top shows Style Record information.
The
"Accessories" area allows the user to see, create, and delete accessory detail
information for the style.
The "Labels" area
allows the user to see and modify the different kinds of labels that might be
related to the style (barcode labels, main labels, size labels, and hangtag
labels). It also allows the user to enter or remove miscellaneous other kinds
of labels in the table below.
The "Group Info"
area appeared to be for reference information only and displayed the set of
StyleGroups related to the current department, as well as the related set of
StyleSubGroups related to the currently selected StyleGroup.
The (Ok) button at the bottom saves any
changes made to the style, while the (Cancel) button
aborts any changes and exits the form.
The Issues We Found
Upon running the
first versions of the TarExample application supplied by
Oracle Consulting we used the JDeveloper event profiler to observe over 100
network roundtrips between the thin-client UI and the application module
deployed as an EJB session bean. This was measured from the time the form was
run, until it appeared with its data populated. This
TarExample already contained some suggested modifications to
the original customer application which the BC4J team had made to Oracle
Consulting during previous, internal conference calls.
We
highlighted a number of the factors of the application's design that
contributed to this high number of network roundtrips:
Application Module Did Not Use View
Links
Instead of using middle-tier, built-in
functionality of the BC4J framework to coordinate master/detail view objects,
the user interface classes contained client-side code that was manually
performing the master/detail coordination. It carried out the task of
retrieving the current primary key values from a master record and setting bind
variables in multiple, related view objects to produce a coordinated set of
detail records.
By declaratively defining view links where
master/detail relationships exist, all of this coordination is done on the
server side with no round trips and much less code.
Client-side Code to Do Query By
Example
Instead of passing the required collection id
into a custom method on the application module that could encapsulate all of
the server-side data-model setup, the application contained client-side code to
manually construct and append a where clause and execute a query to find the
appropriate style record.
Also, the manual code written to build up
WHERE clauses gave us the impression that the customer was not aware that BC4J
provides a built-in ViewCriteria feature in all view objects
that implements a sophisticated query by example for you with no hand-written
code required to build up the WHERE clauses.
Client-side Code to Coordinate JComboBoxes
The data driving the LOV comboboxes were all detail queries requiring the
context of the current department to execute. The application contained code
that was manually coordinating the bind variable population and execution of
the queries from the client instead of letting the server-side component handle
all of this detail via view links.
Client-side Calls to Clear View Object Caches
It was not obvious to us why, but many places in the application appeared
to be making client-side calls to clear the view object
caches.
Client-Side Code to Manage
Poplist Selection for Style Label Details
The details of
the four preset kinds of style labels are stored in the
STYLE_LABELS detail table along with the miscellaneous label
information. Each style can have either zero or one of the preset label kinds,
and in fact the user interface presents these detail rows as if they were a
simple property of the master record. There was a lot of client-side code that
was creating blank rows in the StyleLabels view object to
fill out blank rows for the four different preset label kinds, as well as
client-side code that was coordinating the label poplists with the data in this
detail table.
Rebuilding the "Style Card" Application
We decided that
the most educational feedback we could provide, beyond highlighting the above
specific issues, was to rewrite the "Style Card" form using a number of "best
practices" techniques to minimize network round-trips. In this section, we
highlight the interesting details of the "OptimizedRoundtrips.jws" workspace
containing this rewritten version of the application.
The MC_TEST Sample Schema
Figure 4 shows the Database
diagram (produced with JDeveloper 10g Preview) of the tables we received in the
MC_TEST schema.
Figure 4: Database Diagram of Customer's Sample Schema
The OptimizedRoundtrips.jws Workspace Structure
We created a workspace with two projects:
Model.jpr
This contains the model layer
components that implement the business service, as well as the EJB Session Bean
which the BC4J design time automatically creates for us when we deploy the
application module component as an EJB (Session Facade, Bean Managed
Transaction). The components have been organized into packages with appropriate
names in the customer.app.model.* package
hierarchy.
View.jpr
This contains the view layer components that implement the user interface
of the application. The panels and form live in appropriately named packaged in
the customer.app.view.* package
hierarchy.
Building The StyleApp Application Module
We created BC4J entity objects and
associations by reverse-engineering the tables in the MC_TEST schema using the
BC4J wizard. We opted not to immediately create any view objects or application
module since we wanted to create these in a different package from the
entities.
Next, we created view objects patterned after the view
objects from the TarExample application, except that we built them to have the
appropriate view links with the driving, parent view object. This generally
meant copying the query from the TarExample application (where Expert Mode was
being used) and removing the hand-coded bind variables that would be
automatically added by the BC4J view link coordination feature.
Figure 5 illustrates the view-linked data model that we ended up
with for the StylesApp application module.
Figure 5: Application Module "Data Model" for Style Card App
Leveraging SQL to Improve the Client
Having noticed
that some of the client-side calls were related to the management of the Style
Label information residing in a detail table, I decided to modify the
TarExample's SQL query for the
StyleRecords view object to add four new SQL-derived
attributes:
BarcodeLabelId
MainLabelId
SizeLabelId
HangLabelId
I
accomplished this by enhancing the "Expert Mode" query for StyleRecords to add
the parts below that are flagged as /* ADDED */, in order
that in each row of this rowset reflect its current values from the detail
STYLE_LABELS table for the four built-in style label types.
The way I've written the query with outer-joins, the lack
of a detail row in STYLE_LABELS for any of the style label
types (1,2,3 or 4), will return a NULL value for the current label id. It will
also work as expected if a STYLE_LABELS row exists, but has
a NULL value for LABELS_ID.
SELECT Styles.STYLES_ID, Styles.STYLE_NAME, Styles.SUPPLEMENT, Styles.BLOCKED, Styles.STYLE_NUMBER, Styles.EAN_NO, Styles.BOX, Styles.STYLE_SUBGROUPS_ID, Styles.CREATED_BY, Styles.CREATED_DATE, Styles.CHANGED_BY, Styles.CHANGED_DATE, Styles.DELETED, Styles.STYLE_PICTURES_ID, Styles.STYLE_DESCRIPTION, Styles.QUALITY_DESCRIPTION, Styles.USERS_ID, Styles.BRANDS_ID, StyleColls.STYLE_COLLS_ID, StyleColls.STYLES_ID AS STYLES_ID1, StyleColls.COLLECTIONS_ID, StyleColls.DEPARTMENTS_ID, StyleSubgroups.STYLE_SUBGROUP_NAME, StyleSubgroups.STYLE_SUBGROUPS_ID AS STYLE_SUBGROUPS_ID1, StyleGroups.STYLE_GROUP_NAME, StyleGroups.STYLE_GROUPS_ID, Collections.DEPARTMENTS_ID AS DEPARTMENTS_ID1, Collections.COLLECTION_TERM, Collections.COLLECTION_NAME, Collections.COLLECTIONS_ID AS COLLECTIONS_ID1, Departments.DEPARTMENTS_ID_PRIOR, Departments.DEPARTMENTS_ID AS DEPARTMENTS_ID2, Users.NETWORK_ID, barcodeLabel.LABELS_ID AS BARCODE_LABEL_ID, /* ADDED */ mainLabel.LABELS_ID AS MAIN_LABEL_ID, /* ADDED */ sizeLabel.LABELS_ID AS SIZE_LABEL_ID, /* ADDED */ hangLabel.LABELS_ID AS HANG_LABEL_ID /* ADDED */ FROM MC_TEST.STYLES Styles, MC_TEST.STYLE_SUBGROUPS StyleSubgroups, MC_TEST.STYLE_GROUPS StyleGroups, MC_TEST.STYLE_COLLS StyleColls, MC_TEST.COLLECTIONS Collections, MC_TEST.DEPARTMENTS Departments, MC_TEST.USERS Users, MC_TEST.STYLE_LABELS barcodeLabel, /* ADDED */ MC_TEST.STYLE_LABELS mainLabel, /* ADDED */ MC_TEST.STYLE_LABELS hangLabel, /* ADDED */ MC_TEST.STYLE_LABELS sizeLabel /* ADDED */ WHERE Styles.DELETED = 'N' AND Styles.STYLE_SUBGROUPS_ID = StyleSubgroups.STYLE_SUBGROUPS_ID AND Styles.USERS_ID = Users.USERS_ID(+) AND StyleSubgroups.STYLE_GROUPS_ID = StyleGroups.STYLE_GROUPS_ID AND Styles.STYLES_ID = StyleColls.STYLES_ID AND StyleColls.COLLECTIONS_ID = Collections.COLLECTIONS_ID AND Collections.DEPARTMENTS_ID = Departments.DEPARTMENTS_ID AND barcodeLabel.STYLES_ID (+) = Styles.STYLES_ID /* ADDED */ AND barcodeLabel.LABEL_TYPES_ID (+) = 1 /* ADDED */ AND mainLabel.STYLES_ID (+) = Styles.STYLES_ID /* ADDED */ AND mainLabel.LABEL_TYPES_ID (+) = 2 /* ADDED */ AND sizeLabel.STYLES_ID (+) = Styles.STYLES_ID /* ADDED */ AND sizeLabel.LABEL_TYPES_ID (+) = 3 /* ADDED */ AND hangLabel.STYLES_ID (+) = Styles.STYLES_ID /* ADDED */ AND hangLabel.LABEL_TYPES_ID (+) = 4 /* ADDED */
This query will make presenting and data binding for the
four label poplists much simpler and make it possible to achieve the same
behavior as the original application without code.
Also, instead of
managing the rows in the detail StyleLabels view object from
client-side code, I generated a custom row class for the
StyleRecords view object (including accessors), and then
overrode the way that the set attribute code was performed for these four new
attributes. The correspondingly overridden setter methods in the
StyleRecordsRowImpl.java class look like what you see below.
Each one has a single additional line added to call a private helper method
called modifyDetailStyleLabel.
The modifyDetailStyleLabel helper
method does the work from the server side of coordinating the setting of the
four preset label ids with their corresponding rows in the detail
StyleLabels view object. The straightforward code for that
routine looks like this, and it also lives in the
StyleRecordsRowImpl.java class:
/** * Modify the appropriate detail row in the view-linked StyleLabels * when the SQL-Derived attribute in the StyleRecords row is set by * the user. Since this is done in middle-tier code, it causes no * thin-client-to-appserver traffic. */ private void modifyDetailStyleLabel(int ATTR, Number labelsId) { RowIterator styleLabels = getStyleLabels(); boolean foundDetailLabelRow = false; Number typeToUpdate = styleLabelTypeCodeForAttr(ATTR); while (!foundDetailLabelRow && styleLabels.hasNext()) { StyleLabelsRow slr = (StyleLabelsRow)styleLabels.next(); if (slr.getLabelTypesId().equals(typeToUpdate)) { slr.setLabelsId(labelsId); foundDetailLabelRow = true; } } if (!foundDetailLabelRow) { StyleLabelsRow newStyleRow = (StyleLabelsRow)styleLabels.createRow(); newStyleRow.setLabelsId(labelsId); newStyleRow.setLabelTypesId(typeToUpdate); /* Note: StyleLabelId attribute is of type DBSequence and will be * ---- auto-assigned from the customer's existing DB sequence * by the BEFORE INSERT FOR EACH ROW trigger created by the * customer's script on the STYLE_LABELS table. */ } }
To encapsulate the mapping of the numeric "type" code
for each of the preset label kinds, I chose to implement the further helper
method called styleLabelTypeCodeForAttr like this:
private Number styleLabelTypeCodeForAttr(int ATTR) { switch (ATTR) { case BARCODELABELID: return new Number(1); case MAINLABELID: return new Number(2); case SIZELABELID: return new Number(3); case HANGLABELID: return new Number(4); } return null; }
The net result is that when any of the four new
SQL-derived label id attributes is updated in the
StyleRecords row, behind the scenes -- and of particular
importance, in the middle-tier -- the corresponding rows of the
StyleLabels view object are maintained. When the transaction
is committed, any modified rows will be committed along with any other edits
performed by the user.
Building The User Interface
To build the user interface, we used the default
features of JDeveloper for building JClient-based Swing data panels and forms.
We used the JDeveloper UI Editor to layout the forms, and where necessary,
declaratively setup the data binding code.
NOTE:
We did not try to
copy the user interface exactly, but tried instead to represent the same number
of zones, tables, and poplists that the sample had. Showing additional fields
from the StyleRecords view object on the
StyleRecordsPanel, for example, would not affect the network
round trips. Remember that we were working with a bare minimum amount of
customer data, so some of the poplists in the TarExample were coming up blank
for us when we ran it. If the actual customer application contains more
poplists to populate, the tips in this paper should apply directly to optimize
their population as well.
Building the PanelStyleRecords Panel
We started by using the "Panel" wizard in
the New Gallery's "Swing/JClient for BC4J" category to create a default form
panel for the StyleRecords view object. The wizard produced
a databound panel that looked like Figure 6
Figure 6: PanelStyleRecords.java in the UI Editor
Building the PanelStyleAccessoriesDescriptions Panel
Next we built the
PanelStyleAccessoriesDescriptions panel by again using the
"Panel" wizard, but this time selecting a "table" instead of a "form".
Following the look of the original Style Card, we only included the
AccessoryType and Description as shown in
Figure 7. We used the UI Editor to set the
BorderLayout layout manager, to add an extra JPanel to the "South" of the
panel, and created two JButton's in that panel for the
(Insert) and (Delete) functionality.
The wizard already took care of the data binding for the JTable, so we only
needed to bind the two buttons to the appropriate action bindings. By selecting
each of the two buttons in turn in the UI Editor, and clicking to edit their
model property in the Property Inspector, we launch the
"JClient Action Binding" editor where we select which view object to bind to
(StyleAccessoriesDescriptions) and which action the button
should be "wired" to. The (Insert) button is wired this
way to the "Create a new row" action, and the (Delete)
button is wired to the "Delete current row" action for this view object.
Figure 7: PanelStyleAccessoriesDescriptions
Building the PanelGroups Panel
Next we use
the "Panel" wizard again to create a form display for the
StyleGroups view object, indicating to only include the one
field for StyleGroupName. In this way, the wizard sets me up
a prompt and a field for the group name. I deleted the field just created by
the wizard, then clicked out two JComboBox controls into the GridBagLayout as
shown in Figure 8. Again I use the Property Inspector to edit
the model property of the two comboxes to declaratively setup the navigation
binding for the StyleGroups and StyleSubgroups, indicating to display the
StyleGroupName and the StyleSubgroupName in the poplist respectively. I also
added an extra JLabel to be the prompt for the 2nd "subgroup" poplist.
Since the navigation binding will automatically set the current row in
the StyleGroups view object when the user picks a choice
from the list, and since the StyleGroups and
StyleSubgroups view objects are view linked, no user-written
code is required to keep these datasets and poplists coordinated.
Figure 8: PanelGroups
To create a prompt
for the subgroup poplist, I switched quickly to the Code Editor and borrowed
one line of code that the wizard generated for me to setup the "Group" prompt,
and pasted a 2nd copy like this:
This will cause the prompt for the subgroup poplist to
be automatically picked up from the control hints supplied in the BC4J
components for the StyleSubgroupName attribute instead of
hard-coding a prompt in here.
Building the PanelLabels Panel
Last but not least, I created the PanelLabels
panel using the JClient "Panel" wizard to build a Form-style panel for the
StyleRecords view object, picking on the four label id fields that I added to
the view above. Then, I deleted the default text fields created, and created
four JComboBox controls in their place.
Figure 9: PanelLabels
Again using the declarative binding editors that appear when I edit the
model property of these controls, I setup their LOV
Bindings. Figure 10 shows one of the two tabs of the
model property editor for a combo box. I've defined the LOV
binding to show choices in the list from the SizeLabelsLOV
view object, and to use the StyleRecords view object as the
target view to update when the user selects something from the list. Clicking
(Add) to add an
<LOVAttribute,TargetVOAttribute>
pair, I pick the attribute names from the poplists in the table to that the
LabelsId attribute from the row the user picks in the
SizeLabelsLOV view object will automatically update the
SizeLabelId attribute of the current row in the
StyleRecords view object. This attribute binding information
is also used to automatically coordinate the combobox to reflect the current
value queried from the database for existing records as well.
Figure 10: Using LOV Binding Editor to Setup SizeLabels ComboBox
I
repeat this process three more times for the other three combo boxes, choosing
their respective LOV view objects, and using the corresponding attribute of the
StyleRecords view object as the target. With these bindings
in place (coupled with the modified StyleRecords SQL query and the
StyleRecordRowImpl.java server-side code we highlighted
above) all of the functionality related to the "Labels" panel poplists is
reduced to zero code. In particular, zero client-side code.
To
finish the job for this panel, I created a JTable and set its model property to
use the AttributeList table binding for tables to bind it to the
OtherLabelsDescriptions view object. I added an extra JPanel
to the bottom and put down two JButton instances and setup their properties and
ActionBindings so that they would automatically be "wired" to the
create and remove actions of the
iterator.
Assembling the StyleFormApp Form From DataPanels
The last step is "assembling" the StyleApp Form from
the constituent panels that we've created above. There is an easy way, and a
hard way to do this. The easy way is the way which requires no hand-coding and
which produces the most optimized code. As we've seen above, the "optimal" way
often involves leveraging built-in features of the framework and/or framework
design time.
In this case, I use the JClient "Empty Form" wizard to
create a blank form. Then I use the property inspector and give it a title of
"Style Card" using the property inspector.
Figure 11 shows the component palette set to the "JClient Controls"
page, and illustrates which palette tool is for the "dataPanel" control.
Clicking this "dataPanel" tool, and clicking into the Structure Pane or
directly onto the UI Editor to select a parent container for the new panel, you
will see a dialog that allows you to choose which JClient data panel you would
like to place there. Only the classes in the current project that implement the
oracle.jbo.uicli.controls.JClientPanel interface will appear
in the list for selection.
Figure 11: The Handy JClient DataPanel Tool
The data panel tool handles all
of the code generation in your form class to:
Create a private member variable to hold an instance of the
panel
Instantiate the panel
Set the panel instance's panelBinding to share the same panel
binding that the form is using.
I used the
UI Editor and the Data Panel tool to add an instance of each of our four
JClient panels described above to produce the final StylesAppForm that you see
in Figure 12
Figure 12: The Rebuilt StylesAppForm
Implementing Additional Network Optimizations
This section
describes the additional optimizations we made to further reduce network
traffic.
Using Custom Application Module Methods to Setup Data Model
Application Modules are your business service
components that give you a convenient place to "hang" code that does
not belong in the:
entity objects
That's where reusable
business rules go
view
objects
That's where query-related logic goes, as well
as customized, server-side row-handling logic and calculated attributes
go.
client user interface
layer
That's where only the minimum amount of
UI-specific presentation logic should go.
When working with JClient/Swing forms, think of the application module as
the "server-side buddy" of your thin-client form. Whenever and wherever
possible, code that works with your data model should be encapsulated inside an
application module class and exposed as necessary a custom method for
invocation by clients. This is especially true if you need
to perform looping operations, data setup, or multiple BC4J API calls on view
objects in your data model. By partitioning this code on the business service
side of your three-tier application -- instead of in the client -- you can
accomplish a large amount of work in a single network round trip.
Before
looking at specifically how we used a custom method in the customer's
application to improve network round-trips, let's first briefly recall the
steps to expose a custom method for invocation by a client. There are basically
just two steps:
Write a method in your
MyAppModuleImpl.java class
The method can use any serializable type as arguments and/or return
value. You can also return any of the oracle.jbo.*
interfaces like RowSet, Row,
ViewObject, ApplicationModule, or arrays
of those.
Visit the Client
Methods panel of the Application Module Editor as shown in
Figure 13, and shuttle the desired custom methods from the
Available list to the Selected
list.
NOTE:
Methods with return types or arguments of types which
are not serializable will not show up in the
list.
Figure 13: Selecting the Set of Custom App Module Methods to Expose
After clicking
(OK) JDeveloper will generate a service interface for you
which extends oracle.jbo.ApplicationModule and adds your custom methods that
are exposed like this. By convention, if your service lives in a package
a.b.c, then the custom service interface will live in
package a.b.c.common.
package customer.app.model.services.common; import oracle.jbo.ApplicationModule; import oracle.jbo.domain.Number; /** * Generated interface. Do not modify. */ public interface StylesApp extends ApplicationModule { void setCollectionIdForStylesEditing(Number collectionId); }
As we learned above, custom application module methods
are even more powerful because of BC4J's Value Messenger design pattern
implementation. Any data that their middle-tier logic causes to change or be
refreshed will automatically be carried down to the thin client as part of the
network round-trip response. We'll see how that helps us reduce round trips in
the next section.
In the original application given to us by
Oracle Consulting, the form supplied a fixed "Collection ID" and "Department
ID" as startup parameters in order to know which style information to edit. By
studying the database diagram of the schema, it appeared that each collection
had a unique ID and that each collection was related to exactly one department.
So, I decided that it should be enough to pass in just the collection ID as the
external information to use to "prime" the data model for editing the style
information for that collection and the department that it's related to.
I started by modifying the query for the Departments
view object to be a join between DEPARTMENTS and COLLECTIONS
and to have a bind variable which allows me to restrict the result to a
particular collection based on its ID. The query ended up looking like
this:
SELECT Departments.DEPARTMENTS_ID, Departments.DEPARTMENTS_ID_PRIOR, Departments.DEPARTMENT_NAME, Departments.SHORT_NAME, Departments.GENDER, Departments.ADMIN_FUNCTION, Departments.DELETED, Collections.Collections_ID AS COLLECTIONS_ID FROM DEPARTMENTS Departments, COLLECTIONS Collections WHERE Collections.DEPARTMENTS_ID = Departments.DEPARTMENTS_ID AND Collections.COLLECTIONS_ID = :1
By including the COLLECTIONS_ID as a
select-list item in the query, this makes it easy to build View Links to other
view objects later where the master/detail link might need the collection
id.
With the query in place sporting our new binding variable for
the collection ID, I wrote the
setCollectionIdForStylesEditing method in the
StylesAppImpl.java class that you see below, and published it as a custom
method as described in the previous section. The method simply:
Sets the value of the first bind variable in the
query (zero-based!) to the collection ID parameter value passed into the
method,
Executes the query,
and
Accesses the first row of the
result.
/* * Setup the driving department view object by getting the * appropriate collection ID to edit from the client. By performing * the query in the middle tier, we generate no additional client-server * round trips, and the view links automatically coordinate all of * the respective detail views also without any additional round trips. */ public void setCollectionIdForStylesEditing(Number collectionId ) { getDepartments().setWhereClauseParam(0,collectionId); getDepartments().executeQuery(); Row result = getDepartments().first(); }
Since we have designed the data model in the
StyleApp application module that has view link instances
linking the master/detail view object instances, any time the current row
changes in the iterator for the Departments view object, all
related detail queries will be pro-actively executed to retrieve the related
data. The line of code in our custom method above that calls the
first() method on the view object causes the default
iterator to move to that first row. In the process, all of the view linked view
object queries are executed automatically.
The simple bit of code
above, in combination with the default behavior of the view links, and the
Value Messenger implementation, does the job of preparing the entire data model
for the desired collection ID, related department, and all detail queries, as
well as bringing all of that changed data back in bulk to the thin client cache
in the same round trip.
In essence, what we're saying is that when
the Java VM in the thin client executes the call to this remote custom AM
method like this:
/* * Before the call, there is no data yet in the BC4J thin client cache. */ stylesApp.setCollectionIdForStylesEditing(new Number(87065) /* Collection ID */); /* * After the method call, the Value Messenger implementation has returned in bulk * all of the data retrieved as a result of the custom method invocation. */
This means that the data for the
StyleRecords view object, and all other view objects that
our JClient bindings are "bound" to are now in the thin-client cache. All of
this happens in a single round-trip to the middle tier, and as we've now
appreciated, all without writing any "plumbing" code
ourselves to perform this coordination or bulk data-transfer.
The small amount of code that we've written in the client is clearly
identified in the StyleAppForm.java class by the
comments:
/* * ---v----v----v---v--- START OF ADDED CODE ---v----v----v---v--- */
and
/* * ---^----^----^---^--- END OF ADDED CODE ---^----^----^---^--- */
The first bit of code that we'll highlight is the code
that invokes the custom AM method we created above, and which causes all of the
data needed by the form at startup time to be retrieved in that single round
trip. Earlier in the code we retrieved the application module from the panel
binding, and here we cast it to the custom StylesApp
interface so we can call our custom method.
NOTE:
Since we were
only supplied the data for a single collection to test with (#87065), we have
hard-coded the collection ID that is passed into the custom
method.
/* * Cast the appmodule to our custom StylesApp module interface * so we can invoke the custom application module method to * setup the datamodel needed for this form. In this case, we're * passing in all arguments that will be needed in order to * properly setup the data model. All of the data model setup * occurs on the server side, so we don't end up having lots * of thin-client-to-appserver roundtrips to set it up. */ StylesApp stylesApp = (StylesApp)appMod;
/* * The real application would get this collection ID to edit * from some other context... This is just for testing. */ final Number COLLECTION_ID_TO_EDIT = new Number(87065);
/* * No need to do the panelBinding.execute() since we already * performed the execute on the server. */ // panelBinding.execute();
/* * Since when we cancel we are bailing out of the window via * window close or pressing the (Cancel) button, there is no * value in re-executing the iterators in the panel binding. */ panelBinding.setExecuteOnRollback(false);
Since all of the data has been retrieved by the few
lines of code in our custom AM method, we have commented out the call to the
execute() method on the panelBinding that
the wizard generated into the form. Executing this method again would cause
additional, unnecessary round trips. It's generated there by default to make
sure that any panel or form will have data when it appears without further user
coding, but here we're trying to make things work in the most optimal
way.
Also, since the (Cancel) button on the
form presumably will cause the form to exit, there is no value provided by the
default behavior of re-executing the panelBinding upon a rollback of the
transaction. Therefore, to save this step we've called the
setExecuteOnRollback(false).
Fetching the Attribute Properties in Bulk
One of the
powerful features of the BC4J framework is the ability to access view object
and view object attribute metadata in your client application.
This feature makes it
possible to build intelligent, metadata-driven behavior in the client to avoid
hard-coding. One built-in example that's illustrated in the rebuilt sample
application is the use of attribute "Control Hints", like Tooltip and Label.
Figure 14 shows the panel of the View Object Editor in JDeveloper
9.0.3.3 where control hints can be set. JDeveloper manages saving your
translatable tips into a Java message bundle, allowing you to easily translate
the bundles into multiple languages.
Figure 14: Setting Attribute-Level Control Hints Like Label and Tooltip
At runtime, your
application can access the control hints as part of constructing the end-user
UI controls. For example, the following four lines of code in the
PanelStyleGroups.java class setup the label and tooltips for the two comboboxes
based on the control hints that are accessible from the JClient panel binding
object:
// Setup label and tooltip for style group name JLabel and JComboBox labelStyleGroupName.setText( panelBinding.getLabel("StyleGroups", "StyleGroupName", null) ); styleGroupNameComboBox.setToolTipText( panelBinding.getTooltip("StyleGroups", "StyleGroupName", null) ); // Setup label and tooltip for style subgroup name JLabel and JComboBox jLabel1.setText( panelBinding.getLabel("StyleSubgroups", "StyleSubgroupName", null) ); jComboBox1.setToolTipText( panelBinding.getTooltip("StyleSubgroups", "StyleSubgroupName", null) );
Figure 15 shows what the control-hint
driven labels and tooltips look like at runtime.
Figure 15: Control Labels and Tooltips Driven from BC4J Control Hints
The
ApplicationModule supports the fetchAttributeProperties
method that allows your JClient application to fetch all view object metadata
and attribute properties in a single network trip. To get this optimization, we
added the following code in the StyleFormApp.java
class:
/* * Array of view object instance names for which to fetch attribute props */ String[] voNames = new String[]{ "StyleRecords", "StyleAccessoriesDescriptions", "StyleGroups", "StyleSubgroups", "SizeLabelsLOV", "HangLabelsLOV", "BarcodeLabelsLOV", "MainLabelsLOV", "OtherLabelsDescriptions" };
/* * Fetch all of the attribute properties in one round trip */ ApplicationModule appMod = app.getApplicationModule(); appMod.fetchAttributeProperties(voNames,null,null);
Pinning the ViewObjects We're Working With
The view objects being used by the BC4J thin-client
cache are currently held in a WeakVector. We use a
WeakVector so that view objects that are no longer being
actively used by the client can be garbage-collected by the client Java VM if
memory becomes scarce. If one of the view objects gets garbage-collected from
the client cache, and later is accessed again, its metadata will be re-fetched
on demand from the middle-tier business service. As a domino effect, if the
view object is garbage-collected, then all of the client-side metadata related
to it will also be garbage-collected and might need to be fetched again on
demand from the middle tier. No problem, except that this can cause additional
network round trips. You can avoid these unexpected roundtrips by building some
additional application-specific knowledge into your client. For example, by
"pinning" the view objects that your form is using in a local member array they
won't be subject to garbage-collection as long as your form is still running.
This will insure that the metadata, which we fetched in a single round-trip
with the fetchAttributeProperties method above, will not
ever need to be re-fetched on-demand due to client-side garbage collection. It
may mean that your client application uses a little more memory than it might
otherwise use, but the tradeoff is fewer network roundtrips.
In the
StyleAppForm.java class, you will find the additional code
below which simply creates an array of ViewObject
interfaces, and caches these interfaces in a member array.
/* * Cache ViewObjects so that WeakVector garbage collection * does not cause client to "forget" about these views, which * would cause additional round trips to re-fetch the information * about the view objects, as well as the attribute-property information */ ViewObject[] vos = new ViewObject[voNames.length]; for (int i = 0; i < vos.length; i++) { vos[i] = appMod.findViewObject(voNames[i]); }
Pre-Creating Default RowSet and RowSetIterators
The last technique we've used to further reduce
network traffic for application startup in the rewritten sample is the eager,
pre-creation of the default RowSet and RowSetIterator for all of the view
objects that our form will be using.
NOTE:
This
web
log article explains the relationship between a view object and its
default rowset, and between a rowset and its default iterator.
By overriding the create() method in our
StyleAppImpl.java application module class, we can set the
range size for all of the view objects that we know will be fetching all rows.
The act of setting the range size will cause the default
RowSet and its default RowSetIterator to
be created ahead of time. This way, the client will not have to do network
roundtrips to create them later.
The server-side application module
code looks like this. Recall that setting the range size to
-1 means that the range will contain all rows.
/* * Preset the range sizes used by this panel * This will indirectly cause the default * rowset and default rowsetiterator to be created. */ protected void create() { super.create(); //lovs are set to rangeSize of -1. getBarcodeLabelsLOV().setRangeSize(-1); getHangLabelsLOV().setRangeSize(-1); getMainLabelsLOV().setRangeSize(-1); getOtherLabelsDescriptions().setRangeSize(-1); getSizeLabelsLOV().setRangeSize(-1); getStyleGroups().setRangeSize(-1); getStyleSubgroups().setRangeSize(-1); getStyleRecords().setRangeSize(-1); getStyleAccessoriesDescriptions().setRangeSize(-1); }
Network Performance of Rewritten Sample
Setting Up the Test Data Environment
The DatabaseSetup directory in the
OptimizedRoundtrips.zip file contains four SQL scripts. Three of these were
provided by Oracle Consulting:
CreateTabels.sql
DataToTables.sql
Data.sql
As part
of our testing, we slightly modified the sample data we were provided so that
some rows would be retrieved by every query. Specifically, we made the small
modifications contained in the fourth SQL script that is also in this same
DatabaseSetup directory named
FixData.sql.
By creating a new MC_TEST database
user, and running the above four SQL scripts in order, you will have the same
data environment that we used for the final network traffic measurements
below.
Our Final Network Round Trip Measurements
Using all of the techniques documented above, we were
able to reduce the network round trips at startup for the "Style Card" form
from well over 100, to just 15 round trips using JDeveloper 9.0.3.3.
Table 1 documents the 15 round trips and what they
are doing...
Table 1: Network Roundtrips at Startup for Rewritten Example
Round Trips
Remote Method
Calls
Description
6
isConnected (2)
connectToDataSource (1)
getConnectionMetadata (2)
prepareSession
(1)
Sets up the
root application module and its database connection. This is a one-time root AM
startup cost.
1
getLocale
Syncs up the client and server locale for the user
1
fetchAttributeProperties
Bulk-fetches all view object metadata and attribute
properties
3
syncIterator (3)
Syncs each updatable iterator with its middle-tier
counterpart
2
syncIterator (2)
Each navigation binding calls syncIterator to set the
currency on the first row.
2
getEstimatedRowCount (2)
Retrieves estimated row count needed by table binding to
properly size any eventual table scroll
bar
This optimal result occurs when
using the latest JDeveloper 9.0.3.3 maintenance release or later and all of the
view objects have at least one row of data in their result set.
Due to some Swing JScrollPanel issues, when zero rows are returned in a
data source, two additional client-side methods can occur while the JTable
binding is being asked to participate in calculating the number of rows that
can "fit" in the table. Table 2 describes these additional
round trips that might appear in your application where no data is queried.
Table 2: Additional Remote Method Calls Related to Table Bindings
Remote Method Call
Description
setRangeSize
The Swing JScrollPanel tries to calculate the size of the
scroll panel's scroll bar based on the number of rows that will fit in the
display area. The table binding then adjusts the range size to match that
number of rows that can display.
scroll
In JDK
1.3, the Swing JScrollPanel generates an extra scroll event when there is no
data in the table. (Could likely be optimized out for 9.0.3.4 maintenance
release). Doesn't appear to happen any more for JDK
1.4.
NOTE:
While this paper focuses
on tips to apply for the 9.0.3.X series of JDeveloper and BC4J, it's
interesting to note that we have been working hard over the last year and a
half to further optimize the thin-client network traffic. We have introduced a
new "Batch Mode" in JDeveloper 10g and the versions of the JClient and BC4J
frameworks that accompany it in order to further consolidate what number and
types of operations we are able to perform in a bundled way as part of a single
network round-trip.
Tips for Quicker Development
While working on this sample application, I
used one design-time trick to speed up my development/test/profile cycle that
is worth sharing. When you create a BC4J Deployment Profile for deploying your
application module as an EJB Session Bean, the BC4J Design Time extension
creates you two additional application module configurations: one for remote
EJB access, and one for accessing the EJB Session Bean inside the embedded OC4J
container in JDeveloper. For example, for the StyleApp application module,
these two extra configurations are named StylesApp9iAS and
StylesAppBM9iASEmbedded respectively.
Normally
you can click on the *.cpx file in your view project, like
the View.cpx file in our View.jpr project
for the rewritten sample, and see the client data model definitions that it
contains in the Structure Window. By selecting one of these client data models
and selecting the Modify... from the context menu,
you can easily change the configuration that your JClient form will use at
startup to test in local mode (2-Tier) or remote mode (3-Tier).
In
JDeveloper 9.0.3, the configuration name for the embedded
OC4J deployment is unfortunately not present in the poplist to choose from.
However, with a quick edit of the View.cpx file outside of
JDeveloper, we can set the name of the Configuration
attribute of the <Session> element to have the value
"StylesAppBM9iASEmbedded" as shown below.
Then next time you load the project in JDeveloper and
run your form from inside the IDE with the *.cpx file changed this way, you can
run, debug, or profile your 3-tier application without having to go through the
physical deployment step to an external application server each time the
middle-tier project undergoes any changes.
To run your EJB Session
Bean, simply find it the navigator as shown in Figure 16 and
run or debug it.
Figure 16: Running the EJB Session Bean for Your AM on the Embedded Container
With the
server-side of your 3-tier application running in the embedded OC4J container
inside JDeveloper, and with the SalesApp client data model inside your View.cpx
file pointing at the StylesAppBM9iASEmbedded configuration,
you can then Run, Debug, or Profile your client application without the hassle
of deployment each time. If you make changes to your middle-tier components,
just stop the EJB in the embedded container using JDeveloper's Run
| Terminate... menu, then re-run it.
JDeveloper even
allows you to run, debug, and/or profile multiple applications at once so you
can pick any combination of running, debugging, or profiling either or both of
your thin-client and middle tier components.
When you're ready to
do the real deployment, deploy to the external application server, change your
client data model to use the SalesApp9iAS configuration
again, and re-run the form.
Other Issues
We close with a few brief notes on other observations that we've made on
the initial application that was supplied to us:
Optimize View Object Fetch Sizes to Reduce
AppServer-to-Database Round Trips
The "Fetch Size"
parameter on the "Tuning" panel of the View Object editor controls how many
rows at a time should be fetched via JDBC from the database to the application
server where the BC4J middle-tier components are running. By default, view
objects of a fetch size of one. If you write a query that you know is being
used to populate a list of values, then set the fetch size to a larger number
to make fewer round trips to the database to grab the data. If you leave it at
the default of 1, you'll make as many database round trips as you have rows to
fetch. While this will not affect the thin-client-to-appserver round trips, it
does incur a performance penalty for the extra round-trips between application
server and database.
Minimize
Database Connections by Using Root AppModules Correctly
Each time you create a top-level JUApplication object
in JClient, it will correspondingly create a top-level (or "root") application
module that will have its own database connection. Sometimes you want this when
different JClient forms need to have completely independent transactions (that
is, a rollback in one does not affect pending work in another). However, BC4J
and JClient were carefully design to work together and BC4J's feature to
support nested application module instances is paralleled by JClient's ability
to create nested JUApplication objects. By leveraging nested application
modules and nested JUApplications you can decide which forms should share a
transaction as well as making it more easy to load and dispose of application
modules as the forms that use them are dismissed. It requires some work on the
developer's part to exploit these framework features, but as we've seen above,
the payback can be worth the effort in manageability and performance.
Also, we've seen in the document here that by design a JClient form
should be assembled of instances of JClient panels (except for the simplest of
forms that might only need a single form with no extra panels). These panels
are designed to all share a single panel binding from the form that contains
them. The JDeveloper UI Editor and JClient property editors cooperate to set
this up for you automatically when you use the "Data Panel" tool from the
"JClient Controls" component palette page.
Use bind variables
Wherever possible,
use bind variables in your view object SQL statements instead of concatenati