How Does the Business Logic Tier Cache Data? | Home |
Solution Area |
Contents |
The business logic tier stores data it receives from a database and clients in two types of caches: an entity cache and a view cache. This sophisticated, patented caching system handles many complex design patterns and programmatic tasks for you, so you can concentrate on implementing the business logic and client interfaces specific to your application.
You could implement an entire business components application without ever learning how the caching system works. However, it's useful to understand its major features so you can make informed implementation and optimization decisions. In addition, experienced Java programmers can customize or bypass the caching system for application-specific reasons (although it's rarely necessary). After reading this topic, you should have an understanding of what the caching system does for you and what aspects of the caching system you can control.
The business logic tier caches business component instances in memory for five main reasons:
To improve application performance. When an application needs to work with data, it retrieves the data from the database, accesses the data, and possibly updates the data. When finished, the application adds changes to persistent data to the database. If the data was not cached, the application might have to retrieve and post data many times, resulting in performance degradation.
To manage business component states. As the program performs operations on business component instances, the cache manages their state. The state can indicate that the instance is newly created, unmodified, deleted, and so on. When the program is ready to commit to the database, the business logic tier uses the business component states to determine what database operations to perform. You don't have to write complex code to manage these states or to save changes to the database, because the business components framework does it for you.
To implement a well-defined business component lifecycle. The cache enables the framework to implement a well-defined lifecycle model, including how business component instances are created in memory, accessed, modified, validated, and written back to the database. You can create your applications knowing what the lifecycle behavior is and, if needed, customize certain points in the lifecycle to better suit your programs.
To coordinate the use of data and business logic by multiple views. All clients of a business logic tier share the caches and the business logic. Changes made in one view of data are visible to other views looking at the same data.
Several framework classes in the oracle.jbo.server package affect the caches. It's useful to have a general idea of this relationship to understand how your code uses the caches.
For each EntityDefImpl instance used in a session, the top-level application module manages an entity cache that contains EntityImpl instances.
A view object manages a view cache. The view cache contains row sets. The row sets contain ViewRowImpl instances.
To better understand these relationships, consider a simple example where an application begins running, retrieves data from the database, and later updates the data in the database.
Typically, when an application first starts running, a view object query populates
the caches. When the view object query executes, each row of the query's result
set is cached in a view row:
View attributes that are based on (map to) entity attributes are placed in one or more entity caches. For these attributes, the view cache points to the entity cache.
The process of populating the caches follows these general steps:
first
method will bring in
the first row and leave the rest out of the cache. The next
method
brings in the next row, and so on.EntityImpl
instance for each
entity object related to this view object.For example, in the following code snippet, the client creates an application module instance, finds a view object, adjusts the view object query, retrieves the first row, and gets the salary of the first row. All view attributes based on an entity attribute are added to the entity cache. All view attributes that are not based on an entity attribute are added to the view cache. For the Managers view object, all attributes are persistent, so the view object places all attribute values in the entity cache.
An application does not have to use a view cache or entity cache.
If there are no view attributes that are based on entity attributes, the entity cache is not used.
To prevent data from entering the view cache, you can use the setForwardOnly method in a view object. This is called forward only mode. Using forward only mode can save memory resources and time, because only one view row is in memory at a time. Forward only mode is useful when you are fetching data and reading it once, such as formatting data for a web page, or for batch operations that proceed linearly. Note that if the view object is based on one or more entity objects, the data does pass to the entity cache in the normal manner, but no rows are added to the view cache.
In addition, an entity object can populate the entity cache directly, bypassing the view cache. If you look up an entire entity object with the findByPrimaryKey method, all of its attribute values are added to the entity cache. (It gives priority to data in the cache, and then brings in data from the database if it's not already there, as described later.) Using a view object to look up data is more flexible, in that you can bring in any attributes that are relevant to the task and leave out those that are not. The view object executes a query, and if the attribute doesn't already contain data, it adds the data for that attribute.
You can auto-create a primary key (as a sequence, GUID, and so on) whose value is generated by the database when a new row is posted to it. If you want to use to use an entity attribute as a database sequence, for example, you could add code to the EntityImpl create method, select the Refresh After Insert attribute setting, or use the DBSequence domain. The code you in the create method would need to return the value immediately when a new row is inserted. For the Refresh After Insert setting, the value is returned after the row is saved to the database; you need a database trigger that inserts the value into the column of a new row. You may want to make these types of attributes read-only so that an end user cannot change it.
If a query returns multiple row sets because of a bind variable, multiple row sets can be created. For example, if the query is SELECT DEPTNO, DNAME, LOC FROM DEPT WHERE LOC=:1, and the bind values are SAN JOSE, CHICAGO, and NEW YORK, there can be three row sets, one for each location, in the view cache.
Each view object instance has its own view cache. After a query, each returned database row is represented by a row in the view cache. Each column is represented by a corresponding attribute in the row. Attributes that did not receive a value remain empty in the row.
Each entity object instance has its own entity cache. After a view object query, in each returned row, the values of view attributes that map to entity attributes are placed in the appropriate entity cache. Remember that view objects can map to more than one entity object. So different attributes in a row can end up in more than one entity cache. Each row in an entity cache is uniquely identified by its primary key. (So if you specify more than one entity object for a view object, you must always include the primary key of each entity object.) Attributes whose values were not populated by the query remain empty in the row. Whenever a view attribute that is mapped to an entity object is retrieved or set, it delegates to the getter or setter in the entity object.
Each view object instance contained by a top-level application module shares entity caches with all other view object instances in the top-level application module. (This includes any view object instances in nested application modules.) If you have several view objects that refer to the same entity object, you know that they also refer to the same entity cache. This means that when shared data is modified through one view object instance, all other view object instances are notified of this change and can show the modification in a client form, for example.
When a view object executes a query, the data needs to be merged into the entity cache. The business logic tier uses modified data in the entity cache, if it exists. If an entity object instance has been modified, the query returns the entity cache's data instead of new data from the database. If the entity object instance does not exist or is unmodified, the query returns data from the database. This ensures that unposted changes are not overwritten when the same row is retrieved from database through other view object instances. The entity object checks its cached data against database data at locking time.
Here's an example:
The following code snippet shows how different view objects can view the same cached data.
In the entity cache, attributes may be missing, for example, if a view object does not have a view attribute that maps to a particular entity attribute so that attribute is never populated. If business logic tries to access an attribute value stored in a database row, and the value hasn't been brought into the entity cache, fault-in occurs. All empty attributes in the row are populated from the database so that business logic can complete. Here's an example:
Fault-in ensures that business logic has values it needs. As an optimization technique, you can prevent fault-in by making sure that all attributes needed by business logic are included in view object queries. This prevents extra SQL statements from being executed and creates a clean separation between entity objects and view objects. However, you may want to keep rarely accessed attributes out of the view object to conserve memory.
The following entity object code snippet, from EmployeeImpl.java, shows why fault-in will occur. The CommissionPct attribute is accessed, but not in the cache, so it's faulted-in.
The following code snippet, in TestClient.java, shows where fault-in occurs.
There are four conditions under which all attributes of a row in the entity cache are brought in:
The view object query includes all attributes.
Fault-in occured.
The findByPrimaryKey method was used.
The business logic tier has obtained a lock for the corresponding row in the database (through pessimistic locking or the Row.lock method).
There are two types of locking: pessimistic and optimistic. Or you can choose not to use locking at all, which is typically not the best choice. You usually use optimistic locking when you are optimistic that two users will never modify the same row at the same time, due to the nature of your application. Pessimistic locking is the most conservative form of locking.
When an application uses locking, the business logic tier attempts to lock a row in these circumstances:
For pessimistic locking, there is an attempt to set data in an existing
row. The client or business logic tier successfully calls setAttribute on
a mapped attribute for the first time on the row. First, business logic
validation is triggered. Then the entity object attempts to acquire a lock
on the database row.
When an application doesn't use locking, if the row is locked by another user,
the entity object will wait until the row is released by the other user who
locked it. Then it will write the changes, which could overwrite the previous
user's changes.
Here is how the business logic tier acquires a lock:
When the transaction associated with a top-level application module uses pessimistic locking (the default), and the row is an existing row in the database, the business logic tier attempts to lock a database row the first time one of its attributes is successfully modified, including passing any validation. (If the entity object is part of a composition, the framework first tries to lock the row for the top-level entity object in the composition.) If it can lock the row, the business logic tier can modify the row. If the business logic tier cannot lock the row, it throws an exception and the value returns to what it was before the modification.
When the transaction of a top-level application module uses optimistic locking, at post time, the business logic tier attempts to lock each row corresponding to any deleted or modified entity object instances. (If the entity object is part of a composition, the framework first tries to lock the row associated with the top-level entity object.) If any of the attempts to lock are unsuccessful, an exception is thrown.
For session-oriented programs, pessimistic locking is best, because the application discovers errors immediately and there is less chance of the transaction aborting when it tries to commit. For nonsession-oriented programs, optimistic locking can be best, for example, you don't want to lock a record if a user leaves a web page open on their screen for a few days.
Locking is defined in the oracle.jbo.Transaction interface. You set the locking
mode with the setLockingMode method, and
get the current locking mode with the getLockingMode method. After an entity
object instance is successfully modified in the entity cache (after it acquires
a lock), all view object instances who have queried data for the row are notified
so clients can refresh their displayed data.
After locking a row, the entity object verifies that the data retrieved from the database during the lock acquisition matches the unmodified data in the entity cache. If not, an oracle.jbo.RowInconsistException is thrown. This ensures that if another program in another database session has modified that row, then the application does not unknowingly overwrite that modification.
There are different data resolution processes for locking, fault-in, and updates.
For locking, here is how the business logic tier resolves the data:
If there are no differences, the data from the database is placed in the cache. If there are differences, a RowInconsistException is thrown. You can catch the exception and resolve the differences in your code.
For example, let's say there is $100 in a joint checking account and the two owners go to an ATM:
For fault-in, if the business logic tier has modified an attribute in the cache, the modified value is kept. If the business logic tier has not modified the value, the value retrieved from the database is used.
For updates, the business logic tier locks before the update and compares values, so it knows immediately if data has changed.
Associated with the top-level application module is a transaction object. This object manages the interaction between the entity cache and database. To do this, the transaction object keeps a transaction list, which is a list of entity object instances that have been changed during this transaction.
In general, the commit and rollback cycle follows these steps:
When the client calls Transaction postChanges or commit, the transaction
object iterates through the transaction list and calls the doDML method
on each entity object, to perform delete, data update, and insert operations
in the database.
Note that commit calls postChanges.
In general, the transaction object processes entity objects in the transaction
list in the chronological order of when the entity object was first modified
in this transaction. However, there is no guaranteed order that changes
are processed. For a composition, however, the transaction object processes
the parent entity object first, followed by children entity objects.
After the posts are complete, the transaction list cleared.
The data is committed or rolled back in the database.
The locks are released.
The order that you insert rows in a database matters for associations. For example, suppose you have an association between Employees and Departments, with a key of Department_Id that associates the two entity objects. First, you create an employee with a Department_Id of a department not yet created. Next you create a department for that new Department_Id. If you post these changes, the transaction object will create the new employee first (with the new Department_Id), followed by the department. If the database had a foreign key referential integrity constraint on the Employees table, you will get a database error. This is because the transaction object tried to create a row in the Employees table where the associated department had not yet been created. This situation is a post order problem: associated rows were not created in the proper order.
One way to avoid post order problems is to use a composition. In a composition, the transaction object ensures that the parent entity object is written to database first, followed by children. In the Employees/Departments example, if you marked the association as a composition, Departments being the parent and Employees the child, then the new department is created first, so you would not get a referential integrity error.
Only valid rows are posted. The entity object makes sure each row is valid before posting.
When a view object or application module is destroyed, the associated view caches are also destroyed. You can also explicitly clear view caches by calling certain methods, including clearVOCaches on the application module, clearEntityCache on the transaction, and clearCache on the view object.
When no view caches point to an instance in the entity cache, and the entity instance hasn't been modified, the instance becomes a candidate for clearing. When the rows are cleared depends on the Java VM you are using. The Java VM usually removes them when it needs more memory.
Business Components for Java provides methods that can clear the cache. These methods are mainly used for these two purposes:
You can use the following methods to clear the caches:
These last two groups of methods set (or inquire about) the flag that controls whether the entity cache is automatically cleared after commit or rollback. For example, if setClearCacheOnCommit(true) is called on a transaction, whenever the transaction is committed, all entity caches are cleared. Subsequent access to entity objects will refresh data from the database.
Note that by default, isClearCacheOnCommit is false and isClearCacheOnRollback
is true. So, by default, the entity caches are cleared after rollback. But after
commit, the entity caches are retained (not cleared).