Skip Headers

Oracle Application Server TopLink Application Developer's Guide
10g (9.0.4)

Part Number B10313-01
Go To Documentation Library
Home
Go To Product List
Solution Area
Go To Table Of Contents
Contents
Go To Index
Index

Go to previous page Go to next page

7
Transactions

A database transaction is a set of operations (create, read, update, or delete) that either succeed or fail as a single operation. The database discards, or rolls back, unsuccessful transactions, leaving the database in its original state.

In OracleAS TopLink, transactions are encapsulated by the Unit of Work object. Using the Unit of Work, you can transactionally modify objects directly or by way of a Java 2 Enterprise Edition (J2EE) external transaction controller such as the Java Transaction API (JTA).

This chapter explains how to use the OracleAS TopLink Unit of Work, including:

Introduction to Transaction Concepts

This section describes generic database transaction concepts and how they apply to the OracleAS TopLink Unit of Work.

Database Transactions

Transactions execute in their own context, or logical space, isolated from other transactions and database operations.

The transaction context is demarcated; that is, it has a defined structure that includes:

The degree to which concurrent (parallel) transactions on the same data are allowed to interact is determined by the level of transaction isolation configured. ANSI/SQL defines four levels of database transaction isolation as shown in Table 7-1. Each offers a trade-off between performance and resistance from the following unwanted behaviors:

As a transaction is committed, the database maintains a log of all changes to the data. If all operations in the transaction succeed, the database allows the changes; if any part of the transaction fails, the database uses the log to roll back the changes.

OracleAS TopLink Unit of Work Transactions

In OracleAS TopLink, transactions are encapsulated by the Unit of Work object. Like any transaction, a Unit of Work transaction provides:

Transaction Context

Unit of Work operations occur within a Unit of Work context, isolated from the database until commit time. The Unit of Work executes changes on copies, or clones, of objects in its own internal cache, and if successful, applies changes to objects in the database and the session cache.

Transaction Demarcation

If your application is a stand-alone OracleAS TopLink application, your application demarcates transactions using the Unit of Work.

If your application includes a J2EE container that provides container-managed transactions, you can configure OracleAS TopLink to integrate with the container's transaction demarcation. The Unit of Work supports:

JTA Transaction Demarcation

J2EE containers use JTA to manage transactions in the application. If your application includes a J2EE container, the Unit of Work executes as part of an external JTA transaction. The Unit of Work still manages its own internal operations, but relies on the external transaction to commit changes to the database. The Unit of Work waits for the external transaction to commit successfully before writing changes back to the session cache.

Note that because the transaction happens outside of the Unit of Work context and is controlled by the JTA, errors can be more difficult to diagnose and fix.

For more information, see "J2EE Integration".

CMP Transaction Demarcation

Entity beans that use container-managed persistence can participate in either client-demarcated or container-demarcated transactions. They can demarcate transactions with the javax.transaction.UserTransaction interface. OracleAS TopLink automatically wraps invocations on entity beans in container transactions based on the transaction attributes in the EJB deployment descriptor. For more information about transactions with EJBs, see the EJB specification and your J2EE container documentation.

In transactions involving EJBs, OracleAS TopLink waits until the transaction begins its two-stage commit process before updating the database. This allows for:

Transaction Isolation

OracleAS TopLink DatabaseLogin API allows you to set the transaction isolation level used when you open a connection to a database:

databaseLogin.setTransactionIsolation(DatabaseLogin.TRANSACTION_SERIALIZABLE);

However, the Unit of Work does not participate in database transaction isolation. Because the Unit of Work may execute queries outside the database transaction, the database has no control over the data and its visibility outside the transaction.

To maintain transaction isolation, each Unit of Work instance operates on its own copy (clone) of affected objects (see "Clones and the Unit of Work"). Multiple reads to the same object return the same clone and the clone's state is from when it was first accessed (registered).

Optimistic locking, optimistic read locking, or pessimistic locking can be used to ensure concurrency (see "Locking Policy").

The Unit of Work method ShouldAlwaysConformResultsInUnitOfWork allows querying to be done on object changes within a Unit of Work (see "Using Conforming Queries and Descriptors").

Changes are committed to the database only when the Unit of Work commit method is called (either directly or by way of an external transaction controller.)

Understanding the Unit of Work

This section describes:

Unit of Work Benefits

The OracleAS TopLink Unit of Work simplifies transactions and improves transactional performance. It is the preferred method of writing to a database in OracleAS TopLink because it:

Unit of Work Life Cycle

The Unit of Work is used as follows:

  1. Client application acquires a Unit of Work from a session object.

  2. Client application queries OracleAS TopLink to obtain the cache objects it wants to modify and then registers the cache objects with the Unit of Work.

  3. When the first object is registered, the Unit of Work starts its transaction.

    As each object is registered, the Unit of Work accesses the object from the Session cache or database and creates a backup clone and working clone (see "Clones and the Unit of Work").

    The Unit of Work returns the working clone to the client application.

  4. Client application modifies the working clones.

  5. Client application (or external transaction controller) commits the transaction (see "Commit and Rollback").

    Figure 7-1 The Life Cycle of a Unit of Work

    Text description of uow_life.gif follows

    Text description of the illustration uow_life.gif

Example 7-1 shows the life cycle in code.

Example 7-1 Unit of Work Life Cycle

// The application reads a set of objects from the database.
Vector employees = session.readAllObjects(Employee.class);

// The application specifies an employee to edit.
. . .
Employee employee = (Employee) employees.elementAt(index);

try {
    // Acquire a Unit of Work from the session.
    UnitOfWork uow = session.acquireUnitOfWork();
    // Register the object that is to be changed. Unit of Work returns a clone
    // of the object and makes a backup copy of the original employee
    Employee employeeClone = (Employee)uow.registerObject(employee);
    // We make changes to the employee clone by adding a new phoneNumber. 
    // If a new object is referred to by a clone, it does not have to be
    // registered. Unit of Work determines it is a new object at commit time.
    PhoneNumber newPhoneNumber = new PhoneNumber("cell","212","765-9002");
    employeeClone.addPhoneNumber(newPhoneNumber);
    // We commit the transaction: Unit of Work compares the employeeClone with
    // the backup copy of the employee, begins a transaction, and updates the
    // database with the changes. If successful, the transaction is committed
    // and the changes in employeeClone are merged into employee. If there is an
    // error updating the database, the transaction is rolled back and the
    // changes are not merged into the original employee object.
    uow.commit();
} catch (DatabaseException ex) {
    // If the commit fails, the database is not changed. The Unit of Work should
    // be thrown away and application-specific action taken.
}
// After the commit, the Unit of Work is no longer valid. Do not use further.

Clones and the Unit of Work

The Unit of Work maintains two copies of the original objects registered with it:

After you change the working clones and the transaction is committed, the Unit of Work compares the working copy clones to the backup copy clones, and writes any changes to the database. The Unit of Work uses clones to allow parallel Units of Work (see "Nested and Parallel Units of Work") to exist, a requirement in multi-user three-tier applications.

The OracleAS TopLink cloning process is efficient in that it clones only the mapped attributes of registered objects, and stops at indirection objects unless you trigger the indirection. For more information, see "Indirection".

You can customize the cloning process using the descriptor's copy policy. For more information, see "Descriptor Copy Policy".

Never use a clone after committing the Unit of Work that the clone is from (even if the transaction fails and rolls-back). A clone is a working copy used during a transaction and as soon as the transaction is committed (successful or not), the clone must not be used. Accessing an uninstantiated clone value holder after a Unit of Work commit will raise an exception. The only time you can use a clone after a successful commit is when you use the advanced API described in "Resuming a Unit of Work After Commit".

Nested and Parallel Units of Work

You can use OracleAS TopLink to create a:

For information and examples on using nested and parallel Units of Work, see "Using a Nested or Parallel Unit of Work".

Nested Unit of Work

You can nest a Unit of Work (the child) within another Unit of Work (the parent). A nested Unit of Work does not commit changes to the database. Instead, it passes its changes to the parent Unit of Work, and the parent attempts to commit the changes at commit time. Nesting Units of Work enables you to break a large transaction into smaller isolated transactions, and ensures that:

Parallel Unit of Work

You can modify the same objects in multiple Unit of Work instances in parallel because the Unit of Work manipulates copies of objects. OracleAS TopLink resolves any concurrency issues when the Units of Work commit.

Reading and Querying Objects with the Unit of Work

A Unit of Work is a Session, and as such, offers the same set of database access methods as a regular session.

When called from a Unit of Work, these methods access the objects in the Unit of Work, register the selected objects automatically, and return clones.

Although this makes it unnecessary for you to call the registerObject and registerAllObjects methods, be aware of the restrictions on registering objects described in "Creating an Object" and "Associations: New Source to Existing Target Object".

Reading Objects with the Unit of Work

As with regular sessions, you use the readObject and readAllObjects methods to read objects from the database.

Querying Objects with the Unit of Work

You can execute queries in a Unit of Work with the executeQuery method.


Note:

Because a Unit of Work manages changes to existing objects and the creation of new objects, modifying queries such as InsertObjectQuery or UpdateObjectQuery are not necessary and therefore are not supported by the Unit of Work.


Commit and Rollback

When a Unit of Work transaction is committed, it either succeeds or fails and rolls back. A commit can be initiated by your application or a J2EE container.

Commit

At commit time, the Unit of Work compares the working clones and backup clones to calculate the change set (that is, to determine the minimum changes required). Changes include updates to or deletion of existing objects, and the creation of new objects. The Unit of Work then begins a database transaction, and attempts to write the changes to the database. If all changes commit successfully on the database, the Unit of Work merges the changed objects into the session cache. If any of the changes fail on the database, the Unit of Work rolls back any changes on the database, and does not merge changes into the session cache.

The Unit of Work calculates commit order using foreign key information from one-to-one and one-to-many mappings. If you encounter constraint problems during commit, verify your mapping definitions. The order in which you register objects with the registerObject method does not affect the commit order.

Commit and JTA

When your application uses JTA, the Unit of Work commit behaves differently than in a non-JTA application. In most cases, the Unit of Work attaches itself to an external transaction. If no transaction exists, the Unit of Work creates a transaction. This distinction affects commit behavior as follows:

In either case, only the external transaction can call commit on the database because it owns the database connection.

For more information, see "J2EE Integration".

Rollback

A Unit of Work commit must succeed or fail as a unit. Failure in writing changes to the database causes the Unit of Work to roll back the database to its previous state. Nothing changes in the database, and the Unit of Work does not merge changes into the session cache.

Rollback and JTA

In a JTA environment, the Unit of Work does not own the database connection. In this case, the Unit of Work sends the rollback call to the external transaction rather than the database, and the external transaction treats the rollback call as a request to roll the transaction back.

For more information, see "J2EE Integration".

Primary Keys

You cannot modify the primary key attribute of an object in a Unit of Work. This is an unsupported operation and doing so will result in unexpected behaviour (exceptions and/or database corruption).

To replace one instance of an object with unique constraints with another, see "Using the Unit of Work setShouldPerformDeletesFirst Method".

Example Object Model and Schema

Throughout this chapter, the following object model and schema is used in the examples provided. The example object model appears in Figure 7-2 and the example entity-relationship (data model) diagram appears in Figure 7-3.

Figure 7-2 Example Object Model

Text description of xobjmod.gif follows

Text description of the illustration xobjmod.gif

Figure 7-3 Example Data Model

Text description of xerdiag.gif follows

Text description of the illustration xerdiag.gif

Unit of Work Basics

This section explores the essential Unit of Work API calls most commonly used throughout the development cycle:

For more information about the available methods for the UnitOfWork, see "Advanced Unit of Work" and the Oracle Application Server TopLink API Reference.

Acquiring a Unit of Work

This example shows how to acquire a Unit of Work from a client session object.

Server server = 
    (Server) SessionManager.getManager().getSession(
        sessionName, MyServerSession.class.getClassLoader()
    );
Session session = (Session) server.acquireClientSession();
UnitOfWork uow = session.acquireUnitOfWork();

You can acquire a Unit of Work from any session type. Note that you do not need to create a new session and login before every transaction.

The Unit of Work is valid until the commit or release method is called. After a commit or release, a Unit of Work is not valid even if the transaction fails and is rolled back.

A Unit of Work remains valid after the commitAndResume method is called as described in "Resuming a Unit of Work After Commit".

When using a Unit of Work with JTA, you can also use the advanced API getActiveUnitOfWork method as described in "J2EE Integration".

Creating an Object

When you create new objects in the Unit of Work, use the registerObject method to ensure that the Unit of Work writes the objects to the database at commit time.

The Unit of Work calculates commit order using foreign key information from one-to-one and one-to-many mappings. If you encounter constraint problems during commit, verify your mapping definitions. The order in which you register objects with the registerObject method does not affect the commit order.

Example 7-2 and Example 7-3 show how to create and persist a simple object (without relationships) using the clone returned by the Unit of Work registerObject method.

Example 7-2 Creating an Object: Preferred Method

UnitOfWork uow = session.acquireUnitOfWork();
    Pet pet = new Pet();
    Pet petClone = (Pet)uow.registerObject(pet);
    petClone.setId(100);
    petClone.setName("Fluffy");
    petClone.setType("Cat");
uow.commit();

Example 7-3 shows a common alternative:

Example 7-3 Creating an Object: Alternative Method

UnitOfWork uow = session.acquireUnitOfWork();
    Pet pet = new Pet();
    pet.setId(100);
    pet.setName("Fluffy");
    pet.setType("Cat");
    uow.registerObject(pet); 
uow.commit();

Both approaches produce the following SQL:

INSERT INTO PET (ID, NAME, TYPE, PET_OWN_ID) VALUES (100, 'Fluffy', 'Cat', NULL)

Example 7-2 is preferred: it gets you into the pattern of working with clones and provides the most flexibility for future code changes. Working with combinations of new objects and clones can lead to confusion and unwanted results.

Modifying an Object

In Example 7-4, a Pet is read prior to a Unit of Work: the variable pet is the cache copy for that Pet. Inside of the Unit of Work, we must register the cache copy to get a working copy. We then modify the working copy and commit the Unit of Work.

Example 7-4 Modifying an Object

// Read in any pet.
Pet pet = (Pet)session.readObject(Pet.class);
UnitOfWork uow = session.acquireUnitOfWork();
    Pet petClone = (Pet) uow.registerObject(pet);
    petClone.setName("Furry");
uow.commit();

In Example 7-5, we take advantage of the fact that you can query through a Unit of Work and get back clones, saving the registration step. However, the drawback is that we do not have a handle to the cache copy.

If we wanted to do something with the updated Pet after commit, we would have to query the session to get it (remember that after a Unit of Work is committed, its clones are invalid and must not be used).

Example 7-5 Modifying an Object: Skipping the Registration Step

UnitOfWork uow = session.acquireUnitOfWork();
    Pet petClone = (Pet) uow.readObject(Pet.class);
    petClone.setName("Furry");
uow.commit();

Both approaches produce the following SQL:

UPDATE PET SET NAME = 'Furry' WHERE (ID = 100)

Take care when querying through a Unit of Work. All objects read in the query are registered in the Unit of Work and therefore will be checked for changes at commit time. Rather than do a ReadAllQuery through a Unit of Work, it is better for performance to design your application to do the ReadAllQuery through a session and then only register in a Unit of Work the objects that need to be changed.

Associations: New Target to Existing Source Object

There are two ways to associate a new target object with an existing source object with 1-many and 1-1 relationships:

Deciding which approach to use depends on whether or not your code requires a reference to the cache copy of the new object after the Unit of Work is committed and on how adaptable to change you want your code to be.

Associating without Reference to the Cache Object

Example 7-6 shows the first way of associating a new target with an existing source.

Example 7-6 Associating without Reference to the Cache Object

UnitOfWork uow = session.acquireUnitOfWork();
    Pet petClone = (Pet)uow.readObject(Pet.class);

    PetOwner petOwner = new PetOwner();
    petOwner.setId(400);
    petOwner.setName("Donald Smith");
    petOwner.setPhoneNumber("555-1212");

    VetVisit vetVisit = new VetVisit();
    vetVisit.setId(500);
    vetVisit.setNotes("Pet was shedding a lot.");
    vetVisit.setSymptoms("Pet in good health.");
    vetVisit.setPet(petClone);

    petClone.setPetOwner(petOwner);
    petClone.getVetVisits().addElement(vetVisit); 
uow.commit();

This executes the proper SQL:

INSERT INTO PETOWNER (ID, NAME, PHN_NBR) VALUES (400, 'Donald Smith', 
'555-1212')
UPDATE PET SET PET_OWN_ID = 400 WHERE (ID = 100)
INSERT INTO VETVISIT (ID, NOTES, SYMPTOMS, PET_ID) VALUES (500, 'Pet was 
shedding a lot.', 'Pet in good health.', 100)

When associating new objects to existing objects, the Unit of Work treats the new object as if it was a clone. That is, after the commit:

petOwner != session.readObject(petOwner)

For a more detailed discussion of this fact, see "Using registerNewObject").

Therefore, after the Unit of Work commit, the variables vetVisit and petOwner no longer point to their respective cache objects: they point at working copy clones.

If you need the cache object after the Unit of Work commit, you must query for it or create the association with a reference to the cache object (as described in "Associating with Reference to the Cache Object").

Associating with Reference to the Cache Object

Example 7-7 shows how to associate a new target with an existing source with reference to the cache object.

Example 7-7 Associating with Reference to the Cache Object

UnitOfWork uow = session.acquireUnitOfWork();
    Pet petClone = (Pet)uow.readObject(Pet.class);

    PetOwner petOwner = new PetOwner();
    PetOwner petOwnerClone = (PetOwner)uow.registerObject(petOwner);
    petOwnerClone.setId(400);
    petOwnerClone.setName("Donald Smith");
    petOwnerClone.setPhoneNumber("555-1212");

    VetVisit vetVisit = new VetVisit();
    VetVisit vetVisitClone = (VetVisit)uow.registerObject(vetVisit);
    vetVisitClone.setId(500);
    vetVisitClone.setNotes("Pet was shedding a lot.");
    vetVisitClone.setSymptoms("Pet in good health.");
    vetVisitClone.setPet(petClone);

    petClone.setPetOwner(petOwnerClone);
    petClone.getVetVisits().addElement(vetVisitClone); 
uow.commit();

Now, after the Unit of Work commit:

petOwner == session.readObject(petOwner)

This means that we have a handle to the cache copy after the commit, rather than a clone.

Example 7-8 shows another way to add a new object in a Unit of Work when a bidirectional relationship exists.

Example 7-8 Resolving Issues When Adding New Objects

// Get an employee read from the parent session of the Unit of Work.
Employee manager = (Employee)session.readObject(Employee.class);

// Acquire a Unit of Work.
UnitOfWork uow = session.acquireUnitOfWork();

// Register the manager to get its clone
Employee managerClone = (Employee)uow.registerObject(manager);

// Create a new employee
Employee newEmployee = new Employee();
newEmployee.setFirstName("Spike");
newEmployee.setLastName("Robertson");

/* INCORRECT: Do not associate the new employee with the original manager. This 
will cause a QueryException when OracleAS TopLink detects this error during  
commit. */
//newEmployee.setManager(manager);

/* CORRECT: Associate the new object with the clone. Note that in this example, 
the setManager method is maintaining the bidirectional managedEmployees 
relationship and adding the new employee to its managedEmployees. At commit 
time, the Unit of Work will detect that this is a new object and will take the 
appropriate action. */
newEmployee.setManager(managerClone);

/* INCORRECT: Do not register the newEmployee: this will create two copies and 
cause a QueryException when OracleAS TopLink detects this error during commit.*/
//uow.registerObject(newEmployee);

/* CORRECT: 
In the above setManager call, if the managerClone's managedEmployees was not
maintained by the setManager method, then you should call registerObject before 
the new employee is related to the manager. If in doubt, you could use the  
registerNewObject method to ensure that the newEmployee is registered in the 
Unit of Work. The registerNewObject method registers the object, but does not 
make a clone. */
uow.registerNewObject(newEmployee);

// Commit the Unit of Work
uow.commit();

Associations: New Source to Existing Target Object

This section describes how to associate a new source object with an existing target object with 1-many and 1-1 relationships.

OracleAS TopLink follows all relationships of all registered objects (deeply) in a Unit of Work to calculate what is new and what has changed. This is known as persistence by reachablity. In "Associations: New Target to Existing Source Object", we saw that when you associate a new target with an existing source, you can choose to register the object or not. If you do not register the new object, it is still reachable from the source object (which is a clone, hence it is registered). However, when you need to associate a new source object with an existing target, you must register the new object. If you do not register the new object, then it is not reachable in the Unit of Work and OracleAS TopLink will not write it to the database.

For example, imagine we want to create a new Pet and associate it with an existing PetOwner. The code shown in Example 7-9 will accomplish this:

Example 7-9 Associating a New Source to an Existing Target Object

UnitOfWork uow = session.acquireUnitOfWork();
    PetOwner existingPetOwnerClone =
        (PetOwner)uow.readObject(PetOwner.class);

    Pet newPet = new Pet();
    Pet newPetClone = (Pet)uow.registerObject(newPet);
    newPetClone.setId(900);
    newPetClone.setType("Lizzard");
    newPetClone.setName("Larry");
    newPetClone.setPetOwner(existingPetOwnerClone);
uow.commit();

This generates the proper SQL:

INSERT INTO PET (ID, NAME, TYPE, PET_OWN_ID) VALUES (900, 'Larry', 'Lizzard', 
400)

In this situation, you should register the new object and work with the working copy of the new object. If you associate the new object with the PetOwner clone without registering, it will not be written to the database. If you are in a situation where you want to associate the PetOwner clone with the new Pet object, use the advanced API registerNewObject as described in "Using registerNewObject".

If you fail to register the clone and accidentally associate the cache version of the existing object with the new object, then OracleAS TopLink will generate an error which states that you have associated the cache version of an object ("from a parent session") with a clone from this Unit of Work. You must work with working copies in units of work.

Associations: Existing Source to Existing Target Object

This section explains how to associate an existing source object with an existing target object with 1-many and 1-1 relationships.

As shown in Example 7-10, associating existing objects with each other in a Unit of Work is as simple as associating objects in Java. Just remember to only work with working copies of the objects.

Example 7-10 Associating an Existing Source to Existing Target Object

// Associate all VetVisits in the database to a Pet from the database
UnitOfWork uow = session.acquireUnitOfWork();
    Pet existingPetClone = (Pet)uow.readObject(Pet.class);
    Vector allVetVisitClones;
    allVetVisitClones = (Vector)uow.readAllObjects(VetVisit.class);
    Enumeration enum = allVetVisitClones.elements();
    while(enum.hasMoreElements()) {
        VetVisit vetVisitClone =(VetVisit)enum.nextElement();
        existingPetClone.getVetVisits().addElement(vetVisitClone);
        vetVisitClone.setPet(existingPetClone);
    };

uow.commit();

The most common error when associating existing objects is failing to work with the working copies. If you accidentally associate a cache version of an object with a working copy you will get an error at commit time indicating that you associated an object from a parent session (the cache version) with a clone from this Unit of Work.

Example 7-11 shows another example of associating an existing source to an existing target object.

Example 7-11 Associating Existing Objects

// Get an employee read from the parent session of the Unit of Work.
Employee employee = (Employee)session.readObject(Employee.class)

// Acquire a Unit of Work.
UnitOfWork uow = session.acquireUnitOfWork();
Project project = (Project) uow.readObject(Project.class);

/* When associating an existing object (read from the session) with a clone, we 
must make sure we register the existing object and assign its clone into a Unit 
of Work. */

/* INCORRECT: Cannot associate an existing object with a Unit of Work clone. A 
QueryException will be thrown. */
//project.setTeamLeader(employee);

/* CORRECT: Instead register the existing object then associate the clone. */
Employee employeeClone = (Employee)uow.registerObject(employee);
project.setTeamLeader(employeeClone);
uow.commit();

Deleting Objects

To delete objects in a Unit of Work, use the deleteObject or deleteAllObjects method. When you delete an object that is not already registered in the Unit of Work, the Unit of Work registers the object automatically.

When you delete an object, OracleAS TopLink deletes the object's privately owned parts, because those parts cannot exist without the owning object. At commit time, the Unit of Work generates SQL to delete the objects, taking database constraints into account.

When you delete an object, you must take your object model into account. You may need to set references to the deleted object to null (for an example, see "Using privateOwnedRelationship").

This section explains how to delete objects with a Unit of Work, including:

Using privateOwnedRelationship

Relational databases do not have garbage collection like a Java Virtual Machine (JVM) does. To delete an object in Java you just de-reference the object. To delete a row in a relational database you must explicitly delete it. Rather than tediously manage when to delete data in the relational database, use the mapping attribute privateOwnedRelationship to make OracleAS TopLink manage the garbage collection in the relational database for you.

As shown in Example 7-12, when you create a mapping using Java, use its privateOwnedRelationship method to tell OracleAS TopLink that the referenced object is privately owned: that is, the referenced object cannot exist without the parent object.

Example 7-12 Specifying a Mapping as Privately Owned

OneToOneMapping petOwnerMapping = new OneToOneMapping();
petOwnerMapping.setAttributeName("petOwner");
petOwnerMapping.setReferenceClass(com.top.uowprimer.model.PetOwner.class);
petOwnerMapping.privateOwnedRelationship();
petOwnerMapping.addForeignKeyFieldName("PET.PET_OWN_ID", "PETOWNER.ID");
descriptor.addMapping(petOwnerMapping);

When you create a mapping using the Mapping Workbench, you can select the Private Owned check box under the General tab.

When you tell OracleAS TopLink that a relationship is private owned, you are telling it two things:

Do not configure private owned relationships to objects that might be shared. An object should not be the target in more than one relationship if it is the target in a private owned relationship.

The exception to this rule is the case when you have a many-to-many relationship in which a relation object is mapped to a relation table and is referenced through a one-to-many relationship by both the source and target. In this case, if the one-to-many mapping is configured as privately owned, then when you delete the source, all the association objects will be deleted.

Consider the example shown in Example 7-13.

Example 7-13 Private Owned Relationships

// If the Pet-PetOwner relationship is privateOwned
// then the PetOwner will be deleted at uow.commit()
// otherwise, just the foreign key from PET to PETOWNER will
// be set to null.  The same is true for VetVisit.
UnitOfWork uow = session.acquireUnitOfWork();
    Pet petClone = (Pet)uow.readObject(Pet.class);
    petClone.setPetOwner(null);
    VetVisit vvClone =
        (VetVisit)petClone.getVetVisits().firstElement();
    vvClone.setPet(null);
    petClone.getVetVisits().removeElement(vvClone);
uow.commit();

If the relationships from Pet to PetOwner and from Pet to VetVisit are not private owned, this code produces the following SQL:

UPDATE PET SET PET_OWN_ID = NULL WHERE (ID = 150)
UPDATE VETVISIT SET PET_ID = NULL WHERE (ID = 350)

If the relationships are private owned, this code produces the following SQL:

UPDATE PET SET PET_OWN_ID = NULL WHERE (ID = 150)
UPDATE VETVISIT SET PET_ID = NULL WHERE (ID = 350)
DELETE FROM VETVISIT WHERE (ID = 350)
DELETE FROM PETOWNER WHERE (ID = 250)

Explicitly Deleting from the Database

If there are cases where you have objects that will not be garbage collected through private owned relationships (especially root objects in your object model) then you can explicitly tell OracleAS TopLink to delete the row representing the object using the deleteObject API. For example:

Example 7-14 Explicitly Deleting

UnitOfWork uow = session.acquireUnitOfWork();
    pet petClone = (Pet)uow.readObject(Pet.class);
    uow.deleteObject(petClone);
uow.commit();

The above code generates the following SQL:

DELETE FROM PET WHERE (ID = 100)

Understanding the Order in which Objects are Deleted

The Unit of Work does not track changes or the order of operations. It is intended to isolate you from having to modify your objects in the order the database requires.

By default, at commit time, the Unit of Work orders all inserts and updates using the constraints defined by your schema. After all inserts and updates are done, the Unit of Work will issue the necessary delete operations.

Constraints are inferred from one-to-one and one-to-many mappings. If you have no such mappings, you can add additional constraint knowledge to OracleAS TopLink as described in "Controlling the Order of Deletes".

Advanced Unit of Work

This section explores more advanced Unit of Work API calls and techniques most commonly used later in the development cycle, including:

For more information about integrating the Unit of Work with J2EE and external transaction controllers, see "J2EE Integration".

For more information about the available methods for the UnitOfWork, see the Oracle Application Server TopLink API Reference.

Troubleshooting a Unit of Work

This section examines common Unit of Work problems and debugging techniques, including:

Avoiding the Use of Post-commit Clones

A common Unit of Work error is holding on to clones after commit. Typically the clones are stored in a static variable and the developer incorrectly thinks 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 thinks is the cache copy is not updated (because a Unit of Work only updates the cache copy, not old clones).

Consider the error in Example 7-15. In this example we 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 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 7-15 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's 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

Determining Whether or not an Object is the Cache Object

In "Modifying an Object", we 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:

session.readObject(pet);

Is equivalent to the following:

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.

Given this, we have a very quick and simple method for accessing the cache copy of an object as shown in Example 7-16.

Example 7-16 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

Dumping the Contents of a Unit of Work

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 objects known 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 7-17.

Example 7-17 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

Handling Exceptions

OracleAS TopLink exceptions are instances of RuntimeException, which means that methods that throw them do not have to be placed in a try-catch statement.

However, the Unit of Work commit method is one that should be called within a try-catch statement to deal with problems that may arise.

Example 7-18 shows one way to handle Unit of Work exceptions:

Example 7-18 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

Catching exceptions at commit time is mandatory if you are using optimistic locking 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 Policy".

Creating and Registering an Object in One Step

Example 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 pattern to create your objects (and specified this in the builder), the newInstance method will use the appropriate factory.

Example 7-19 Creating and Registering an Object in One Step

UnitOfWork uow = session.acquireUnitOfWork();
    Pet petClone = (Pet)uow.newInstance(Pet.class);
    petClone.setId(100);
    petClone.setName("Fluffy");
    petClone.setType("Cat");
uow.commit();

Using registerNewObject

This example examines how to use the registerNewObject method, including:

Registering a New Object with registerNewObject

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:

Example 7-20 shows how to register a new object with the registerNewObject method:

Example 7-20 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 committed, then you should use the first approach described in "Associations: New Source to Existing Target Object". In that example, the variable newPet is the cache version after the Unit of Work is committed.

Associating New Objects with One Another

At commit time, OracleAS TopLink can determine if an object is new or not. In "Associations: New Target to Existing Source Object", we saw that if a new object is reachable from a clone, you do not need to register it. OracleAS TopLink effectively does a registerNewObject to all new objects it can reach from registered objects.

When working with new objects, remember the following rules:

Example 7-21 shows how to associate new objects with the registerNewObject method:

Example 7-21 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, the variables newPet, newPetOwner, and newVetVisit should not be used since they were technically copies from the Unit of Work.

If we needed a handle to the cache version of these business objects we could query for them or we could have done the Unit of Work as shown in Example 7-22.

Example 7-22 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(); 

Using registerAllObjects

The registerAllObjects method takes a Collection of objects as an argument and returns a Collection of clones. This allows you to register many objects at once as shown in Example 7-23:

Example 7-23 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();

Using Registration and Existence Checking

When OracleAS TopLink writes an object to the database, OracleAS TopLink runs an existence check to determine whether to perform an insert or an update. You can specify the default existence checking policy for a project as a whole or on a per-descriptor basis. By default, OracleAS 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 OracleAS TopLink to register an object.

This section explains how to use one of the following existence checking policies to accelerate object registration:

Check Database

If your existence checking policy is check database then OracleAS TopLink will check the database for existence for all objects 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. OracleAS TopLink will not check the database for existence on objects that you have registered with these methods. It will automatically do an insert if registerNewObject is called or an update if registerExistingObject is called.

Assume Existence

If your existence checking policy is assume existence then all objects registered in a Unit of Work are assumed to exist and OracleAS TopLink will always do an update to the database on all registered objects, even new objects that you registered with registerObject. However, if you use the registerNewObject method on the new object, OracleAS TopLink knows to do an insert in the database even though the existence checking policy says assume existence.

Assume Non-existence

If your existence checking policy is assume non-existence then all objects registered in a Unit of Work are assumed to be new and OracleAS TopLink will always do an insert to the database, even on objects read from the database. However, if you use the registerExistingObject method on existing objects, OracleAS TopLink knows to do an update to the database.

Working with Aggregates

Aggregate mapped objects should never be registered in an OracleAS TopLink Unit of Work (in fact, you will get an exception if you try). 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.

The bottom line of working with aggregates is you should always use an aggregate within the context of its owner:

Unregistering Working Clones

The Unit of Work unregisterObject method allows you to 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 also not recommended).

Declaring Read-Only Classes

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.

Setting Read-Only Classes for a Single Unit of Work

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 A will change: you know that no class B's will be changed. Before registering an instance of B, use:

myUnitofWork.addReadOnlyClass(B.class);

Then you can proceed with your transaction: registering 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 copy clones with the working copy 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 your C are not be persisted because C extends B and B was identified as read-only.

To identify multiple classes as read only, add them to a Vector and use:

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".

Setting Read-Only Classes for All Units 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.

Read-Only Descriptors

When you declare a class as read-only, the read-only flag extends to its descriptors. You can flag a descriptor as read-only at development time, using either Java code or the OracleAS TopLink Mapping 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 flag a descriptor as read-only in the OracleAS TopLink Mapping Workbench, select the Read Only check box for a specific descriptor.

For more information, see "Working with Descriptors," in the Oracle Application Server TopLink Mapping Workbench User's Guide.

Using Conforming Queries and Descriptors

This section explains how to include new, changed, or deleted objects in queries within a Unit of Work prior to commit, including:

Using Conforming Queries

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.

Assume that a single Pet of type Cat already exists on the database. Examine the code shown in Example 7-24.

Example 7-24 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]

Bear in mind that conforming will impact performance. Before you use conforming, make sure that it is actually necessary. For example, consider the alternative described in "Conforming Query Alternatives".

Conforming Query Alternatives

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:

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, this simple and more efficient solution is a very practical alternative.

Using Conforming Descriptors

OracleAS TopLink's support for conforming queries in the Unit of Work can be specified in the descriptors.

You can flag 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 the OracleAS TopLink Mapping Workbench.

You can flag descriptors to always conform in the Unit of Work by calling the method on the descriptor as follows:

descriptor.setShouldAlwaysConformResultsInUnitOfWork(true);

To set this flag in the OracleAS TopLink Mapping Workbench, select the Conform Results in Unit Of Work check box for a descriptor.

Merging Changes in Working Copy Clones

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 register the original object in a Unit of Work and then use the Unit of Work methods listed in Table 7-2 to merge serialized object changes into the working copy clone. Each method takes the serialized object as an argument.

Table 7-2 Unit of Work Merge Methods
Method Purpose Used When

mergeClone

Merges the serialized object and all its privately owned parts (excluding 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.

mergeCloneWithReferences

Merges the serialized object and all its privately owned parts (including references from it to independent objects) into the working copy clone.

The client edits the object and the targets of its relationships and has not marked any attributes as transient.

shallowMergeClone

Merges only serialized object changes to attributes mapped with direct mappings into the working copy clone.

The client only edits the object's direct attributes or has marked all of the object's relationships as transient.

deepMergeClone

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.

Use with caution: if two different copies of an object are in the same traversal, it will merge one set of changes over the other. You should not have any transient attributes in any of your related objects.

Note that if your three-tier client is sufficiently complex, consider using the TopLink remote session (see "Remote Session"). It automatically handles merging and allows you to use a Unit of Work on the client.

You can merge clones with both existing and new objects. Because they 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 7-25 shows one way to update the original object with the changes contained in the corresponding serialized object (rmiClone) received from a client.

Example 7-25 Merging a Serialized Object

update(Object original, Object rmiClone)
{
  original = uow.registerObject(original);
  uow.mergeCloneWithRefereneces(rmiClone);
  uow.commit();
}

Resuming a Unit of Work After Commit

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, OracleAS TopLink offers API that enables you to continue working with a Unit of Work and its clones:

Example 7-26 shows how to use the commitAndResume method:

Example 7-26 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 the SQL:

UPDATE PETOWNER SET NAME = 'Mrs. Newowner' WHERE (ID = 400)

And then the commit call produces the SQL:

UPDATE PETOWNER SET PHN_NBR = 'KL5-7721' WHERE (ID = 400)

Reverting a Unit of Work

Under certain circumstances, you may want to abandon some or all changes to clones in a Unit of Work, but not abandon the Unit of Work itself. The following options exist for reverting all or part of the Unit of Work:

Using a Nested or Parallel Unit of Work

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.

Parallel Unit of Work

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.

Nested Unit of Work

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.

OracleAS 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 copies 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 7-27 shows how to use nested Units of Work:

Example 7-27 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();

Using a Unit of Work with Custom SQL

You can add custom SQL to a Unit of Work at any time by calling the Unit of Work executeNonSelectingCall method as shown in Example 7-28.

Example 7-28 Using the executeNonSelectingCall Method

uow.executeNonSelectingCall(new SQLCall(mySqlString));

Validating a Unit of Work

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 OracleAS 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. OracleAS TopLink offers API to toggle validation, as follows:

Validating the Unit of Work Before Commit

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 of traversed objects. This may help you more easily find the problem. You can call this method at any time on a Unit of Work.

Controlling the Order of Deletes

"Deleting Objects" explained that OracleAS TopLink always properly orders the SQL based on the mappings and foreign keys in your object model and schema. You can control the order of deletes by:

Using the Unit of Work setShouldPerformDeletesFirst Method

It is possible to tell the Unit of Work to issue deletes before inserts and updates by calling the Unit of Work setShouldPerformDeletesFirst method.

By default, OracleAS TopLink does inserts and updates first to ensure that referential integrity is maintained.

If you are replacing an object with unique constraints by deleting it and inserting a replacement, if the insert occurs before the delete, you may raise a constraint violation. In this case, you may need to call setShouldPerformDeletesFirst so that the delete is performed before the insert.

Using the Descriptor addConstraintDependencies Method

The constraints used by OracleAS 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 OracleAS 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, non-private relationship with C. You want to delete A (and in doing so the included B's) but before deleting the B's, for some of them (not all) you want to delete the associated object C.

There are two possible solutions:

Using deleteAllObjects without addConstraintDependencies

In the first option, we do not use privately-owned on the one-to-many (A to B) relationship. When deleting an A, we make sure to delete all of it's B's as well as any C instances. For example:

uow.deleteObject(existingA);
uow.deleteAllObjects(existingA.getBs());
// delete one of the C's 
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)

Using deleteAllObjects with addConstraintDependencies

In the second option, we keep the one-to-many (A to B) relationship privately owned and add a constraint dependency from A to C. For example:

session.getDescriptor(A.class).addConstraintDependencies(C.class);

Now the delete code would be:

uow.deleteObject(existingA);
uow.deleteAllObjects(existingA.getBs());
// delete one of the C's 
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 is deleted before A and C. The main difference is that the second option will generate fewer SQL statements as it knows that it is deleting the entire set of B's related from A.

Improving Unit of Work Performance

For best performance when using a Unit of Work, consider the following tips:

J2EE Integration

OracleAS TopLink J2EE integration provides support for external datasources and external transaction controllers. Together, these features provide support for JTA. This enables you to incorporate external container support into your application, and to use JTA transactions.

This section describes:

External Connection Pooling

For most non-J2EE applications OracleAS TopLink provides an internal connection or pool of connections. However, most J2EE applications use external connection pooling offered by the J2EE Container JTA DataSource. For J2EE applications OracleAS TopLink integrates with the J2EE Container connection pooling.

When to Use External Connection Pools

External connection pools enable your OracleAS TopLink application to:

Configure OracleAS TopLink to use the built-in JTA integration support to take advantage of these benefits. Without JTA, external connection pools generally offer benefits only if transactions in an OracleAS TopLink application are independent of each other and any other transactions in the system. In that case, the complexities of an OracleAS TopLink connection or connection pool are unnecessary.

Configuring an External Connection Pool in sessions.xml

To configure the use of an external connection pool in the sessions.xml file:

  1. Configure the DataSource on the server.

  2. Add the following elements to the login tag in the sessions.xml file to specify a DataSource and the use of an external connection pool:

    <data-source>jdbc/MyApplicationDS</data-source>
    <uses-external-connection-pool>true</uses-external-connection-pool>
    

Configuring an External Connection Pool in Java

To configure the use of an external connection pool in Java:

  1. Configure the DataSource on the server.

  2. Configure the Login to specify a DataSource and the use of an external connection pool:

    login.setConnector(
        new JNDIConnector(new InitialContext(), "jdbc/MyApplicationDS")
    );
    login.setUsesExternalConnectionPooling(true);
    

External Transaction Controllers

A transaction controller is an OracleAS TopLink class that synchronizes the session cache with the data on the database. The transaction controller manages messages and callbacks from the J2EE transaction. On commit, the transaction controller executes the Unit of Work SQL on the database, and merges changed objects into the OracleAS TopLink session cache. Because JTA transaction controllers require a JTA-enabled DataSource, configure an external transaction controller and enable OracleAS TopLink external connection pool support.

OracleAS TopLink provides transaction controllers for container-specific support, as well as a generic controllers that can be used for other specification-conforming servers.

Table 7-3 lists the custom external transaction controllers OracleAS TopLink provides.

Table 7-3 OracleAS TopLink Custom External Transaction Controllers
Application Server or J2EE Container OracleAS TopLink External Transaction Controller

Oracle Application Server Containers for J2EE

oracle.toplink.jts.oracle9i.Oracle9iJTSExternalTransactionController

IBM WebSphere 3.5

oracle.toplink.jts.was.WebSphereJTSExternalTransactionController

IBM WebSphere 4.0

oracle.toplink.jts.was.JTSExternalTransactionController_4_0

IBM WebSphere 5.0

oracle.toplink.jts.was.JTSExternalTransactionController_5_0

BEA WebLogic

oracle.toplink.jts.wls.WebLogicJTSExternalTransactionControlle r

Other JTA Container

oracle.toplink.jts.JTSExternalTransactionController

Configuring an External Transaction Controller in sessions.xml

To configure the use of an external transaction controller in the sessions.xml file:

  1. Configure a JTA-enabled DataSource on the server.

    For more information, see the J2EE container documentation.

  2. Add the following elements to the login tag in the sessions.xml file to specify a DataSource, the use of an external transaction controller, and the use of an external connection pool:

    <data-source>jdbc/MyApplicationDS</data-source>
    <uses-external-transaction-controller>
        true
    </uses-external-transaction-controller>
    <uses-external-connection-pool>true</uses-external-connection-pool>
    
    
  3. Specify an external transaction controller class in the sessions.xml file.

    For example:

    <external-transaction-controller-class>
    
    oracle.toplink.jts.oracle9i.Oracle9iJTSExternalTransactionController
    
    </external-transaction-controller-class>
    

Configuring an External Transaction Controller in Java

To configure the use of an external transaction controller in Java:

  1. Configure a JTA-enabled DataSource on the server.

    For more information, see the J2EE container documentation.

  2. Configure the Login to specify a DataSource, the use of an external transaction controller, and the use of an external connection pool:

    login.setConnector(
        new JNDIConnector(new InitialContext(), "jdbc/MyApplicationDS")
    );
    login.setUsesExternalTransactionController(true);
    login.setUsesExternalConnectionPooling(true);
    
    
  3. Configure the session to use a particular instance of ExternalTransactionController:

    serverSession.setExternalTransactionController(
        new Oracle9iJTSExternalTransactionController()
    );
    

Acquiring a Unit of Work in a JTA Environment

You use a Unit of Work to write to a database even in a JTA environment. 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 7-29.


Note:

Although there are other ways to write to a database through a JTA external controller, using getActiveUnitOfWork method is the safest approach to database updates under JTA.


The getActiveUnitOfWork method searches for an existing external transaction:

If a non-null Unit of Work is returned, use it exactly as you would in a non-JTA environment: the only exception is that you do not call the commit method (see "Using a Unit of Work When an External Transaction Exists").

If a null Unit of Work is returned, start an external transaction either explicitly through the UserTransaction interface, or by acquiring a new Unit of Work using the acquireUnitOfWork method on the client session (see "Using a Unit of Work When No External Transaction Exists").

Example 7-29 Using a Unit of Work in a JTA Transaction

boolean shouldCommit = false;
// Read in any pet.
Pet pet = (Pet)clientSession.readObject(Pet.class);
UnitOfWork uow = clientSession.getActiveUnitOfWork();
    if (uow == null) {
        uow = clientSession.acquireUnitOfWork(); // Start external transaction
        shouldCommit = true;
    }
    Pet petClone = (Pet) uow.registerObject(pet);
    petClone.setName("Furry");
    if (shouldCommit) {
        uow.commit(); // Ask external transaction controller to commit
    }

Using a Unit of Work When an External Transaction Exists

When getActiveUnitOfWork returns a non-null Unit of Work, 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 JTA 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, OracleAS TopLink receives sychronization callbacks at key points during the commit.

The Unit of Work sends the required SQL to the database when it receives the beforeCompletion call back.

The Unit of Work uses the boolean argument received from the afterCompletion call back to determine if the commit was successful (true) or not (false).

If the commit was successful, the Unit of Work merges changes to the session cache. If the commit was unsuccessful, the Unit of Work discards the changes.

Figure 7-4 Unit of Work When an External Transaction Exists

Text description of uow_ex.gif follows.

Text description of the illustration uow_ex.gif

Using a Unit of Work When No External Transaction Exists

When getActiveUnitOfWork 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 OracleAS TopLink and the database updates and cache merge occurs based upon those callbacks.

Figure 7-5 Unit of Work When No External Transaction Exists

Text description of uow_noex.gif follows.

Text description of the illustration uow_noex.gif


Go to previous page Go to next page
Oracle
Copyright © 2000, 2003 Oracle Corporation.

All Rights Reserved.
Go To Documentation Library
Home
Go To Product List
Solution Area
Go To Table Of Contents
Contents
Go To Index
Index