Oracle® TopLink Developer's Guide 10g Release 3 (10.1.3.1.0) Part Number B28218-01 |
|
|
View PDF |
This chapter explains the advanced unit of work API calls and techniques commonly used later in the development cycle, including the following:
Using Optimistic Read Locking With forceUpdateToVersionField
Integrating the Unit of Work With an External Transaction Service
For more information about the available methods for the UnitOfWork
, see Oracle TopLink API Reference.
The unit of work provides the following object registration options:
Example 99-1 shows how to use the unit of work newInstance
method to create a new Pet
object, register it with the unit of work, and return a clone, all in one step. If you are using a factory design pattern to create your objects (and have specified this in the query builder), the newInstance
method will use the appropriate factory.
This section examines how to use the registerNewObject
method, including the following the following:
Note: You cannot useUnitOfWork methods registerObject , registerNewObject , or registerExistingObject with an aggregate object (see "Relational Aggregate Descriptors"). Doing so will raise a ValidationException or other errors at commit time. For more information, see "Working With Aggregates". |
The registerNewObject
method registers a new object as if it was a clone. At commit time, the unit of work creates another instance of the object to be the cache version of that object.
Use registerNewObject
in situations where:
You do not need a handle to the cache version of the object after the commit transaction and you do not want to work with clones of new objects.
You must pass a clone into the constructor of a new object, then register the new object.
Example 99-2 shows how to register a new object with the registerNewObject
method.
Example 99-2 Registering a New Object with the registerNewObject Method
UnitOfWork uow = session.acquireUnitOfWork(); PetOwner existingPetOwnerClone = PetOwner)uow.readObject(PetOwner.class); Pet newPet = new Pet(); newPet.setId(900); newPet.setType("Lizzard"); newPet.setName("Larry"); newPet.setPetOwner(existingPetOwnerClone); uow.registerNewObject(newPet); uow.commit();
By using registerNewObject
, the variable newPet
should not be used after the unit of work is committed. The new object is the clone and if you need the cache version of the object, you need to query for it. If you needed a handle to the cache version of the Pet
after the unit of work has been committed, then you should use the first approach described in "Associating a New Source to an Existing Target Object". In that example, the variable newPet
is the cache version after the unit of work is committed.
At commit time, TopLink can determine if an object is new or not. As described in "Associating a New Target to an Existing Source Object", if a new object is reachable from a clone, you do not need to register it. TopLink effectively uses the registerNewObject
method on all new objects it can reach from registered objects.
When working with new objects, remember the following rules:
Only reachable or registered objects will be persisted.
New objects or objects that have been registered with registerNewObject
are considered to be working copies in the unit of work.
If you call registerObject
with a new object, the clone, and the argument, is considered the cache version.
Example 99-3 shows how to associate new objects with the registerNewObject
method.
Example 99-3 Associating New Objects with the registerNewObject Method
UnitOfWork uow = session.acquireUnitOfWork(); Pet newPet = new Pet(); newPet.setId(150); newPet.setType("Horse"); newPet.setName("Ed"); PetOwner newPetOwner = new PetOwner(); newPetOwner.setId(250); newPetOwner.setName("George"); newPetOwner.setPhoneNumber("555-9999"); VetVisit newVetVisit = new VetVisit(); newVetVisit.setId(350); newVetVisit.setNotes("Talks a lot"); newVetVisit.setSymptoms("Sore throat"); newPet.getVetVisits().addElement(newVetVisit); newVetVisit.setPet(newPet); newPet.setPetOwner(newPetOwner); uow.registerNewObject(newPet); uow.commit();
However, after the unit of work commit, do not use the objects in the variables newPet
, newPetOwner
, and newVetVisit
because they are technically copies from the unit of work.
If you need a handle to the cache version of these objects, query for them or use the unit of work as shown in Example 99-4.
Example 99-4 Associating New Objects with the newObjectMethod and Retaining a Handle to the Cache Objects
UnitOfWork uow = session.acquireUnitOfWork(); Pet newPet = new Pet(); Pet newPetClone = (Pet)uow.registerObject(newPet); newPetClone.setId(150); newPetClone.setType("Horse"); newPetClone.setName("Ed"); PetOwner newPetOwner = new PetOwner(); PetOwner newPetOwnerClone = (PetOwner)uow.registerObject(newPetOwner); newPetOwnerClone.setId(250); newPetOwnerClone.setName("George"); newPetOwnerClone.setPhoneNumber("555-9999"); VetVisit newVetVisit = new VetVisit(); VetVisit newVetVisitClone = (VetVisit)uow.registerObject(newVetVisit); newVetVisitClone.setId(350); newVetVisitClone.setNotes("Talks a lot"); newVetVisitClone.setSymptoms("Sore throat"); newPetClone.getVetVisits().addElement(newVetVisitClone); newVetVisitClone.setPet(newPetClone); newPetClone.setPetOwner(newPetOwnerClone); uow.commit();
The registerAllObjects
method takes a Collection
of objects as an argument and returns a Collection
of clones. This lets you register many objects at once as shown in Example 99-5.
Note: You cannot useUnitOfWork methods registerObject , registerNewObject , or registerExistingObject with an aggregate object (see "Relational Aggregate Descriptors"). Doing so will raise a ValidationException or other errors at commit time. For more information, see "Working With Aggregates". |
Example 99-5 Using registerAllObjects
UnitOfWork uow = session.acquireUnitOfWork(); Collection toRegister = new Vector(2); VetVisit vv1 = new VetVisit(); vv1.setId(70); vv1.setNotes("May have flu"); vv1.setSymptoms("High temperature"); toRegister.add(vv1); VetVisit vv2 = new VetVisit(); vv2.setId(71); vv2.setNotes("May have flu"); vv2.setSymptoms("Sick to stomach"); toRegister.add(vv2); uow.registerAllObjects(toRegister); uow.commit();
When you register an object with the unit of work, TopLink runs an existence check to determine whether or not the object exists. TopLink uses this information at commit time to determine whether to perform an insert or an update operation. You can specify the default existence checking policy for a project as a whole (see "Configuring Existence Checking at the Project Level") or on a per-descriptor basis ("Configuring Cache Existence Checking at the Descriptor Level"). By default, TopLink uses the check cache existence checking policy. If you use any existence checking policy other than check cache, then you can use the way you register your objects to your advantage to reduce the time it takes TopLink to register an object.
This section explains how to use one of the following existence checking policies to accelerate object registration:
If you configure a class's descriptor with an existence checking policy of check database, TopLink will check the database for existence for all instances of that class registered in a unit of work. However, if you know that an object is new or existing, rather than use the basic registerObject
method, you can use registerNewObject
or registerExistingObject
to bypass the existence check. TopLink will not check the database for existence on objects that you have registered with these methods. It will automatically perform an insert operation if registerNewObject
is called, or an update operation if registerExistingObject
is called.
If you configure a class's descriptor with an existence checking policy of assume existence, TopLink will assume that all instances of that class registered with a unit of work exist and TopLink will always perform an update operation to the database on all such registered objects; even new objects that you registered with registerObject
method. However, if you use the registerNewObject
method on the new object, TopLink knows to perform an insert operation in the database even though the existence checking policy says assume existence.
If you configure a class's descriptor with an existence checking policy of assume nonexistence then TopLink assumes that all instances of that class registered with a unit of work do not exist and will always perform an insert operation on the database, even on objects read from the database. However, if you use the registerExistingObject
method on existing objects, TopLink knows to perform an update operation on the database.
Aggregate mapped objects should never be registered in a TopLink unit of work–doing so will generate an exception. Aggregate cloning and registration is automatic based on the owner of the aggregate object. In other words, if you register the owner of an aggregate, the aggregate is automatically cloned. When you get a working copy of an aggregate owner, its aggregate is also a working copy.
When working with aggregates, you should always use an aggregate within the context of its owner:
If you get an aggregate from a working clone owner, then the aggregate is a working clone.
If you get an aggregate from a cache version owner, then the aggregate is the cache version.
For more information about aggregate objects, see "Relational Aggregate Descriptors".
The unit of work unregisterObject
method lets you unregister a previously registered object from a unit of work. An unregistered object will be ignored in the unit of work, and any uncommitted changes made to the object up to that point will be discarded.
In general, this method is rarely used. It can be useful if you create a new object, but then decide to delete it in the same unit of work (which is not recommended).
You can declare a class as read-only within the context of a unit of work. Clones are neither created nor merged for such classes, thus improving performance. Such classes are ineligible for changes in the unit of work.
When a unit of work registers an object, it traverses and registers the entire object tree. If the unit of work encounters a read-only class, it does not traverse that branch of the tree, and does not register objects referenced by the read-only class, so those classes are ineligible for changes in the unit of work. The read-only classes are cached and must not be changed by the user.
For example, suppose class A owns a class B, and class C extends class B. You acquire a unit of work in which you know only instances of class A will change: you know that no class Bs will change. Before registering an instance of class B, use the following:
myUnitofWork.addReadOnlyClass(B.class);
You can then proceed with your transaction: registering class A objects, modifying their working copies, and committing the unit of work.
At commit time, the unit of work will not have to compare backup clones with the working clones for instances of class B (even if instances were registered explicitly or implicitly). This can improve unit of work performance if the object tree is very large.
Note that if you register an instance of class C, the unit of work does not create or merge clones for this object; any changes made to class C are not be persisted because class C extends class B and class B was identified as read-only.
To identify multiple classes as read-only, add them to a Vector
and use the following code:
myUnitOfWork.addReadOnlyClasses(myVectorOfClasses);
Note that a nested unit of work inherits the set of read-only classes from the parent unit of work. For more information on using a nested unit of work, see "Using a Nested or Parallel Unit of Work".
To establish a default set of read-only classes for all units of work, use the project method setDefaultReadOnlyClasses(Vector)
. After you call this method, all new units of work include the Vector
of read-only classes.
When you declare a class as read-only, the read-only declaration extends to its descriptors. You can declare a descriptor as read-only at development time, using either Java code or TopLink Workbench. This option improves performance by excluding the read-only descriptors from unit of work registration and editing.
To flag descriptors as read-only in Java code, call the setReadOnly
method on the descriptor as follows:
descriptor.setReadOnly();
To declare a descriptor as read-only in TopLink Workbench, select the Read Only check box for the specific descriptor.
For more information, see "Configuring Read-Only Descriptors".
By default, when you call the unit of work commit
method, TopLink writes your changes to the data source and commits your changes.
Alternatively, you can perform a two-stage or partial commit transaction by using the unit of work writeChanges
method prior to calling commit
(either directly or by way of an external transaction service).
When you call the unit of work writeChanges
method, the unit of work commit process begins, and all changes are written out to the data source. However, the data source transaction is not committed, nor will changes be merged into the shared session cache. To finalize your changes, the unit of work commit
method must still be called (either directly or by way of an external transaction service).
After you call the unit of work writeChanges
method, any attempt to register objects or execute object-level queries will throw an exception. You may execute report queries, noncaching queries, and data read and modify queries.
If any exception is thrown, the transaction will be rolled back (or marked rollback only) and you cannot recover the unit of work.
You can call this method only once. You cannot use this method to write out changes in an incremental fashion.
You can use the unit of work writeChanges
method to address a variety of transaction issues, including the following:
As an alternative to conforming (see "Using Unit of Work Method writeChanges Instead of Conforming")
To handle external transaction issues (see "Using the Unit of Work to Handle External Transaction Timeouts and Exceptions")
Because queries are executed on the database, querying though a unit of work will not, by default, include new, uncommitted objects in a unit of work. The unit of work will not spend time executing your query against new, uncommitted objects in the unit of work unless you explicitly tell it to. If you have uncommitted changes, this can pose a problem in a unit of work. Uncommitted changes not yet written to the database cannot influence which result set gets returned.
Conforming is a query feature that lets you include new, changed, or deleted objects in queries within a unit of work prior to committing. This lets you to query against your relative logical or transaction view of the database.
Before you use conforming, be aware of its limitations (see "Guidelines for Using Conforming") and make sure that conforming is actually necessary. For example, consider the alternative described in "Conforming Query Alternatives".
This section explains the following:
Note: By default, TopLink suppresses exceptions thrown during the memory search stage of conforming. For more information on handling exceptions during conforming, see "Exceptions During Conforming". |
When using conforming, follow the guidelines that this section describes to ensure that conforming queries return the correct results:
Conforming is supported by the following queries:
ReadObjectQuery
ReadAllQuery
Expressions
EJB QL
Query by example
Query by selection object or primary key (only new or deleted objects apply)
Conforming is not supported by the following queries:
ReportQuery
DataReadQuery
DataReadQuery
(deleted objects can be conformed)
StoredProcedureCall
(deleted objects can be conformed)
EISCall
(deleted objects can be conformed)
Expression or EJB QL queries that use database-specific functions, or subselects.
When conforming is used on a ReadAllQuery
, the database result is first queried. If the unit of work has not yet committed any changes to the database, this result will not reflect the unit of work changes. The database results are then conformed in memory using the following criteria:
Registered new objects that conform to the query are added.
Modified existing objects that no longer conform are removed.
Modified existing objects that conform are added.
Deleted objects are removed.
Note: If new objects are not explicitly registered, they are not conformed. Also, if removed object are not explicitly deleted, they are not conformed. |
If the query uses ordering, ordering of conformed results is not maintained and conformed instances are added to the front of the result. To apply ordering, store the result in memory using Collections
method sort
, or a TreeSet
result collection class. When using conforming on a ReadObjectQuery
, first query the unit of work for a conforming object: if the conforming object is found, it is returned and the database access is avoided; if the conforming object is not found, the database is queried. If the unit of work has not yet committed any changes to the database, this result does not reflect the unit of work changes. The database results are then conformed in memory using the following criteria:
If the database result no longer conforms, null is returned.
If the database result has been deleted, null is returned.
Note: If the database result returns multiple results, only the first result is considered, because it is an instance of theReadObjectQuery and only a single result is expected. If the first result no longer conforms, null is returned, even if there were potential valid conforming results.
If you expect the query to return multiple results, use a |
If a new object is only related to an existing object, and not explicitly registered, queries for this object are not able to conform it. If you remove, but do not explicitly delete a privately owned object, this object cannot be conformed.
If a query traverses relationships (uses joins) and the related objects are changed, the query can only conform these objects if both of the following conditions are met:
The source objects have been registered in the unit of work.
The source objects' relationship has been instantiated.
TopLink provides a conforming option that forces an instantiation of indirection. However, you use this option with caution as it can cause an increased database access.
Consider the example shown in Example 99-5. In this example, you have Employee
objects with an address
attribute configured for indirection (see "Indirection") mapped by a one-to-one mapping to an Address
object.
You want to read all employees who live in Ottawa, but first, you need to modify some of the Address
objects to change city from Toronto to Ottawa. Jane Doe is one such employee.
First, using the UnitOfWork
, you read all Address
objects and change some city
attributes (including Jane's) from Toronto to Ottawa. Then you run a conforming query to get all employees who live in Ottawa. However, for the following reasons Jane is not included in the results, even though she now lives in Ottawa:
Jane is not returned from the database because the transaction has not yet been committed and in the database, her address still says Toronto.
Jane cannot be added to the conformed result in memory because she is not registered in the UnitOfWork
cache.
Conforming only recognizes explicit changes. In this example, Jane Doe's Employee
object was only implicitly changed. In order to be considered explicitly changed, an Employee
must meet the following criteria:
be registered in the UnitOfWork
have its address
attribute changed (in this example, indirection must be triggered for the address
attribute)
The correct way to handle this example would be as follows:
Using the UnitOfWork
, read in all employees.
All these Employee
objects are now registered with the UnitOfWork
Using the same UnitOfWork
, access the employees' addresses, instantiating the indirect relationships.
Modify the employees' addresses, changing some of the addresses to be in Ottawa.
Run the conforming query on employees with addresses inside Ottawa.
All employees with addresses in Ottawa are returned, including both employees that were in Ottawa originally and employees whose addresses were changed in this transaction.
Commit the transaction.
If you do not register all employees whose address may be changed, and instantiate their address relationship, the conforming query will not include Jane.
An alternate approach is to use short transactions: the safest conforming query is one made immediately after a commit. For example:
Using the UnitOfWork
, read in all addresses outside of Ottawa.
Modify the addresses, changing some of the addresses to be in Ottawa
Commit the transaction.
Using the UnitOfWork
, read in all employees inside Ottawa.
Assume that a single Pet
of type Cat
already exists on the database. Examine the code shown in Example 99-6.
Example 99-6 Using Conforming Queries
UnitOfWork uow = session.acquireUnitOfWork(); Pet pet2 = new Pet(); Pet petClone = (Pet)uow.registerObject(pet2); petClone.setId(200); petClone.setType("Cat"); petClone.setName("Mouser"); ReadAllQuery readAllCats = new ReadAllQuery(); readAllCats.setReferenceClass(Pet.class); ExpressionBuilder builder = new ExpressionBuilder(); Expression catExp = builder.get("type").equal("Cat"); readAllCats.setSelectionCriteria(catExp); Vector allCats = (Vector)uow.executeQuery(readAllCats); System.out.println("All 'Cats' read through UOW are: " + allCats); uow.commit();
This produces the following output:
All 'Cats' read through UOW are: [Pet type Cat named Fluffy id:100]
If you tell the query readAllCats
to include new objects:
readAllCats.conformResultsInUnitOfWork();
The output would be:
All 'Cats' read through UOW are: [Pet type Cat named Fluffy id:100, Pet type Cat named Mouser id:200]
TopLink's support for conforming queries in the unit of work can be specified at the descriptor level.
You can define a descriptor directly to always conform results in the unit of work so that all queries performed on this descriptor conform its results in the unit of work by default. You can specify this either within code or from TopLink Workbench.
You can configure a descriptor to always conform in the unit of work using TopLink Workbench or Java code.
To configure a descriptor to always conform in the unit of work in Java code, use Descriptor
method setShouldAlwaysConformResultsInUnitOfWork
, passing in an argument of true
.
To configure a descriptor to always conform in the unit of work using TopLink, see "Configuring Unit of Work Conforming at the Descriptor Level".
This section describes alternatives to conforming that may meet your needs without the performance penalty imposed by conforming. This section describes the following:
Using UnitOfWork
method writeChanges
, you can write uncommitted changes to the data source: you can execute report queries, noncaching queries, and data read and modify queries against these changes (see Example 99-7).
Example 99-7 Using Unit of Work Method writeChanges
UnitOfWork uow = session.acquireUnitOfWork(); Pet pet = new Pet(); Pet petClone = (Pet)uow.registerObject(pet); petClone.setId(100); petClone.setName("Fluffy"); petClone.setType("Cat"); uow.writeChanges(); // Use uow to perform report, noncaching, and data read and modify queries // against the changes made so far uow.commit();
However, you can call writeChanges
only once; any attempt to register objects or to execute object-level queries will throw an exception.
For more information, see "Writing Changes Before Commit Time"
Sometimes, you need to provide other code modules with access to new objects created in a unit of work. Conforming can be used to provide this access. However, the following alternative is significantly more efficient.
Somewhere a unit of work is acquired from a session and is passed to multiple modules for portions of the requisite processing:
UnitOfWork uow = session.acquireUnitOfWork();
In the module that creates the new employee, note the following:
Pet newPet = new Pet(); Pet newPetClone = (Pet)uow.registerObject(newPet); uow.setProperty("NEW PET", newPet);
In other modules where newPet
needs to be accessed for further modification, it can simply be extracted from the unit of work's properties:
Pet newPet = (Pet) uow.getProperty("NEW PET"); newPet.setType("Dog");
Conforming queries are ideal if you are not sure if an object has been created yet or the criteria is dynamic.
However, for situations where the quantity of objects is finite and well known, using unit of work properties is a simple and more efficient solution.
In a three-tier application, the client and server exchange objects using a serialization mechanism such as RMI or CORBA. When the client changes an object and returns it to the server, you cannot register this serialized object into a unit of work directly. On the server, you must merge the serialized object with the original object in the session cache.
Using the unit of work methods listed in Table 99-1, you can merge a deserialized object into your session cache. Each method takes the serialized object as an argument and returns the original object.
Before doing so, you must ensure that the source object is in your session cache. Attempting to merge a deserialized object into a session cache that does not yet contain the object will result in a descriptor exception (see "200: ATTEMPT_TO_REGISTER_DEAD_INDIRECTION"). To avoid this, Oracle recommends that you first read the object instance that the deserialized object represents. If you are using a coordinated cache or your application is running in a cluster, the session you merge into may not yet contain your original object. By performing a read operation first, you guarantee that the object will be in the cache before you merge.
Table 99-1 Unit of Work Merge Methods
Method | Purpose | Used When |
---|---|---|
|
Merges the serialized object and all its privately owned parts (excluding non-private references from it to independent objects) into the working copy clone. |
The client edits the object but not its relationships, or marks its independent relationships as transient. |
|
Merges the serialized object and all references into the working copy clone. |
The client edits the object and the targets of its relationships and has not marked any attributes as transient. |
|
Merges only serialized object changes to attributes mapped with direct mappings into the working copy clone. |
The client edits only the object's direct attributes or has marked all of the object's relationships as transient. |
|
Merges the serialized object and everything connected to it (the entire object tree where the serialized object is the root) into the working copy clone. |
The client traverses all relationships of the objects and makes changes. Note: Use |
Note that if your three-tier client is sufficiently complex, consider using the TopLink remote session (see "Remote Sessions"). It automatically handles merging and lets you use a unit of work on the client.
You can merge clones with both existing and new objects. Because clones do not appear in the cache and may not have a primary key, you can merge new objects only once within a unit of work. If you need to merge a new object more than once, call the unit of work setShouldNewObjectsBeCached
method, and ensure that the object has a valid primary key; you can then register the object.
Example 99-8 shows one way to update the original object with the changes contained in the corresponding serialized object (rmiClone
) received from a client.
Example 99-8 Merging a Serialized Object
update(Object original, Object rmiClone) { original = uow.registerObject(original); uow.mergeCloneWithRefereneces(rmiClone); uow.commit(); }
For more information, see "Indirection, Serialization, and Detachment".
At commit time, a unit of work and its contents expire: you must not use the unit of work nor its clones even if the transaction failed and rolled back.
However, TopLink offers an API that lets you continue working with a unit of work and its clones:
commitAndResume
: Commits the unit of work, but does not invalidate it or its clones.
commitAndResumeOnFailure
: Commits the unit of work. If the commit transaction succeeds, the unit of work expires. However, if the commit transaction fails, this method does not invalidate the unit of work or its clones. This method lets you modify the registered objects in a failed unit of work and retry the commit transaction.
You should resume a unit of work only in an application that makes repeated changes to the same, small dataset. Reusing the same unit of work while accessing different datasets may result in poor performance.
Example 99-9 shows how to use the commitAndResume
method.
Example 99-9 Using the commitAndResume Method
UnitOfWork uow = session.acquireUnitOfWork(); PetOwner petOwnerClone = (PetOwner)uow.readObject(PetOwner.class); petOwnerClone.setName("Mrs. Newowner"); uow.commitAndResume(); petOwnerClone.setPhoneNumber("KL5-7721"); uow.commit();
The commitAndResume
call produces this SQL:
UPDATE PETOWNER SET NAME = 'Mrs. Newowner' WHERE (ID = 400)
Then, the commit
call produces this SQL:
UPDATE PETOWNER SET PHN_NBR = 'KL5-7721' WHERE (ID = 400)
Under certain circumstances, you may want to abandon some or all changes to clones in a unit of work, but not abandon the unit itself. The following options exist for reverting all or part of the unit of work:
revertObject
: Abandons changes to a specific working copy clone in the unit of work
revertAndResume
: Uses the backup copy clones to restore all clones to their original states, deregister any new objects, and reinstate any deleted objects.
You can use a unit of work within another unit of work (nesting), or you can use two or more units of work with the same objects in parallel.
To start multiple units of work that operate in parallel, call the acquireUnitOfWork
method multiple times on the session. The units of work operate independently of one another and maintain their own cache.
To nest units of work, call the acquireUnitOfWork
method on the parent unit of work. This creates a child unit of work with its own cache. If a child unit of work commits, it updates the parent unit of work rather than the database. If the parent does not commit, the changes made to the child are not written to the database.
TopLink does not update the database or the cache until the outermost unit of work is committed. You must commit or release the child unit of work before you can commit its parent.
Working copy clones from one unit of work are not valid in another units of work: not even between an inner and outer unit of work. You must register objects at all levels of a unit of work where they are used.
Example 99-10 shows how to use nested units of work.
Example 99-10 Using Nested Units of Work
UnitOfWork outerUOW = session.acquireUnitOfWork(); Pet outerPetClone = (Pet)outerUOW.readObject(Pet.class); UnitOfWork innerUOWa = outerUOW.acquireUnitOfWork(); Pet innerPetCloneA = (Pet)innerUOWa.registerObject(outerPetClone); innerPetCloneA.setName("Muffy"); innerUOWa.commit(); UnitOfWork innerUOWb = outerUOW.acquireUnitOfWork(); Pet innerPetCloneB = (Pet)innerUOWb.registerObject(outerPetClone); innerPetCloneB.setName("Duffy"); innerUOWb.commit(); outerUOW.commit();
You can execute native SQL or invoke a stored procedure within a unit of work by using unit of work method executeNonSelectingCall
or by executing a DataModifyQuery
. This makes the unit of work begin its database transaction early and execute the call to the data immediately.
If you release the unit of work, it will roll back the database changes. If you commit the unit of work and the commit succeeds, the unit of work will commit the changes to the database.
You can execute a DataModifyQuery
only in a unit of work or a database session. You cannot execute a DataModifyQuery
in a client or server session directly.
You can execute a DataReadQuery
or use session method executeSelectingCall
in any session type because these do not modify data.
Example 99-11 illustrates using SQLCall
with the unit of work method executeNonSelectingCall
.
Example 99-11 Using the executeNonSelectingCall Method
uow.executeNonSelectingCall(new SQLCall(mySqlString));
WARNING: Allowing an unverified SQL string to be passed into methods makes your application vulnerable to SQL injection attacks. |
"Deleting Objects" explained that TopLink always properly arranges (orders) the SQL based on the mappings and foreign keys in your object model and schema. You can control the order of delete operations by the following:
By default, TopLink does insert and update operations first, before delete operations, to ensure that referential integrity is maintained. This is the preferred approach.
If you are forced to replace an object with unique constraints by deleting it and inserting a replacement, you may cause a constraint violation if the insert operation occurs before the delete operation. In this case, call setShouldPerformDeletesFirst
to perform the delete operation before the insert operation.
The constraints used by TopLink to determine delete order are inferred from one-to-one and one-to-many mappings. If you do not have such mappings, you can add constraint knowledge to TopLink using the descriptor addConstraintDependencies(Class)
method.
For example, suppose you have a composition of objects: A contains B (one-to-many, privately owned) and B has a one-to-one, nonprivate relationship with C. You want to delete A (and in doing so the included Bs) but before deleting the Bs, for some of them (not all) you want to delete the associated object C.
There are two possible solutions:
In the first option, you do not identify the one-to-many (A to B) relationship as privately owned. When deleting an A object, you must delete all of its B objects, as well as any C objects, as shown in the following example:
uow.deleteObject(existingA);
uow.deleteAllObjects(existingA.getBs());
// delete one of the Cs
uow.deleteObject(((B) existingA.getBs().get(1)).getC());
This option produces the following SQL:
DELETE FROM B WHERE (ID = 2) DELETE FROM B WHERE (ID = 1) DELETE FROM A WHERE (ID = 1) DELETE FROM C WHERE (ID = 1)
In the second option, keep the one-to-many (A to B) relationship privately owned and add a constraint dependency from A to C, as shown in the following example:
session.getDescriptor(A.class).addConstraintDependencies(C.class);
Now the delete code would be:
uow.deleteObject(existingA);
// delete one of the Cs
uow.deleteObject(((B) existingA.getBs().get(1)).getC());
This option produces the following SQL:
DELETE FROM B WHERE (A = 1) DELETE FROM A WHERE (ID = 1) DELETE FROM C WHERE (ID = 1)
In both cases, the B object is deleted before A and C. The main difference is that the second option generates fewer SQL statements, as it knows that it is deleting the entire set of Bs related from A.
If your descriptors are configured to use an optimistic version locking policy (see "Optimistic Version Locking Policies") or field locking policy (see "Optimistic Field Locking Policies"), use the unit of work method forceUpdateToVersionField
to solve either or both of the following problems:
You want an OptimisticLockingException
thrown at commit time if an object you read in a transaction has changed since it was registered even though you have not changed the object in your transaction (see "Forcing a Check of the Optimistic Read Lock").
You modify an object in a transaction in such a way that its version field is not updated but you want to alert other threads of the change by way of the version field (see "Forcing a Version Field Update").
For example, you change a privately owned object that has its own database table so the parent object's version field is not, by default, updated. In this case, you can use forceUpdateToVersionField
to update the parent's version field.
As an alternative to this approach, consider "Optimistic Version Locking Policies and Cascading".
To remove forceUpdateToVersionField
configuration from an object before a commit operation, use the unit of work method removeForceUpdateToVersionField
(see "Disabling forceUpdateToVersionField").
When you read an object with the unit of work, optimistic lock checking is not applied to that object at commit time unless you change the object. However, there are times when you want your transaction to fail if the state of an object has changed since it was read, even though you have not modified the object.
Example 99-12 shows a transaction that updates a mortgage rate by multiplying the central bank prime rate by 1.25. The transaction forces an optimistic read lock on the central prime rate at commit time to ensure that the prime rate has not changed since the transaction began. Note that in this example, the transaction does not increment the version of the unchanged object (the central prime rate).
Example 99-12 Optimistic Read Lock with No Version Increment
try { UnitOfWork uow = session.acquireUnitOfWork(); MortgageRate cloneMortgageRate = (MortgageRate) uow.registerObject(mortgageRate); CentralPrimeRate cloneCentralPrimeRate = (CentralPrimeRate) uow.registerObject(CentralPrimeRate); /* Change the Mortgage Rate */ cloneMortgageRate.setRate(cloneCentralPrimeRate.getRate() * 1.25); /* Optimistic read lock check on Central prime rate with no version update */ uow.forceUpdateToVersionField(cloneCentralPrimeRate, false); uow.commit(); } catch(OptimisticLockException exception) { /* Refresh the out-of-date object */ session.refreshObject(exception.getObject()); /* Retry… */ }
For another example that forces both optimistic locking and a version field update, see Example 99-13 in "Forcing a Version Field Update".
The unit of work considers an object changed when you modify its direct-to-field or aggregate object mapping attribute. Adding, removing, or modifying objects related to the source object does not render the source object changed for the purposes of the unit of work. In other words, when a relationship is changed in a one-to-many or one-to-one target foreign key mapping, by default, the version field (if any) of the affected object is not changed.
If you configure a descriptor to refresh the cache only if the database version is newer than the cache version (using descriptor method onlyRefreshCacheIfNewerVersion
), and such a relationship changes, you will not be able to refresh the object at all. Because the version has not changed, the unit of work method refreshObject
and even a query with refreshIdentityMapResults
option set to true
cannot refresh the object.
Using the unit of work method forceUpdateToVersionField
passing in both the unit of work copy clone and true
value will ensure that the object's version field is updated when such a change is made. It will also ensure that changes to the object before it is refreshed will result in optimistic locking exceptions, preventing the writing of stale data (see "Forcing a Check of the Optimistic Read Lock").
Example 99-13 and Example 99-14 show transactions executing in separate threads that access the same customer object concurrently. The unit of work method forceUpdateToVersionField is used to ensure that changes to the customer object in one thread are detected by the other threads.
Example 99-13 shows a transaction in which an invoice thread calculates an invoice for a customer. Example 99-14 shows a transaction in which another thread, the service thread, adds a service to the same customer or modifies the current service. In either case, the service thread must inform the invoice thread, which adds the changes to the invoice.
Example 99-13 Optimistic Read Lock with Version Increment: Service Thread
/* The following code represents the service thread. Notice that the thread forces a version update */ try { UnitOfWork uow = session.acquireUnitOfWork(); Customer cloneCustomer = (Customer uow.registerObject(customer); Service cloneService = (Service uow.registerObject(service); /* Add a service to customer */ cloneService.setCustomer(cloneCustomer); cloneCustomer.getServices().add(cloneService); /* Modify the customer version to inform other application that the customer has changed */ uow.forceUpdateToVersionField(cloneCustomer, true); uow.commit(); } catch (OptimisticLockException exception) { /* Refresh out-of-date object */ session.refreshObject(exception.getObject()); /* Retry… */ }
Example 99-14 Optimistic Read Lock with Version Increment: Invoice Thread
/* The following code represents the invoice thread, and calculates a bill for the customer. Notice that it does not force an update to the version */ try { UnitOfWork uow = session.acquireUnitOfWork(); Customer cloneCustomer = (Customer) uow.registerObject(customer); Invoice cloneInvoice = (Invoice) uow.registerObject(new Invoice()); cloneInvoice.setCustomer(cloneCustomer); /* Calculate service charge */ int total = 0; for(Enumeration enum = cloneCustomer.getServices().elements(); enum.hasMoreElements();) { total += ((Service) enum.nextElement()).getCost(); } cloneInvoice.setTotal(total); /* Force optimistic lock checking on the customer to guarantee a valid calculation */ uow.forceUpdateToVersionField(cloneCustomer, false); uow.commit(); } catch(OptimisticLockException exception) { /* Refresh the customer and its privately owned parts */ session.refreshObject(cloneCustomer); /* If the customer's services are not privately owned then use a ReadObjectQuery to refresh all parts */ ReadObjectQuery query = new ReadObjectQuery(customer); /* Refresh the cache with the query's result and cascade refreshing to all parts including customer's services */ query.refreshIdentityMapResult(); query.cascadeAllParts(); /* Refresh from the database */ query.dontCheckCache(); session.executeQuery(query); /* Retry… */ }
The forceUpdateToVersionField
configuration you apply to an object stays in effect for the lifetime of your unit of work. After you commit your transaction, forceUpdateToVersionField
configuration no longer applies.
To remove forceUpdateToVersionField
configuration from an object before commit time, use the unit of work method removeForceUpdateToVersionField
. TopLink will not apply optimistic read locking to the object unless you change it in this transaction (that is, unless you modify its direct-to-field or aggregate object mapping attribute).
Auditing datasource changes is a common requirement: when a user commits a change to the datasource, the application updates a field in the datasource to record the user who made the change and the date.
For example, suppose each row in your database schema includes fields lastUpdateBy
(to record the user name of the user who commits a change) and lastUpdateOn
(to record the date of the change).
You can use UnitOfWork
method setProperty
to record the name of the user who acquires the UnitOfWork
and implement a descriptor event listener for AboutToUpdateEvent
descriptor events that extracts the property and updates the lastUpdateBy
and lastUpdateOn
fields.
For more information, see the following:
To support transactions managed by an application server's external transaction service, TopLink supports external connection pools and external transaction controller classes for supported servers. This lets you incorporate external transaction service support into your application, and use the unit of work with transactions managed externally by the server. For more information, see "Unit of Work Transaction Demarcation".
To integrate a unit of work with an external transaction service, you must enable the use of the following:
an external transaction controller (see "Configuring the Server Platform")
an external connection pool (see "Configuring External Connection Pooling")
After you configure external connection pool and external transaction controller support, you use a unit of work in your TopLink application as you would typically, with few exceptions. This section describes these exceptions as follows:
Acquiring a Unit of Work With an External Transaction Service
Using the Unit of Work to Handle External Transaction Timeouts and Exceptions
You use a unit of work to commit changes to a data source even when using an external transaction service. To ensure that only one unit of work is associated with a given transaction, use the getActiveUnitOfWork
method to acquire a unit of work as shown in Example 99-15.
Note: Although there are other ways to commit changes to a data source using an external transaction service, Oracle recommends using thegetActiveUnitOfWork method. |
The getActiveUnitOfWork
method searches for an existing external transaction in the following way:
If there is an active external transaction and a unit of work is already associated with it, return this unit of work.
If there is an active external transaction with no associated unit of work, then acquire a new unit of work, associate it with the transaction, and return it.
If there is no active external transaction in progress, return null.
If TopLink returns a unit of work that is not null
, use it exactly as you would typically: the only exception is that you do not call the commit
method (see "Using a Unit of Work When an External Transaction Exists").
If TopLink returns a null
unit of work, start an external transaction explicitly through the UserTransaction
interface.
Example 99-15 Using a Unit of Work With an External Transaction Service
// Read in any pet
Pet pet = (Pet)clientSession.readObject(Pet.class);
UnitOfWork uow = clientSession.getActiveUnitOfWork();
if (uow == null) {
throw new RuntimeException("No transaction started");;
}
Pet petClone = (Pet)uow.registerObject(pet);
petClone.setName("Furry");
When getActiveUnitOfWork
returns a unit of work that is not null
, you are associated with an existing external transaction. Use the unit of work as usual.
As the external transaction was not started by the unit of work, issuing a commit
on it will not cause the external transaction to be committed. The unit of work will defer to the application or container that began the transaction. When the external transaction does get committed by the container, TopLink receives synchronization callbacks at key points during the commit transaction.
The unit of work sends the required SQL to the database when it receives the beforeCompletion
callback.
The unit of work uses the Boolean
argument received from the afterCompletion
callback to determine if the commit was successful (true
) or not (false
).
If the commit transaction was successful, the unit of work merges changes to the session cache. If the commit transaction was unsuccessful, the unit of work discards the changes.
Figure 99-2 shows the life cycle of a unit of work when an external transaction exists.
Figure 99-2 Unit of Work When an External Transaction Exists
When the getActiveUnitOfWork
method returns a null unit of work, there is no existing external transaction. You must start a new external transaction.
Do this either by starting an external transaction explicitly using the UserTransaction
interface, or by acquiring a new unit of work using the acquireUnitOfWork
method on the server session.
Use the unit of work as usual.
Once the modifications to registered objects are complete, you must commit the transaction either explicitly through the UserTransaction
interface or by calling the unit of work commit
method.
The transaction synchronization callbacks are then invoked on, and the database updates and cache merge occur based upon those callbacks.
Figure 99-3 Unit of Work When No External Transaction Exists
This section describes two common problems with external transactions:
When an external transaction is committed, the external transaction service expects each transaction owner to commit its portion of the overall transaction within a finite amount of time. If any individual transaction exceeds this timeout interval, the external transaction service will fail the specific transaction and roll it back (or mark it rollback only).
If your transaction is large and its commit transaction may exceed the external transaction service timeout interval, use UnitOfWork
method writeChanges
to write changes to the data source before committing the external transaction. This will reduce the time it takes for your part of the global transaction to commit.
For more information about the UnitOfWork
method writeChanges
, including restrictions and warnings, see "Writing Changes Before Commit Time".
When you use the unit of work with an external transaction service, commit exceptions may not be thrown until long after your application thread calls its UnitOfWork
method commit
and returns. In this case, commit exceptions are thrown to the client of the container-managed transaction (CMT) call, forcing the client to handle this server-side failure.
You can use the UnitOfWork
method writeChanges
to write changes to the data source before the external transaction commits. This allows your application thread to catch and handle most exceptions that could be thrown at the time the external transaction service commits the global transaction.
For more information about the UnitOfWork
method writeChanges
, including restrictions and warnings, see "Writing Changes Before Commit Time".
For more information on handling unit of work exceptions in general, see "Handling Exceptions".
All modifications to persistent beans should be carried out in the context of a transaction.
Modifying entity beans without a transaction can lead to an inconsistent state, potentially corrupting the values in the TopLink cache. Because of this, TopLink does not support modifying a bean through its remote interface when no transaction is active. If you attempt to do so, TopLink simply does not write changes to the database.
Although TopLink does not let you modify an entity bean through its remote interface without a transaction, TopLink does let you invoke methods on its home interface that change the state in the underlying database without a transaction. For example, you may invoke remove and create methods on the home interface of an entity bean without a transaction.
To integrate TopLink transactions and the unit of work with container-managed persistence (CMP), you must consider the following:
To ensure that all modifications to persistent beans are carried out in the context of a transaction, transactional attributes must be properly specified in the bean deployment descriptors.
The transaction may be either client-controlled or container-controlled.
Client-controlled transactions are started explicitly by your application by way of the javax.transaction.UserTransaction
interface.
Container-controlled transactions are started implicitly by the container to satisfy the transaction attribute configuration when a bean method is invoked in the absence of a client-controlled transaction.
Table 99-2 shows what transaction (if any) an EJB method invocation uses depending on how its transaction attribute is configured and whether or not a client-controlled transaction exists at the time the method is invoked.
Oracle recommends that you do not make modifications to entity beans under conditions identified as "Use no transaction" in Table 99-2. Oracle also recommends that you avoid using the Supports
transaction attribute because it leads to a nontransactional state whenever the client does not explicitly provide a transaction.
Table 99-2 EJB Transaction State by Transaction Attribute
Transaction Attribute | Client-Controlled Transaction Exists | Client-Controlled Transaction Does Not Exist |
---|---|---|
|
Use no transaction |
Use no transaction |
|
Use client-controlled transaction |
Use no transaction |
|
Use client-controlled transaction |
Use container-controlled transaction |
|
Use client-controlled transaction |
Use container-controlled transaction |
|
Use client-controlled transaction |
Exception raised |
|
Exception raised |
Use no transaction |
Depending on the EJB container you use, you may be able to write without a container-controlled transaction (see "Integrating the Unit of Work With an External Transaction Service"). In this case, TopLink automatically uses a transaction of its own, referred to as a local transaction (see "Local Transactions").
Some EJB containers (such as OC4J) support writing without an active JTA transaction.
If you execute a bean method outside a JTA transaction while the transaction attribute (see "CMP Transaction Attribute") is set to Supports
, NotSupported
, or Never
, TopLink performs the operation within a local unit of work and commits the unit of work at the end of the method. This unit of work is referred to as a local transaction.
The reason for this is because the update semantics in the EJB specification are left undefined for these scenarios, and a proper transactional model demands that a transaction be active before being able to modify data. TopLink also requires change operations to occur within a unit of work to ensure that the session cache remains consistent.
Some EJB containers (such as OC4J) support nondeferred changes: the ability to modify the data source immediately as you change the persistent fields of an entity bean.
Using nondeferred changes, you can achieve backwards compatibility with the native behavior of some EJB containers (such as OC4J) and you can accommodate advanced applications that rely on the database and entity changes being synchronized for such things as triggers or stored procedures based on transient state within the transaction, deletion and creation of rows with the same primary key, or other complex queries that depend on transient transaction state.
Nondeferred changes have the disadvantage of being the least efficient approach: they produce the greatest number of data source interactions.
By default, TopLink defers all changes until commit time. This is the most efficient approach that produces the least number of data source interactions.
For more information, see "Nondeferred Changes".
Achieving a particular database transaction isolation level in a TopLink application is more involved than simply using the DatabaseLogin
method setTransactionIsolation
.
In a typical TopLink application and in J2EE applications that require persistence in general, a variety of factors affect when database transaction isolation levels apply and to what extent a particular database transaction isolation can be achieved.
This section describes these factors and provides guidelines on configuring and using TopLink to achieve each database transaction isolation level to the extent possible given these factors.
This section includes the following:
This section describes some of the important factors and variables that may affect the degree to which your TopLink application can achieve a particular database transaction isolation level. These factors include the following:
In many cases, your TopLink application is not the only application that can update to the database. External, non-TopLink applications, can also update the database at any time.
In this case, your TopLink application must use the ObjectLevelReadQuery
method refreshIdentityMapResult
(see "Refreshing the Cache") or Descriptor
methods alwaysRefreshCache
and disableCacheHits
(see "Configuring Cache Refreshing").
For more information, see "Managing Cache Access".
If the external application can update a version field in the database, your TopLink application could use alwaysRefreshCache
in conjunction with Descriptor
method onlyRefreshCacheIfNewerVersion
to ensure that refresh operations are performed only when required. Another, recommended way to achieve this, is to use the descriptor isolated cache option (see "Cache Isolation"), as well as cache invalidation (see "Cache Invalidation").
Consider multiple TopLink applications (each running on its own application server instance) configured to use a distributed, coordinated cache (as described in "Understanding Cache Coordination"). A TopLink application instance first commits changes to its own cache before the change is distributed to other caches. Because cache coordination is not instantaneous, there is a possibility that one TopLink application instance may read an older version of an object from its cache before a cache coordination message is received.
To provide your TopLink application with the most up-to-date version of an object use the descriptor isolated cache option (see "Cache Isolation"), as well as cache invalidation (see "Cache Invalidation").
You can also avoid stale data by using Descriptor
methods alwaysRefreshCache
and disableCacheHits
. For more information on the disableCacheHits
method, see "Managing Cache Access".
Caution: UsingDescriptor methods alwaysRefreshCache and disableCacheHits will result in frequent database hits. Use only when absolutely necessary. |
Use the DatabaseLogin
method setTransActionIsolation
to configure the database transaction isolation level that TopLink applies to any database connection it obtains, for example:
databaseLogin.setTransactionIsolation(DatabaseLogin.TRANSACTION_SERIALIZABLE);
This method sets the transaction isolation level used for both database read and write operations on the database connections obtained from either an internal or external connection pool (see "Connection Pools"), for both internal transactions and external transactions as in the case of CMP.
However, with TopLink, by default read operations use a different database connection than write operations, typically obtained from an external connection pool, or may use the cache, bypassing the database entirely. Thus, with TopLink, by default, read operations are always performed outside the transaction or unit of work, even if you perform the read operation within a transaction or unit of work. Although database transaction isolation applies to both read and write connections, the read is not performed as part of the transaction. Therefore, the read operation overrides the transaction isolation set on the database.
Depending on the level of transaction isolation you are trying to achieve, you may require that the same transaction isolation be applied to both read and write operations. You must take special action to make TopLink use the same connection for both read and write operations. For more information, see "Reading Through the Write Connection".
Recall that TopLink, by default, performs read operations with a different database connection than used for write operations ("DatabaseLogin Method setTransactionIsolation"). However, from the perspective of database transaction isolation, there is a one-to-one relationship between transaction and database connection: that is, all database operations (including read operations) must use the same database connection in order to achieve a particular database transaction isolation level.
In general, when TopLink performs a read operation, if a write connection already exists, TopLink will use the write connection for the read operation. This is called "reading through the write connection." If a write connection does not yet exist, TopLink will acquire another connection and use that for the read operation.
You can configure TopLink to allocate a write connection early using any of the following:
Caution: Depending on the database transaction isolated level reading through the write connection may lock the object being read. This will affect performance and reduce concurrency. Oracle recommends that you do not use these advanced techniques unless strict database transaction isolation is absolutely necessary. |
For more information, see "CMP and External Transactions".
When you use pessimistic locking (ObjectLevelReadQuery
methods acquireLocks
or acquireLocksWithoutWaiting
or Session
method refreshAndLockObject
), TopLink does the following:
Allocates a write connection used for both read and write operations.
Always reads from the database.
Always updates the cache with the database version.
This method is advanced API. If you call beginTransactionEarly
on an instance of a unit of work, all read operations should be performed through that instance of the unit of work.
This method starts a database transaction immediately: any objects you read will lock data in the database before commit time, reducing concurrency.
Client sessions can access the data source using a connection pool or an exclusive connection. To use an exclusive connection, acquire your client session using a ConnectionPolicy
(see "Acquiring a Client Session That Uses Exclusive Connections").
If you are using isolated client sessions (see "Isolated Client Sessions"), you can use exclusive connections for reading isolated data. In this case, you can configure TopLink to acquire an exclusive connection from the write connection pool and use it for both writing and reading isolated data. However, TopLink still acquires a shared connection from the read connection pool for reading nonisolated data.
For more information, see "Exclusive Write Connections".
By default, TopLink uses the shared session cache as much as possible. Doing so increases concurrency and improves performance. However, to achieve a particular transaction isolation level, you may need to avoid the cache using some or all the following:
This method always goes to the database for the initial read operation of an object whose descriptor is configured as isolated. By avoiding the shared session cache, you do not need to use the more complicated descriptor and query APIs to disable cache hits or always refresh. For more information about isolated client sessions, see "Isolated Client Sessions". This is particularly useful for achieving serializable transaction isolation (see "Serializable Read Levels").
This API goes to the database unless it is a primary key-based query, in which case it will go to the cache first. For information on how to avoid the cache entirely in this case, see "Descriptor Method disableCacheHits".
This API always goes to the databases. For information on how to avoid the cache entirely in this case, see the description of the Descriptor
method alwaysRefreshCache
in "Cache Refresh API".
This API allows for cache hits on primary key, read-object queries to be disabled. This can be used with the Descriptor
method alwaysRefreshCache
to ensure queries always go to the database.
This is a query-level means of preventing objects from being added to the shared session cache. Using an isolated client session (see "Isolated Client Session Cache") is a simpler approach to achieving the same ends.
In general, the transaction isolation information in this section applies to both CMP and non-CMP applications, with the following exception:
For application servers other than OC4J, when using a TopLink application with CMP, Oracle recommends that you configure your container to use separate read and write connection pools, and to associate only the write connections with an external transaction. This means the read connections do not participate in the transaction.
Note: OC4J always uses the same connection pool for reading and writing: it uses JTA connections from that pool for writing, and non-JTA connections from the pool for reading. |
However, because TopLink treats EJB finders as just another type of query, you can use your descriptor configuration to exploit the options described in "Reading Through the Write Connection". For example, if you configure a descriptor to use pessimistic locking (see "Configuring Locking Policy"), then when its finder is invoked it will allocate a write connection early and both read and write operations will use the same connection.
Refer to "Externally Managed Transactional Data Sources" for more information on external transactions with transactional data sources.
Oracle does not recommend using this transaction isolation level.
In general, a read uncommitted operation is not necessary. Using TopLink, a transaction isolation of read committed gives you better performance than read uncommitted but with greatly improved data integrity.
Using the unit of work guarantees that you will read only committed data in the shared session cache or committed data in the database.
To achieve repeatable read operations, you must use a unit of work, you must register all objects in the unit of work (both objects you intend to modify and objects you intend only to read), and you must use ObjectLevelReadQuery
method conformResultsInUnitOfWork
or Descriptor
method alwaysConformResultsInUnitOfWork
.
By doing so, each time you query a registered object, you will get the version of the object as it currently is in your unit of work.
To achieve serializable transaction isolation with TopLink, Oracle recommends that you use an isolated client session (see "Isolated Client Sessions") as follows:
Configure the database transaction isolation as serializable.
Configure objects as isolated (see "Configuring Cache Isolation at the Project Level" or "Configuring Cache Isolation at the Descriptor Level").
Use the UnitOfWork
method beginTransactionEarly
(see "Unit of Work Method beginTransactionEarly").
If you are only concerned about the write aspect of serializable, optimistic locking is sufficient.
To prevent phantom read transactions (that is, when a transaction detects that new records that have been added to the database after the transaction started), use the ReadQuery
method cacheQueryResults
.
This section examines common unit of work problems and debugging techniques, including the following:
A common unit of work error is holding on to clones after commit time. Typically the clones are stored in a static variable and the developer incorrectly believes that this object is the cache copy. This leads to problems when another unit of work makes changes to the object and what the developer believes is the cache copy is not updated (because a unit of work updates only the cache copy, not old clones).
Consider the error in Example 99-16. In this example you get a handle to the cache copy of a Pet
and store it in the static CACHE_PET
. We get a handle to a working copy clone and store it in the static CLONE_PET
. In a future unit of work, the Pet
is changed.
Developers who incorrectly store global references to clones from units of work often expect them to be updated when the cache object is changed in a future unit of work. Only the cache copy is updated.
Example 99-16 Incorrect Use of Handle to Clone
// Read a Pet from the database, store in static CACHE_PET = (Pet)session.readObject(Pet.class); // Put a clone in a static. This is a bad idea and is a common error UnitOfWork uow = session.acquireUnitOfWork(); CLONE_PET = (Pet)uow.readObject(Pet.class); CLONE_PET.setName("Hairy"); uow.commit(); //Later, the pet is changed again UnitOfWork anotherUow = session.acquireUnitOfWork(); Pet petClone = (Pet)anotherUow.registerObject(CACHE_PET); petClone.setName("Fuzzy"); anotherUow.commit(); // If you incorrectly stored the clone in a static and thought it should be // updated when it is later changed, you would be wrong: only the cache copy is // updated; NOT OLD CLONES System.out.println("CACHE_PET is" + CACHE_PET); System.out.println("CLONE_PET is" + CLONE_PET);
The two System.out
calls produce the following output:
CACHE_PET isPet type Cat named Fuzzy id:100 CLONE_PET isPet type Cat named Hairy id:100
In "Modifying an Object", it was noted that it is possible to read any particular instance of a class by executing:
session.readObject(Class);
There is also a readObject
method that takes an object as an argument: this method is equivalent to doing a ReadObjectQuery
on the primary key of the object passed in. For example, the following code is equivalent to the code in the subsequent example:
session.readObject(pet);
The following is equivalent to the preceding code:
ReadObjectQuery query = new ReadObjectQuery(); query.setReferenceClass(Pet.class); ExpressionBuilder builder = new ExpressionBuilder(); Expression exp = builder.get("id").equal(pet.getId()); query.setSelectionCriteria(exp); session.executeQuery(query);
Also note that primary key-based queries, by default, will return what is in the cache without going to the database. As a result, you can use very quick and simple method for accessing the cache copy of an object as shown in Example 99-17.
Example 99-17 Testing If an Object Is the Cache Object
//Here is a test to see if an object is the cache copy
boolean cached = CACHE_PET == session.readObject(CACHE_PET);
boolean cloned = CLONE_PET == session.readObject(CLONE_PET);
System.out.println("Is CACHE_PET the Cache copy of the object: " + cached);
System.out.println("Is CLONE_PET the Cache copy of the object: " + cloned);
This code produces the following output:
Is CACHE_PET the Cache copy of the object: true Is CLONE_PET the Cache copy of the object: false
The unit of work has several debugging methods to help you analyze performance or track down problems with your code. The most useful is printRegisteredObjects
, which prints all the information about known objects in the unit of work. Use this method to see how many objects are registered and to make sure objects you are working on are registered.
To use this method, you must have log messages enabled for the session that the unit of work is from. Session log messages are disabled by default. To enable log messages, use the session logMessages
method. To disable log messages, use the session dontLogMessages
method as shown in Example 99-18.
Example 99-18 Dumping the Contents of a Unit of Work
session.logMessages(); // Enable log messages UnitOfWork uow = session.acquireUnitOfWork(); Pet petClone = (Pet)uow.readObject(Pet.class); petClone.setName("Mop Top"); Pet pet2 = new Pet(); pet2.setId(200); pet2.setName("Sparky"); pet2.setType("Dog"); uow.registerObject(pet2); uow.printRegisteredObjects(); uow.commit(); session.dontLogMessages(); // Disable log messages
This example produces the following output:
UnitOfWork identity hashcode: 32373 Deleted Objects: All Registered Clones: Key: [100] Identity Hash Code:13901 Object: Pet type Cat named Mop Top id:100 Key: [200] Identity Hash Code:16010 Object: Pet type Dog named Sparky id:200 New Objects: Key: [200] Identity Hash Code:16010 Object: Pet type Dog named Sparky id:200
This section explains how to handle the following:
TopLink exceptions are instances of RuntimeException
, which means that methods that throw them do not have to be placed in a try
-catch
block.
However, the unit of work commit
method is one that should be called within a try
-catch
block to deal with problems that may arise.
Example 99-19 shows one way to handle unit of work exceptions:
Example 99-19 Handling Unit of Work Commit Exceptions
UnitOfWork uow = session.acquireUnitOfWork(); Pet petClone = (Pet)uow.registerObject(newPet); petClone.setName("Assume this name is too long for a database constraint"); // Assume that the name argument violates a length constraint on the database. // This will cause a DatabaseException on commit try { uow.commit(); } catch (TopLinkException tle) { System.out.println("There was an exception: " + tle); }
This code produces the following output:
There was an exception: EXCEPTION [ORACLEAS TOPLINK-6004]: oracle.toplink.exceptions.DatabaseException
See "Database Exceptions (4002 – 4018)" for more information on DatabaseException
.
If you use optimistic locking, you must catch exceptions at commit time because the exception raised is the indication that there was an optimistic locking problem. Optimistic locking allows all users to access a given object, even if it is currently in use in a transaction or unit of work. When the unit of work attempts to change the object, the database checks to ensure that the object has not changed since it was initially read by the unit of work. If the object has changed, the database raises an exception, and the unit of work rolls back the transaction. For more information, see "Locking and the Unit of Work".
If you are using an external transaction service, exceptions may be thrown long after your UnitOfWork
code has returned. Using UnitOfWork
method writeChanges
, you can catch and handle most exceptions before the external transaction is committed. For more information, see "External Transaction Commit Exceptions".
You can conform query results in a unit of work across one-to-many relationships and a combination of both one-to-one and one-to-many relationships. Example 99-20 illustrates a query across two levels of relationships, one-to-many and one-to-one.
Example 99-20 Querying Across Two Levels of Relationship
Expression exp = bldr.anyOf("managedEmployees").get("address").get("city").equal("Perth");
By default, any exceptions thrown during conforming are suppressed. However, you can use the UnitOfWork
method setShouldThrowConformExceptions
to make the unit of work throw all conforming exceptions. This method takes one int
argument with the following values:
0–do not throw conform exceptions (default)
1–throw all conform exceptions
For more information on customizing exception handling when using conforming and in-memory queries, see "Handling Exceptions Resulting From In-Memory Queries".
The unit of work validates object references at commit time. If an object registered in a unit of work references other unregistered objects, this violates object transaction isolation, and causes TopLink validation to raise an exception.
Although referencing unregistered objects from a registered object can corrupt the session cache, there are applications in which you want to disable validation. TopLink offers the following APIs to toggle validation:
dontPerformValidation
: disables validation
performFullValidation
: enables validation
If the unit of work detects an error when merging changes into the session cache, it throws a QueryException
. Although this exception specifies the invalid object and the reason it is invalid, it may still be difficult to determine the cause of the problem.
In this case, you can use the validateObjectSpace
method to test registered objects and provide the full stack trace of all traversed objects. This may help you more easily find the problem. You can call this method at any time on a unit of work.