HOWTO: Implementing a View Object with Multiple Updateable Dependent Entity Objects
How To Implement a View Object with Multiple Updateable Dependent Entity Objects
An Oracle JDeveloper How To Document
November 2002
Product Versions
This document was written for Oracle9i JDeveloper versions 9.0.2 and 9.0.3.
Contents Overview Setting Up the Tables for the Sample Project Setting Up Components to Represent the Business Domain Objects Setting Up Components for the Application Data Model Overriding a Few Framework Methods to Finish the Job Controlling of the Posting Order of Employee and Department Entity Instances Pre-Populating the Foreign Key Attribute in New Employee Instances Refreshing Foreign Key Values in New Employees Once Final Department Id is Assigned Testing and Debugging the Application Module With Diagnostics On Dealing with a Final Issue For the Create Case Deciding How Update and Delete Should Work Conclusion
Overview
BC4J makes most
common application-building use cases easy without writing any "plumbing" code
by implementing many helpful J2EE design patterns for you. In particular, BC4J
view objects easily handle updateable join queries involving data from multiple
entity objects, and using declarative properties you can indicate how each
entity object involved in the view object's query should behave. For typical
cases, BC4J's automatic coordination of view object rows and underlying entity
object instances handles the job just fine, but when your application requires
a customized treatment for how view object rows and entity objects collaborate,
this article illustrates an example of a common case that can be implemented by
strategically overriding a few framework methods.
Setting Up the Tables for the Sample Project
Let's assume
we need to build a user interface that allows the user to provide the data for
creating both a new department and the first new employee in that department.
To simplify this task, we can build a single view object that joins the
relevant employee and department data to be created.
We'll work
with the standard DEPT table, created using this familiar DDL:
CREATE TABLE DEPT ( DEPTNO NUMBER(2), DNAME VARCHAR2(14), LOC VARCHAR2(13), CONSTRAINT DEPT_PK PRIMARY KEY (DEPTNO));
As well as the EMP table, created like this:
CREATE TABLE EMP ( EMPNO NUMBER(4) NOT NULL, ENAME VARCHAR2(10), JOB VARCHAR2(9), MGR NUMBER(4), HIREDATE DATE, SAL NUMBER(7, 2), COMM NUMBER(7, 2), DEPTNO NUMBER(2), CONSTRAINT EMPLOYEE_PK PRIMARY KEY (EMPNO), CONSTRAINT EMPLOYEE_WORKS_IN FOREIGN KEY (DEPTNO) REFERENCES DEPT);
To make the example more interesting, let's create the
following two sequences:
CREATE SEQUENCE dept_table_Seq START WITH 41;
and
CREATE SEQUENCE emp_table_seq START WITH 1000;
and add the following two triggers to assign the primary
keys for the DEPT and EMP tables from
their respsective sequence created above. The trigger for the DEPTNO sequence
assignment looks like this:
CREATE TRIGGER dept_table_befins BEFORE INSERT ON dept FOR EACH ROW BEGIN -- When using a DBSequence domain, BC4J supplies a temporary -- negative id for the DETPNO column during insert IF (:new.deptno IS NULL OR :new.deptno < 0) THEN SELECT dept_table_seq.nextval INTO :new.deptno FROM dual; END IF; END;
while the trigger for the EMP table
looks like this:
CREATE OR REPLACE TRIGGER emp_table_befins BEFORE INSERT ON emp FOR EACH ROW BEGIN -- When using a DBSequence domain, BC4J supplies a temporary -- negative id for the EMPNO column during insert IF (:new.empno IS NULL OR :new.empno < 0) THEN SELECT emp_table_seq.nextval INTO :new.empno FROM dual; END IF; END;
With the tables setup, let's move on to create the
sample project.
Setting Up Components to Represent the Business Domain Objects
| NOTE:
|
The steps described in this
paper correspond to the JDeveloper 9.0.3 Production release of BC4J and its
design time wizards/editors.
|
After creating a new empty
project in a workspace, use the New Business Component
Package... menu choice on the context menu for the project to create
a new BC4J package named otn.howto.multiple.
By
using the right-mouse menu on this newly created package, use the BC4J entity
object wizards to create an Employee entity object mapped to
the EMP table. In the "Attribute Settings" panel, change the
datatype of the Empno attribute from
Number to DBSequence. Using this special
domain tells BC4J that you will be assigning your primary key from a database
sequence.
Repeat the same steps to create a
Department entity object mapped to the
DEPT table, and as you did with the
Employee entity, change the datatype of the
Deptno attribute from Number to
DBSequence.
Since employees can exist
independently of departments, the BC4J entity objects that
model these two business domain concepts should be
associated, but not composed. So use the BC4J association
wizard to create a new EmployeeWorksInAssoc association that
captures the relationship between the source
Department.Deptno attribute and the destination
Employee.Deptno attribute. On the "Association Properties"
panel, edit the name of the destination association accessor from
Employee to the plural Employees to
better reflect that calling the accessor will return a collection of employees.
By creating this association, we enable the ability to programmatically
navigate from any Department entity instance to its related
collection of Employee entity instances, and vice
versa.
Setting Up Components for the Application Data Model
Next,use the View Object wizard to create a new view object.
- On the "Name" panel, give the new view object the
name
CreateDepartmentAndFirstEmployee.
- On the "Entity Objects" panel of the wizard, select the
Department entity and "shuffle" it into the selected list.
-
Repeat to add the Employee
entity.
With the focus still on the selected
Employee entity in the "Selected" list, notice the two
checkboxes in the bottom right of the dialog. For the second and subsequent
entities that participate in a view object's result "row", these checkboxes are
defaulted to be "Read-Only" and for "Reference". This is based on the most
frequent usage pattern in application building, but enabling the ability to
have multiple, updateable entities in a view row is as simple as unchecking
these two boxes.
- Since we are building a view
object to create both department and its first employee, uncheck the
"Read-Only" checkbox as well as the "Reference" checkbox. This will make both
the Department-related attributes and the Employee-related attributes in the
new view row updateable.
- Continue on to the
"Attributes" panel and select all of the attributes from both entities and
"shuffle" them into the select list of attributes for the view
object.
- On the "Attribute Settings" panel,
select the
Deptno1 attribute (that was automatically renamed
to avoid colliding with the Deptno attribute from the
Department entity) and edit its current name to change it to
DeptnoForEmp for better readability.
- We're done, so click (Finish) to create the new view
object.
Finally, use the Application
Module Wizard to create a new application module named
TestModule, and add an instance (or
usage) of the CreateDepartmentAndFirstEmployee view object
component to the data model. Use the "Instance Name" field at the bottom right
to change the name of the this view object instance from
" CreateDepartmentAndFirstEmployee1" to convenient (and
shorter!) name like " test" for the test we're about to do.
Just like we have freedom to name the member variables in a class anything we
want, we can name the "member instances" of view objects in an application
module any convenient names we want. Again just like class member variable
names, these view object instance names give us a handy local name, in the
context of a particular application module, to refer to an instance of our view
object (definition) that we want to work with at runtime.
Overriding a Few Framework Methods to Finish the Job
If
Department and Employee entities were
related by a composition association, and if we were
creating the new Department entity through a
DepartmentView that was viewlinked to a separate
EmployeeView, we would be done here since in that case BC4J
automatically handles all of the details of:
-
Coordinating the posting order of composed
entities
to insure that a containing parent is inserted
before its contained children to avoid any database-level constraint
violations,
-
Populating contained
child entities foreign key attributes on creation
to
avoid creating "orphaned" child entity instances, and
-
Refreshing the foreign key values of contained child
entities for DBSequence-valued primary keys
to
synchronize the transition from temporary in-memory key values to the final
database-assigned key values.
However our
CreateDepartmentAndFirstEmployee example requires some
customization of the default framework behavior because we have two differences
from the situation that BC4J can handle automatically:
- our
Department and
Employee entities are not composed, but just associated,
and
- we're creating both parent and related
child entities in a single view object instead of in two
separate view objects.
Controlling of the Posting Order of Employee and Department Entity Instances
The first issue we will tackle is controlling the
posting order. We want to make sure that any new Employee
entities being created are posted to the database only after their associated
Department entity instance gets inserted. This will
guarantee that we won't ever receive a database constraint violation that would
be raised if a new row in the EMP table were to be inserted before its "parent"
row in the DEPT table. To insure that this happens, we override the
postChanges() framework method in the
EmployeeImpl.java class for the Employee
entity object like this:
/** [In file: EmployeeImpl.java] * * Force new DepartmentImpl instance to post before new EmployeeImpl * instance if Employee has a related Department */ public void postChanges(TransactionEvent e) { // If this entity instance is NEW (meaning it's going to get inserted) if (getPostState() == STATUS_NEW) { // Get the associated Dept instance DepartmentImpl departmentForThisEmployee = getDepartment(); // If the associated Dept instance is still NEW, post it first if (departmentForThisEmployee != null && departmentForThisEmployee.getPostState() == STATUS_NEW) { departmentForThisEmployee.postChanges(e); } } super.postChanges(e); }
The code is straightforward. It checks to see if the
current Employee instance being posted has the status of
STATUS_NEW, which means it was newly created in the current
transaction and is about to be inserted into the underlying table. If we're
inserting a new entity, we access its related Department entity instance (if it
has one) using the getDepartment() association accessor. If
this returns a non-null value, it will be an instance of the
DepartmentImpl class, which we can then use to check whether
its post-state is also STATUS_NEW. If it is, then we
explicitly call the postChanges() method on this
Department instance to post it first. Then, we call
super.postChanges() to post the current
Employee entity being processed by the BC4J framework's
post-cycle coordination.
Pre-Populating the Foreign Key Attribute in New Employee Instances
When a view object's query
result "row" includes data from multiple entity objects, the act of creating a
new row in the view object automatically results in creating new instances of
any of the updateable entity objects participating in that
row. If we want to customize anything about how a view object row is created,
we just need to:
- Choose to use a View Row class for our view object (which by default
won't get created), and
- Override the
create() method in that View Row class to modify the row
creation behavior as desired.
Start by
editing the CreateDepartmentAndFirstEmployee view object,
and visiting the Java panel. Check the "View Row Class" checkbox, and press
(Finish). This will add a skeleton
CreateDepartmentAndFirstEmployeeRowImpl.java file to your
project, nested beneath the CreateDepartmentAndFirstEmployee
view object component in the navigator.
We override the framework
create() method as shown in the following code:
/** [In file: CreateDepartmentAndFirstEmployeeRowImpl.java] * * By default, the framework will automatically create the new * underlying entity object instances that are related to this * view object row being created. * * We override this default view object row creation to explicitly * pre-populate the new (detail) EmployeeImpl instance using the * new (master) DepartmentImpl instance. Since all entity objects * implement the AttributeList interface, we can directly pass the * new DepartmentImpl instance to the EmployeeImpl create() method * that accepts an AttributeList. */ protected void create(AttributeList attributeList) { // The BC4J framework will already have created "blank" entity instances DepartmentImpl newDepartment = getDepartment(); EmployeeImpl newEmployee = getEmployee(); try { // Let department "blank" entity instance to do programmatic defaulting newDepartment.create(attributeList); // Let employee "blank" entity instance to do programmatic defaulting // passing in new DepartmentImpl instance so its attributes are // available to the EmployeeImpl's create method. newEmployee.create(newDepartment); } catch (JboException ex) { newDepartment.revert(); newEmployee.revert(); throw ex; } catch (Exception otherEx) { newDepartment.revert(); newEmployee.revert(); throw new RowCreateException(true /* EO Row? */, "Department" /* EO Name */, otherEx /* Details */); } }
However, the above code won't compile until we
consciously expose both the Department entity's
create() method and the Employee entity's
create() method. The BC4J framework base class for entity
objects, oracle.jbo.server.EntityImpl, has the
create() method with protected member
access. This means that any subclass of EntityImpl has
access to the method, but not other classes by default.
By
overriding this base class create() method in our
otn.howto.multiple.DepartmentImpl subclass, maintaining the
protected access, we enable any other class in our
otn.howto.multiple package access to it. If our view object
were in another package, we'd need to "widen" the access of the
create() method to public.
The overridden create() method in
DepartmentImpl class looks like this:
/** [In file: DepartmentImpl.java] * * Overide create method to extend protected access to * other components in this package. */ protected void create(AttributeList attributeList) { super.create(attributeList); }
For the Employee entity, our
overridden create() method will be a little more
interesting. Since the overridden create() method at the
view object level has been coded to pass in the new
DepartmentImpl instance as a parameter to the
EmployeeImpl's create() method, we can
refer to any attributes in the DepartmentImpl instance as
part of our programmatic defaulting logic for a new
Employee.
Our overridden
create() method below for the Employee
retrieves the DepartmentImpl's Deptno
attribute as a DBSequence, and then retrieves the sequence
number from that using getSequenceNumber(). It uses that
value to set the foreign key Deptno attribute on the current
EmployeeImpl entity instance.
/** [In file: EmployeeImpl.java] * * Retrieve the DBSequence-valued "Deptno" Attribute from the new * DepartmentImpl instance passed-in, and pre-populate the employee's * "Deptno" foreign key attribute value with the (initially * temporary) value of the DepartmentImpl's Deptno attribute value. * * The overridden refreshFKInNewContainees() method in DepartmentImpl * combined with our overridden postChanges() method here will cooperate * to force the new department to post first, and proactively refresh * this temporary foreign key value to the final (database-trigger-assigned) * sequence number. */ protected void create(AttributeList attributeList) { super.create(attributeList); DBSequence deptno = (DBSequence)attributeList.getAttribute("Deptno"); if (deptno != null) { setDeptno(deptno.getSequenceNumber()); } }
This takes care of the automatic population of the
foreign key attribute. The last step is handling the refreshing of the foreign
key attribute on Employee when the database-assigned Deptno
value on Department gets refreshed from the temporary value to the definitive
sequence number.
Refreshing Foreign Key Values in New Employees Once Final Department Id is Assigned
Here is an
overview of our strategy to implement the refreshing of foreign key values in
new Employee entity instances when a new
Department entity instance gets its temporary primary key
refreshed by the database-sequence-assigned value:
-
For any new Department, just before posting itself we will call the
getEmployees() to hold onto the collection of related
EmployeeImpl instances.
At this point in time,
the EmployeeImpl instances will be related to the
Department based on the temporary primary key that the BC4J
framework assigned to the DBSequence-valued
Department.Deptno attribute at entity creation time. We'll
implement this by overriding the framework method
postChanges() and then calling
super.postChanges() to have the framework do its default
behavior after we cache a copy of an iterator to the children
employees.
-
For any related employees, update
their temporary foreign key value to the final value of
the database-assigned Department primary key.
To
accomplish this, we'll override the framework method
refreshFKInNewContainees() that gets called automatically by
the framework to allow a new entity to refresh the foreign key values in its
related children. By default, the framework only refreshes children entities
that are related by a composition, so since Employee is just
associated but not composed with Department, we write a
little code in this method to do the job
ourselves.
The code to accomplish these
two steps is shown below:
/** [In file: DepartmentImpl.java] * * Used to temporarily cache an iterator to the set of employees in * the department before it primary key gets updated */ private RowIterator newEmpsBeforePost = null; /** * Hold onto iterator to existing employees in the Department * before calling super.postChanges() that will cause the deptno to change * to the database-trigger-assigned sequence number. We hold onto the * iterator at this point because if we wait until the * refreshFKInNewContainees() method below, calling the getEmployees() * method will return no entities since they still have the temporary * new department id as their foreign key value. */ public void postChanges(TransactionEvent e) { // If this entity instance is NEW (meaning it's going to get inserted) if (getPostState() == STATUS_NEW) { newEmpsBeforePost = getEmployees(); } super.postChanges(e); } /** * BC4J framework invokes this method to allow a newly inserted master entity * to update its new detail entities. Typically you do not have to override * this method because the base implementation in oracle.jbo.server.EntityImpl * will automatically handle this refreshing when detail entity instances * are related to new "master" entity instances by composition. In this * example, Department and Employee are related by association but not * by the stronger composition kind of "strong" containment relationship * (it's an optional property of an association to be containership) * * Iterate over the employees in the new department and refresh their * foreign key value to be the database-refreshed sequence value. */ protected void refreshFKInNewContainees() { Number newDeptno = getDeptno().getSequenceNumber(); if (newEmpsBeforePost != null) { while (newEmpsBeforePost.hasNext()) { EmployeeImpl curEmp = (EmployeeImpl)newEmpsBeforePost.next(); curEmp.setDeptno(newDeptno); } newEmpsBeforePost = null; } }
With this code, we should be able to use the BC4J Tester
to test our application module.
Testing and Debugging the Application Module With Diagnostics On
To test our
application module, let's turn on BC4J diagnostic information so that we can
observe all of the DML operations that BC4J is doing. To accomplish
this:
- Double-click on the project node to
bring up the Project Properties dialog.
- Click
on the "Runner" node at the left to show the "Runner" panel
-
In the "Java Options" text box, add the Java VM command-line
argument: -Djbo.debugoutput=console
This tells
the Java VM to set the jbo.debugoutput System parameter to
the value console. The BC4J framework will notice this setting and dump
diagnostic information to System.out, which will
conveniently appear in the JDeveloper log
window.
Let's also stop the action at a
breakpoint so we can observe what's happening under the covers at runtime. Set
a breakpoint on the following line in the overridden
refreshFKInNewContainees() method in the
DepartmentImpl.java source file by clicking on its line
number in the left margin of the code editor:
if (newEmpsBeforePost != null) {
A small red "ball" icon should appear to show that
you've set the breakpoint.
To run the BC4J Tester in debug mode so
that it picks up our Java VM parameters to enable diagnostics and will stop at
our breakpoint, do the following:
- Expand the
node in the System Navigator for the
TestModule application
module, to reveal its implementation files.
- Right-mouse on the
TestModuleImpl.java file and select
Debug TestModuleImpl... from the context
menu.
You should see the initial
diagnostics information like this in the Log window:
Debugger connected to local process. Diagnostics: (BC4J Bootstrap) Routing diagnostics to standard output (use -Djbo.debugoutput=silent to remove) [00] Diagnostic Properties: Timing:false Functions:false Linecount:true Threshold:6 [01] CommonMessageBundle (language base) being initialized [02] Stringmanager using default locale: 'null' : [158] Successfully logged in [159] JDBCDriverVersion: 9.0.1.4.0 [160] DatabaseProductName: Oracle [161] DatabaseProductVersion: Oracle9i Enterprise Edition Release 9.2.0.1.0 - Production
When the BC4J Tester appears, select the view object
instance named test, and double-click it to show the first
row in its query result.
Click the (+) button in the toolbar to
create a new row in this view object. You should immediately see the effect of
the DBSequence-valued primary key attributes for the Deparment and Employee
entity objects by observing the temporary primary key values in the
Deptno and Empno fields. You should also
be able to see the result of our overridden create() method
on the CreateDepartmentAndFirstEmployeeRowImpl view object
row class by noticing that the foreign key attribute named
DeptnoForEmp is in sync with the temporary
Dept.Deptno value. BC4J intentionally assigns negative
numbers as temporary primary key values since Oracle database sequences cannot
produce negative numbers. This avoids any possible conflict between temporary
keys and their final, database-sequence-assigned values.
Enter a
new value for the Dname attribute and the
Ename attribute, and press the Commit button in the toolbar.
It's the button with the clockwise, green arrow on it.
The debugger
should stop at your breakpoint, and you can step though the code that updates
the foreign key attribute in the new employee entity instance to the refreshed
database-sequence-assigned value. After you let the code run to completion, you
can observe the INSERT statements in the Log window that the BC4J diagnostics
print out for your inspection.
Dealing with a Final Issue For the Create Case
To witness one problem that's left to
deal with, repeat the above steps in the tester of creating a new row, but
before setting any value for Dname or Ename (or any other
attribute other than the automatically assigned keys), immediately press the
Commit button. You will get a DMLException raised because the framework has
tried to insert the new Employee entity instance without
trying to insert the new Department instance. Let's
understand why...
One subtle issue to understand is the difference
between the STATUS_INITIALIZED and the
STATUS_NEW for newly created entity object instances. In a
nutshell, in JDeveloper 9.0.3, the BC4J framework introduced the ability to
distinguish between a newly created entity which had only
defaulted values for its attributes, and an entity which has had at
least one of its attributes modified by a client application. These
two states are reflected by the two entity states
STATUS_INITIALIZED and STATUS_NEW,
respectively. Only when an entity instance has the status
STATUS_NEW does the framework add it to the list of entities
that need to be inserted at the next database posting cycle that gets
initiated.
When an entity instance is created, it will
automatically have any of its design-time default values assigned to the
attributes, if any were specified. In addition, the framework method
create() in an entity object provides a "hook" for doing
programmatic defaulting of attributes for newly created entity instances as
well. In the second testing example above, the new
Department instance still had the
STATUS_INITIALIZED since we never set any of its attributes
other than the framework-assigned temporary key. This means the framework
didn't consider the new Department instance as ready for
inserting yet. In the first testing example, we did provide a value for the
Dname attribute, so the entity was transitioned to
STATUS_NEW and everything worked correctly.
We
have a few possible choices to solve the problem:
-
We could mark the Dname attribute of the
Department entity as mandatory by editing the entity and
setting this attribute property.
In practice, a department without
a name is probably not what we want, so this is the easiest solution. This
would force the user to provide a value for this required attribute before it
would be allowed to be posted successfully. Since they would have to set a
value, the new Department entity's status would be
STATUS_NEW, and the insert would work fine.
-
We can force the new
Department entity instance to have
STATUS_NEW in our overridden create()
method at the entity object level.
This would be appropriate if we
wanted new Department entities to
always be inserted even if only
defaulted attributes are provided. This behavior would be consistent in every
view object in which this entity participates.
To implement this,
we would augment the DepartmentImpl class'
create() method to look like this:
/** [In file: DepartmentImpl.java] * * Overide create method to extend protected access to * other components in this package. */ protected void create(AttributeList attributeList) { super.create(attributeList); // Setting Deptno to itself forces entity to be STATUS_NEW instead of STATUS_INITIALIZED setDeptno(getDeptno()); }
-
We can force the new
Department entity instance to have
STATUS_NEW in our overridden create()
method at the view object level.
This would be
appropriate if we wanted to force the STATUS_NEW behavior on
the Department entity instance only when created through
this particular view object. We've already overridden this method to implement
the rest of the behavior we're studying in this example, so doing this would be
one additional line of code that, for example, set the value of the
Deptno to itself.
To implement this approach, we
would augment the overridden create() method on the
CreateDepartmentAndFirstEmployeeRowImpl class to look like
this:
try { // Let department "blank" entity instance to do programmatic defaulting newDepartment.create(attributeList);
// Setting Deptno to itself force entity to be STATUS_NEW instead of STATUS_INITIALIZED newDepartment.setDeptno(newDepartment.getDeptno());
// Let employee "blank" entity instance to do programmatic defaulting // passing in new DepartmentImpl instance so its attributes are // available to the EmployeeImpl's create method. newEmployee.create(newDepartment); }
With this last subtlety of the
"create" case taken care of, we should really investigate two final
issues:
- How should this view object work
for deletes?
- How should this view object work
for updates?
Deciding How Update and Delete Should Work
By default, if multiple entity
usages in the view object are marked as updateable (that is,
not Read-Only and not Reference) then
all of the typical CRUD operations are still supported: create, update, and
delete. We've seen above that the create case required some extra care when the
multiple updateable entities are dependent and require foreign-key
coordination. But what about the update and delete cases?
The
delete case is the easiest to deal with. If you want to prevent deletion in
this view object, simply override the remove() method at the view object row
level and throw an exception like this:
/** [In file: CreateDepartmentAndFirstEmployeeRowImpl.java] * * Deletes are not allowed in this view */ public void remove() { throw new JboException("Deletion is not allowed in this view"); }
The update case also does not present any particular
problems, unless you don't want to allow updates at all.
Using the BC4J tester with the example we have so far, you should be able to
query up a row and modify both Dname and
Ename values in a row of this view object, and commit the
changes. BC4J's normal framework behavior will update underlying entities and
post the updates appropriately to the necessary database tables.
However, if we want to prevent updates completely, we can override the
setAttributeInternal() framework method at the view object
row level to accomplish this. This method is the framework "pinchpoint" for any
attribute settings at the view object row level, so its the perfect place to
put some customized logic if we need that at the view object level. Of course,
we can also choose to override the setAttributeInternal()
method at the entity object object level, too, if what we want to accomplish is
applicable to all uses of a particular entity object in
any view object. That would be a little draconian for our
example here, so let's customize the attribute settability at the view object
row level to implement desired behavior.
The first thing that might
come to mind might be to repeat the logic we did above to just throw an
exception like this:
/** [In file: CreateDepartmentAndFirstEmployeeRowImpl.java] * * WRONG * ~~~~~ * * Does coding an override like this prevent updates? */ protected void setAttributeInternal(int index, Object val) { throw new JboException("Update is not allowed in this view"); }
However, since all attribute setting goes through this
method, doing the above would completely prevent all attribute setting, even
for new rows! So this is a little too "strong" an implementation. We need to
allow setting attributes when the row in the view object is in the STATUS_NEW
or STATUS_INITIALIZED state.
So far we've looked at the status of
entity instances, but haven't directly referenced the status of a view object
row. This is because BC4J consciously only tracks the status of entity objects
in the cache, and not view object rows. This is due to the inherent complexity
in establishing a definition for view object row state when your view object
involves multiple underlying updateable entity object "parts". For example, if
a view object involves three updateable entity usages, a given row of the view
object might have Entity1 with STATUS_NEW, Entity2 with STATUS_MODIFIED, and
Entity3 with STATUS_UNMODIFIED. In this case, what is the status of this
"hybrid" view object row? Not clear in general.
Of course, for any
specific scenario that you implement, as the developer "in
charge" you can easily decide how you want to handle the meaning of view row
status by turning around and looking at the status of the underlying entity
instances. In any view object row class, BC4J auto-generates convenience
methods that allow you to access the individual, underlying entity object
instances that comprise the row. We used this ability above in overriding the
view object row's create() method, and we'll use it here to
add a custom method isNewOrInitRow() to our
CreateDepartmentAndFirstEmployeeRowImpl class. The method
looks like this:
/** [In file: CreateDepartmentAndFirstEmployeeRowImpl.java] * * Return true if both underlying Department entity instance and underlying * Employee entity instance are in either the NEW or INITIALIZED status. */ private boolean isNewOrInitRow() { return isNewOrInit(getDepartment()) && isNewOrInit(getEmployee()); }
Which uses getDepartment() and
getEmployee() to access the underlying entity instances for
this view object row, and for simplicity makes use of the following helper
method to determine whether each is in either the New or Initialized
state:
/** [In file: CreateDepartmentAndFirstEmployeeRowImpl.java] * * Return true if the status of the entity passed in is either * NEW or INITIALIZED. */ private boolean isNewOrInit(EntityImpl e) { byte status = e.getPostState(); return status == Entity.STATUS_NEW || status == Entity.STATUS_INITIALIZED; }
With these methods in place, we can now code our
overridden setAttributeInternal() method with the proper
conditional behavior:
/** [In file: CreateDepartmentAndFirstEmployeeRowImpl.java] * * Only allow attribute setting in rows that are new or initialized state */ protected void setAttributeInternal(int index, Object val) { if (isNewOrInitRow()) { super.setAttributeInternal(index, val); } else { throw new JboException("Update is not allowed in this view"); } }
This will stop any attribute assignments in this view
row for rows that are not new or initialized. For a more customized treatment,
you could imagine a more elaborate scheme to check underlying entity status and
make a more fine-grained decision about conditional attribute settability. It's
just an extension of the simpler technique we've used here.
As a
final touch of class, if we want clients to be able to proactively inquire
about our view rows per-attribute updateability, since we have implemented a
conditional attribute settability scheme above, we should complete the job by
overriding the framework method isAttributeUpdateable() to
reflect the same conditional updateability. The simple implementation looks
like this:
/** [In file: CreateDepartmentAndFirstEmployeeRowImpl.java] * * Give clients who inquire about attribute updateability an accurate * answer based on our conditional attribute settability implemented here. */ public boolean isAttributeUpdateable(int index) { // If the row is new or initialized, according to our custom // definition of view row state, then let the superclass return // the updateability of this attribute (might be updateable, might not). if (isNewOrInitRow()) { return super.isAttributeUpdateable(index); } // Otherwise, return false to indicate the attribute's not updateable. else { return false; } }
Now, we should be able to use the BC4J tester to:
- Create new rows that insert a new department a
new employee,
- Attempt deletion of a row and
be prevented from doing so,
- Notice that we
are prevented from updating attributes in a row that is not
new
The BC4J Tester, and all of the BC4J
client data-binding technologies, use the
isAttributeUpdateable() to inquire about the updateability
of an attribute to render it appropriately on the
screen.
Conclusion
We've
seen in this article that when your application requires a customized treatment
for how view object rows and entity objects coordinate, customizing the default
behavior of the requires just a few lines of code, in the right overridden
framework methods.
|