Chapter 6. Persistent Objects and ObjectContext

Table of Contents

ObjectContext
Persistent Object and its Lifecycle
ObjectContext Persistence API
Cayenne Helper Class
ObjectContext Nesting
Generic Persistent Objects
Transactions

ObjectContext

ObjectContext is an interface that users normally work with to access the database. It provides the API to execute database operations and to manage persistent objects. A context is obtained from the ServerRuntime:

ObjectContext context = runtime.newContext();

The call above creates a new instance of ObjectContext that can access the database via this runtime. ObjectContext is a single "work area" in Cayenne, storing persistent objects. ObjectContext guarantees that for each database row with a unique ID it will contain at most one instance of an object, thus ensuring object graph consistency between multiple selects (a feature called "uniquing"). At the same time different ObjectContexts will have independent copies of objects for each unique database row. This allows users to isolate object changes from one another by using separate ObjectContexts.

These properties directly affect the strategies for scoping and sharing (or not sharing) ObjectContexts. Contexts that are only used to fetch objects from the database and whose objects are never modified by the application can be shared between mutliple users (and multiple threads). Contexts that store modified objects should be accessed only by a single user (e.g. a web application user might reuse a context instance between multiple web requests in the same HttpSession, thus carrying uncommitted changes to objects from request to request, until he decides to commit or rollback them). Even for a single user it might make sense to use mutliple ObjectContexts (e.g. request-scoped contexts to allow concurrent requests from the browser that change and commit objects independently).

ObjectContext is serializable and does not permanently hold to any of the application resources. So it does not have to be closed. If the context is not used anymore, it should simply be allowed to go out of scope and get garbage collected, just like any other Java object.

Persistent Object and its Lifecycle

Cayenne can persist Java objects that implement org.apache.cayenne.Persistent interface. Generally persistent classes are generated from the model as described above, so users do not have to worry about superclass and property implementation details.

Persistent interface provides access to 3 persistence-related properties - objectId, persistenceState and objectContext. All 3 are initialized by Cayenne runtime framework. Application code should not attempt to change them them. However it is allowed to read them, which provides valuable runtime information. E.g. ObjectId can be used for quick equality check of 2 objects, knowing persistence state would allow highlighting changed objects, etc.

Each persistent object belongs to a single ObjectContext, and can be in one of the following persistence states (as defined in org.apache.cayenne.PersistenceState) :

Table 6.1. Persistence States

TRANSIENT The object is not registered with an ObjectContext and will not be persisted.
NEW The object is freshly registered in an ObjectContext, but has not been saved to the database yet and there is no matching database row.
COMMITTED The object is registered in an ObjectContext, there is a row in the database corresponding to this object, and the object state corresponds to the last known state of the matching database row.
MODIFIED The object is registered in an ObjectContext, there is a row in the database corresponding to this object, but the object in-memory state has diverged from the last known state of the matching database row.
HOLLOW The object is registered in an ObjectContext, there is a row in the database corresponding to this object, but the object state is unknown. Whenever an application tries to access a property of such object, Cayenne attempts reading its values from the database and "inflate" the object, turning it to COMMITED.
DELETED The object is registered in an ObjectContext and has been marked for deletion in-memory. The corresponding row in the database will get deleted upon ObjectContext commit, and the object state will be turned into TRANSIENT.


ObjectContext Persistence API

One of the first things users usually want to do with an ObjectContext is to select some objects from a database. This is done by calling "performQuery" method:

SelectQuery query = new SelectQuery(Artist.class);
List<Artist> artists = context.performQuery(query);

We'll discuss queries in some detail in the following chapters. The example above is self-explanatory - we create a SelectQuery that matches all Artist objects present in the database, and then call "performQuery", getting a list of Artist objects.

Some queries can be quite complex, returning multiple result sets or even updating the database. For such queries ObjectContext provides "performGenericQuery"method. While not nearly as commonly-used as "performQuery", it is nevertheless important in some situations. E.g.:

Collection<Query> queries = ... // multiple queries that need to be run together
QueryChain query = new QueryChain(queries);

QueryResponse response = context.performGenericQuery(query);

An application might modify selected objects. E.g.:

Artist selectedArtist = artists.get(0);
selectedArtist.setName("Dali");

The first time the object property is changed, the object's state is automatically set to "MODIFIED" by Cayenne. Cayenne tracks all in-memory changes until a user calls "commitChanges":

context.commitChanges();

At this point all in-memory changes are analyzed and a minimal set of SQL statements is issued in a single transaction to synchronize the database with the in-memory state. In our example "commitChanges" commits just one object, but generally it can be any number of objects.

If instead of commit, we wanted to reset all changed objects to the previously committed state, we'd call rollbackChanges instead:

context.rollbackChanges();

"newObject" method call creates a persistent object and sets its state to "NEW":

Artist newArtist = context.newObject(Artist.class);
newArtist.setName("Picasso");

It will only exist in memory until "commitChanges" is issued. On commit Cayenne might generate a new primary key (unless a user set it explicitly, or a PK was inferred from a relationship) and issue an INSERT SQL statement to permanently store the object.

deleteObjects method takes one or more Persistent objects and marks them as "DELETED":

context.deleteObjects(artist1);
context.deleteObjects(artist2, artist3, artist4);

Additionally "deleteObjects" processes all delete rules modeled for the affected objects. This may result in implicitly deleting or modifying extra related objects. Same as insert and update, delete operations are sent to the database only when "commitChanges" is called. Similarly "rollbackChanges" will undo the effect of "newObject" and "deleteObjects".

localObject returns a copy of a given persistent object that is "local" to a given ObjectContext:

Since an application often works with more than one context, "localObject" is a rather common operation. E.g. to improve performance a user might utilize a single shared context to select and cache data, and then occasionally transfer some selected objects to another context to modify and commit them:

ObjectContext editingContext = runtime.newContext();
Artist localArtist = editingContext.localObject(artist);

Often an appliction needs to inspect mapping metadata. This information is stored in the EntityResolver object, accessible via the ObjectContext:

EntityResolver resolver = objectContext.getEntityResolver();

Here we discussed the most commonly used subset of the ObjectContext API. There are other useful methods, e.g. those allowing to inspect registered objects state en bulk, etc. Check the latest JavaDocs for details.

Cayenne Helper Class

There is a useful helper class called "Cayenne" (fully-qualified name "org.apache.cayenne.Cayenne") that builds on ObjectContext API to provide a number of very common operations. E.g. get a primary key (most entities do not model PK as an object property) :

long pk = Cayenne.longPKForObject(artist);

It also provides the reverse operation - finding an object given a known PK:

Artist artist = Cayenne.objectForPK(context, Artist.class, 34579);

If a query is expected to return 0 or 1 object, Cayenne helper class can be used to find this object. It throws an exception if more than one object matched the query:

Artist artist = (Artist) Cayenne.objectForQuery(context, new SelectQuery(Artist.class));

Feel free to explore Cayenne class API for other useful methods.

ObjectContext Nesting

In all the examples shown so far an ObjectContext would directly connect to a database to select data or synchronize its state (either via commit or rollback). However another context can be used in all these scenarios instead of a database. This concept is called ObjectContext "nesting". Nesting is a parent/child relationship between two contexts, where child is a nested context and selects or commits its objects via a parent.

Nesting is useful to create isolated object editing areas (child contexts) that need to all be committed to an intermediate in-memory store (parent context), or rolled back without affecting changes already recorded in the parent. Think cascading GUI dialogs, or parallel AJAX requests coming to the same session.

In theory Cayenne supports any number of nesting levels, however applications should generally stay with one or two, as deep hierarchies will most certainly degrade the performance of the deeply nested child contexts. This is due to the fact that each context in a nesting chain has to update its own objects during most operations.

Cayenne ROP is an extreme case of nesting when a child context is located in a separate JVM and communicates with its parent via a web service. ROP is discussed in details in the following chapters. Here we concentrate on the same-VM nesting.

To create a nested context, use an instance of ServerRuntime, passing it the desired parent:

ObjectContext parent = runtime.newContext();
ObjectContext nested = runtime.newContext((DataChannel) parent);

From here a nested context operates just like a regular context (you can perform queries, create and delete objects, etc.). The only difference is that commit and rollback operations can either be limited to synchronization with the parent, or cascade all the way to the database:

// merges nested context changes into the parent context
nested.commitChangesToParent();

// regular 'commitChanges' cascades commit through the chain 
// of parent contexts all the way to the database
nested.commitChanges();
// unrolls all local changes, getting context in a state identical to parent
nested.rollbackChangesLocally();

// regular 'rollbackChanges' cascades rollback through the chain of contexts 
// all the way to the topmost parent
nested.rollbackChanges();

Generic Persistent Objects

As described in the CayenneModeler chapter, Cayenne supports mapping of completely generic classes to specific entities. Although for conveniece most applications should stick with entity-specific class mappings, the generic feature offers some interesting possibilities, such as creating mappings completely on the fly in a running application, etc.

Generic objects are first class citizens in Cayenne, and all common persistent operations apply to them as well. There are some pecularities however, described below.

When creating a new generic object, either cast your ObjectContext to DataContext (that provides "newObject(String)" API), or provide your object with an explicit ObjectId:

DataObject generic = ((DataContext) context).newObject("GenericEntity");
DataObject generic = new CayenneDataObject();
generic.setObjectId(new ObjectId("GenericEntity"));
context.registerNewObject(generic);

SelectQuery for generic object should be created passing entity name String in constructor, instead of a Java class:

SelectQuery query = new SelectQuery("GenericEntity");

Use DataObject API to access and modify properties of a generic object:

String name = (String) generic.readProperty("name");
generic.writeProperty("name", "New Name");

This is how an application can obtain entity name of a generic object:

String entityName = generic.getObjectId().getEntityName();

Transactions

Considering how much attention is given to managing transactions in most other ORMs, transactions have been conspicuously absent from the ObjectContext discussion till now. The reason is that transactions are seamless in Cayenne in all but a few special cases. ObjectContext is an in-memory container of objects that is disconnected from the database, except when it needs to run an operation. So it does not care about any surrounding transaction scope. Sure enough all database operations are transactional, so when an application does a commit, all SQL execution is wrapped in a database transaction. But this is done behind the scenes and is rarely a concern to the application code.

Two cases where transactions need to be taken into consideration are container-managed and application-managed transactions.

If you are using an EJB container (or some other JTA environment), you'll likely need to switch Cayenne runtime into "external transactions mode". This is done by setting DI configuration property defined in Constants.SERVER_EXTERNAL_TX_PROPERTY (see Appendix A). If this property is set to "true", Cayenne assumes that JDBC Connections obtained by runtime whenever that might happen are all coming from a transactional DataSource managed by the container. In this case Cayenne does not attempt to commit or rollback the connections, leaving it up to the container to do that when appropriate.

In the second scenario, an application might need to define its own transaction scope that spans more than one Cayenne operation. E.g. two sequential commits that need to be rolled back together in case of failure. This can be done via ServerRuntime.performInTransaction method:

Integer result = runtime.performInTransaction(() -> {
    // commit one or more contexts
    context1.commitChanges();
    context2.commitChanges();
    ....
    // after changing some objects in context1, commit again
    context1.commitChanges();
    ....

    // return an arbitrary result or null if we don't care about the result
    return 5;
});

When inside the transaction, current thread Transaction object can be accessed via a static method. E.g. here is an example that initializes transaction JDBC connection with a custom connection object :

Transaction tx = BaseTransaction.getThreadTransaction();
tx.addConnection("mydatanode", myConnection);