Oracle® Application Development Framework Developer's Guide For Forms/4GL Developers 10g (10.1.3.1.0) Part Number B25947-01 |
|
|
View PDF |
On their own, view objects and entity objects simplify two important jobs that every enterprise application developer needs to do:
Work with SQL query results
Modify and validate rows in database tables
Entity-based view objects can query any selection of data your end user needs to see or modify. Any data they are allowed to change is validated and saved by your reusable business domain layer. The key ingredients you provide as the developer are the ones that only you can know:
You decide what business logic should be enforced in your business domain layer
You decide what queries describe the data you need to put on the screen
These are the things that make your application unique. The built-in functionality of your entity-based view objects handle the rest of the implementation details. You've experimented above with entity-based view objects in the Business Components Browser and witnessed some of the benefits they offer, but now it's time to understand exactly how they work. This section walks step by step through a scenario of retrieving and modifying data through an entity-based view object, and points out the interesting aspects of what's going on behind the scenes. But before diving in deep, you need a bit of background on row keys and on what role the entity cache plays in the transaction, after which you'll be ready to understand the entity-based view object in detail.
As shown in Figure 7-21, when you work with view rows you use the Row
interface in the oracle.jbo
package. It contains a method called getKey()
that you can use to access the Key
object that identifies any row. Notice that the Entity
interface in the oracle.jbo.server
package extends the Row
interface. This relationship provides a concrete explanation of why the term entity row is so appropriate. Even though an entity row supports additional features for encapsulating business logic and handling database access, you can still treat any entity row as a Row
.
Recall that both view rows and entity rows support either single-attribute or multi-attribute keys, so the Key
object related to any given Row
will encapsulate all of the attributes that comprise its key. Once you have a Key
object, you can use the findByKey()
method on any row set to find a row based on its Key
object.
Note: When you define an entity-based view object, by default the primary key attributes for all of its entity usages are marked with their Key Attribute property set totrue . It is best practice to subsequently disable the Key Attribute property for the key attributes from reference entity usages. Since view object attributes related to the primary keys of updatable entity usages must be part of the composite view row key, their Key Attribute property cannot be disabled. |
An application module is a transactional container for a logical unit of work. At runtime, it acquires a database connection using information from the named configuration you supply, and it delegates transaction management to a companion Transaction
object. Since a logical unit of work may involve finding and modifying multiple entity rows of different types, the Transaction
object provides an entity cache as a "work area" to hold entity rows involved in the current user's transaction. Each entity cache contains rows of a single entity type, so a transaction involving both the User
and ServiceHistory
entity objects holds the working copies of those entity rows in two separate caches.
By using an entity object's related entity definition, you can write code in an application module to find and modify existing entity rows. As shown in Figure 7-22, by calling findByPrimaryKey()
on the entity definition for the ServiceRequest
entity object, you can retrieve the row with that key. If it is not already in the entity cache, the entity object executes a query to retrieve it from the database. This query selects all of the entity object's persistent attributes from its underlying table, and find the row using an appropriate WHERE
clause against the column corresponding to the entity object's primary key attribute. Subsequent attempts to find the same entity row by key during the same transaction will find it in the cache, avoiding a trip to the database. In a given entity cache, entity rows are indexed by their primary key. This makes finding and entity row in the cache a fast operation.
When you access related entity rows using association accessor methods, they are also retrieved from the entity cache, or are retrieved from the database if they are not in the cache. Finally, the entity cache is also the place where new entity rows wait to be saved. In other words, when you use the createInstance2()
method on the entity definition to create a new entity row, it is added to the entity cache.
Figure 7-22 During the Transaction ServiceRequest, Entity Rows are Stored In ServiceRequest Entity Cache
When an entity row is created, modified, or removed, it is automatically enrolled in the transaction's list of pending changes. When you call commit()
on the Transaction
object, it processes its pending changes list, validating new or modified entity rows that might still be invalid. When the entity rows in the pending list are all valid, the Transaction
issues a database SAVEPOINT
and coordinates saving the entity rows to the database. If all goes successfully, it issues the final database COMMIT
statement. If anything fails, the Transaction
performs a ROLLBACK TO SAVEPOINT
to allow the user to fix the error and try again.
The Transaction
object used by an application module represents the working set of entity rows for a single end-user transaction. By design, it is not a shared, global cache. The database engine itself is an extremely efficient shared, global cache for multiple, simultaneous users. Rather than attempting to duplicate the 30+ years of fine-tuning that has gone into the database's shared, global cache functionality, ADF Business Components consciously embraces it. To refresh a single entity object's data from the database at any time, you can call its refresh()
method. You can setClearCacheOnCommit()
or setClearCacheOnRollback()
on the Transaction
object to control whether entity caches are cleared at commit or rollback. The defaults are false
and true
, respectively. The Transaction
object also provides a clearEntityCache()
method you can use to programmatically clear entity rows of a given entity type (or all types). By clearing an entity cache, entity rows of that type will be retrieved from the database fresh the next time they are found by primary key, or retrieved by an entity-based view object, as you'll see in the following sections.
When you want to venture beyond the world of finding an entity row by primary key and navigating related entities via association accessors, you turn to the entity-based view object to get the job done. In an entity-based view object, the view object and entity object play cleanly separated roles:
The view object is the data source: it retrieves the data using SQL.
The entity object is the data sink: it handles validating and saving data changes.
Because view objects and entity objects have cleanly separated roles, you can build a hundred different view objects — projecting, filtering, joining, sorting the data in whatever way your user interfaces require application after application — without any changes to the reusable entity object. In fact, in some larger development organizations, the teams responsible for the core business domain layer of entity objects might be completely separate from the ones who build specific application modules and view objects to tackle an end-user requirement. This extremely flexible, symbiotic relationship is enabled by metadata an entity-based view object encapsulates about how the SELECT
list columns related to the attributes of one or more underlying entity objects.
Imagine a new requirement arises where your end users are demanding a page to quickly see open and pending service requests. They want to see only the service request ID, status, and problem description; the technician assigned to resolve the request; and the number of days the request has been open. It should be possible to update the status and the assigned technician. Figure 7-23 shows a new entity-based view object named OpenProblemsAndAssignees
that can support this new requirement.
The dotted lines in the figure represent the metadata captured in the view object's XML component definition that maps SELECT
list columns in the query to attributes of the entity objects used in the view object.
A few things to notice about the view object and its query are:
It joins data from a primary entity usage (ServiceRequest
) with that from a secondary reference entity usage (User
), based on the association related to the assigned technician you've seen in examples above
It's using an outer join of ServiceRequest.ASSIGNED_TO = Technician.USER_ID (+)
It includes a SQL-calculated attribute DaysOpen
based on the SQL expression CEIL(SYSDATE - TRUNC(REQUEST_DATE))
After adding an instance of OpenProblemsAndAssignees
with the same name to the SRService
's data model, you can see what happens at runtime when you execute the query. Like a read-only view object, an entity-based view object sends its SQL query straight to the database using the standard Java Database Connectivity (JDBC) API, and the database produces a result set. In contrast to its read-only counterpart, however, as the entity-based view object retrieves each row of the database result set, it partitions the row attributes based on which entity usage they relate to. This partitioning occurs by creating an entity object row of the appropriate type for each of the view object's entity usages, populating them with the relevant attributes retrieved by the query, and storing each of these entity rows in its respective entity cache. Then, rather than storing duplicate copies of the data, the view row simply points at the entity row parts that comprise it. As shown in Figure 7-24, the highlighted row in the result set is partitioned into a User
entity row with primary key 306
and a ServiceRequest
entity row with primary key 112
. Since the SQL-calculated DaysOpen
attribute is not related to any entity object, its value is stored directly in the view row.
The ServiceRequest
entity row that was brought into the cache above using findByPrimaryKey()
contained all attributes of the ServiceRequest
entity object. In contrast, a ServiceRequest
entity row created by partitioning rows from the OpenProblemsAndAssignees
query result contains values only for attributes that appear in the query. It does not include the complete set of attributes. This partially populated entity row represents an important runtime performance optimization.
Since the ratio of rows retrieved to rows modified in a typical enterprise application is very high, bringing only the attributes into memory that you need to display can represent a big memory savings over bringing all attributes into memory all the time.
Finally, notice that in the queried row for service request 114
there is no assigned technician, so in the view row it has a null
entity row part for its User
entity object.
By partitioning queried data this way into its underlying entity row constituent parts, the first benefit you gain is that all of the rows that include some data queried about the user with UserId
= 306
will display a consistent result when changes are made in the current transaction. In other words, if one view object allows the Email
attribute of user 306
to be modified, then all rows in any entity-based view object showing the Email
attribute for user 306
will update instantly to reflect the change. Since the data related to user 306
is stored exactly once in the User
entity cache in the entity row with primary key 306
, any view row that has queried the user's Email
attribute is just pointing at this single entity row.
Luckily, these implementation details are completely hidden from a client working with the rows in a view object's row set. Just as you did in the Business Components Browser, the client works with a view row, getting and setting the attributes, and is unaware of how those attributes might be related to entity rows behind the scenes.
You see above that among other rows, the OpenProblemsAndAssignees
result set includes a row related to service request 112
. When a client attempts to update the status of service request 112
to the value Closed
, ultimately a setStatus("Closed")
method gets called on the view row. Figure 7-25 illustrates the steps that will occur to automatically coordinate this view row attribute modification with the underlying entity row:
The client attempts to set the Status
attribute to the value Closed
Since Status
is an entity-mapped attribute from the ServiceRequest
entity usage, the view row delegates the attribute set to the appropriate underlying entity row in the ServiceRequest
entity cache having primary key 112
.
Any attribute-level validation rules on the Status
attribute at the ServiceRequest
entity object get evaluated and will fail the operation if they don't succeed.
Assume that some validation rule for the Status
attribute programmatically references the RequestDate
attribute (for example, to enforce a business rule that a ServiceRequest
cannot be closed the same day it is opened). The RequestDate
was not one of the ServiceRequest
attributes retrieved by the query, so it is not present in the partially populated entity row in the ServiceRequest
entity cache.
To ensure that business rules can always reference all attributes of the entity object, the entity object detects this situation and "faults-in" the entire set of ServiceRequest
entity object attributes for the entity row being modified using the primary key (which must be present for each entity usage that participates in the view object.
After the attribute-level validations all succeed, the entity object attempts to acquire a lock on the row in the SERVICE_REQUESTS
table before allowing the first attribute to be modified.
If the row can be locked, the attempt to set the Status
attribute in the row succeeds and the value is changed in the entity row.
If the user also updates the technician assigned to service request 112
, then something else interesting occurs. The request is currently assigned to vpatabal
, who has user ID 306
. Assume that the end user sets the AssignedTo
attribute to 300
to reassign the request to sking
. As shown in Figure 7-26, behind the scenes, the following occurs:
The client attempts to set the AssignedTo
attribute to the value 300
.
Since AssignedTo
is an entity-mapped attribute from the ServiceRequest
entity usage, the view row delegates the attribute set to the appropriate underlying entity row in the ServiceRequest
entity cache having primary key 112
.
Any attribute-level validation rules on the AssignedTo
attribute at the ServiceRequest
entity object get evaluated and will fail the operation if they don't succeed.
The row is already locked, so the attempt to set the AssignedTo
attribute in the row succeeds and the value is changed in the entity row.
Since the AssignedTo
attribute on the ServiceRequest
entity usage is associated to the reference entity usage named Technician
to the User
entity object, this change of foreign key value causes the view row to replace its current entity row part for user 306 with the entity row corresponding to the new UserId = 300
. This effectively makes the view row for service request 112
point to the entity row for sking
, so the value of the Email
in the view row updates to reflect the correct reference information for this newly assigned technician.
When you reexecute a view object's query, by default the view rows in its current row set are "forgotten" in preparation for reading in a fresh result set. This view object operation does not directly affect the entity cache, however. The view object then sends the SQL to the database and the process begins again to retrieve the database result set rows and partition them into entity row parts.
Note: Typically when you re-query, you are doing it in order to see the latest database information. If instead you want to avoid a database roundtrip by restricting your view object to querying only over existing entity rows in the cache, or over existing rows already in the view object's row set, Section 27.5, "Performing In-Memory Sorting and Filtering of Row Sets" explains how to do this. |
As part of this entity row partitioning process during a re-query, if an attribute on the entity row is unmodified, then its value in the entity cache is updated to reflect the newly queried value.
If the value of an entity row attribute has been modified in the current transaction, then during a re-query the entity row partitioning process does not refresh its value. Uncommitted changes in the current transaction are left intact so the end-user's logical unit of work is preserved. As with any entity attribute value, these pending modifications continue to be consistently displayed in any entity-based view object rows that reference the modified entity rows.
Figure 7-27 illustrates this scenario. Imagine that in the context of the current transaction's pending changes, a user "drills down" to a different page that uses the ServiceRequests
view object instance to retrieve all details about service request 112
. That view object has four entity usages: a primary ServiceRequest
usage, and three reference usages for Product
, User
(Technician), and User
(Customer). When its query result is partitioned into entity rows, it ends up pointing at the same ServiceRequest
entity row that the previous OpenProblemsAndAssignees
view row had modified. This means the end user will correctly see the pending change, that the service request is assigned to Steven King
in this transaction.
Figure 7-27 also illustrates the situation that the ServiceRequests
view object's query retrieves a different subset of reference information about users than the OpenProblemsAndAssignees
did. The ServiceRequests
queries up FirstName
and LastName
for a user, while the OpenProblemsAndAssignees
view object queried the user's Email
. The figure shows what happens at runtime in this scenario. If while partitioning the retrieved row, the entity row part contains a different set of attributes than the partially populated entity row that is already in the cache, the attributes get "merged". The result is a partially populated entity row in the cache with the union of the overlapping subsets of user attributes. In contrast, for John Chen (user 308) who wasn't in the cache already, the resulting new entity row contains only the FirstName
and LastName
attributes, but not the Email
.
Suppose the user is happy with her changes, and commits the transaction. As shown in Figure 7-28, there are two basic steps:
The Transaction
object validates any invalid entity rows in its pending changes list.
The entity rows in the pending changes list are saved to the database.
The figure depicts a loop in step 1 before the act of validating one modified entity object might programmatically affect changes to other entity objects. Once the transaction has processed its list of invalid entities on the pending changes list, if the list is still nonempty, it will complete another pass through the list of invalid ones. It will attempt up to ten passes through the list. If by that point there are still invalid entity rows, it will throw an exception since this typically means you have an error in your business logic that needs to be investigated.
The last aspect to understand about how view objects and entity objects cooperate involves two exceptions that can occur when working in a multiuser environment. Luckily, these are easy to simulate for testing purposes by simply starting up the Business Components Browser two times on the SRService
application module (without exiting from the first instance of course). Try the following two tests to see how these multiuser exceptions can arises:
In one Business Components Browser tester modify the status of an existing service request and tab out of the field. Then, in the other Business Components Browser window, try to modify the same service request in some way. You'll see that the second user gets the oracle.jbo.AlreadyLockedException
Try repeating the test, but after overriding the value of jbo.locking.mode
to be optimistic
on the Properties page of the Business Components Browser Connect dialog. You'll see the error occurs at commit time for the second user instead of immediately.
In one Business Components Browser tester modify the status of an existing service request and tab out of the field. Then, in the other Business Components Browser window, retrieve (but don't modify) the same status request. Back in the first window, commit the change. If the second user then tries to modify that same service request, you'll see that the second user gets the oracle.jbo.RowInconsistentException
. The row has been modified and committed by another user since the second user retrieved the row into the entity cache