The VLib example demonstrates a Tapestry Web application using JDO as its model.
In this sense, the example is a proof-of-concept. The integration of Tapestry and JDO can be done!
The example has other important goals as well. Since working example code is often used as a
template for real-world applications, the VLib example strives to avoid bad code. There is no
reason for you to learn bad lessons from an example. It also attempts to show good architectural
design. In other words, not only does the example work, but it works in a good way. No doubt there
are bugs in this code, and the bugs may go deeper than some code not working. If you spot bad code
or bad design, speak up, and help improve this example.
'Design patterns' is too strong a term for the designs decisions discussed here, since not all
of these design decisions have been applied in multiple applications, and the classic exposition
of a design pattern has not been adopted. Nevertheless, this example contains several design
insights that are worth understanding. If you understand them, you can choose whether to apply
them to the applications that you write.
Use one persistence manager per HTTP request
The first thing to understand when building a Web application using JDO, is to select and stick
with the one-pm-per-request design pattern. This pattern was first described in the book
Using and Understanding Java Data Objects
. It is a simple concept, and it works without
subtle errors.
The reason to obtain one persistence manager at the start of each request is the need to release
the persistence manager at the end of the request. Persistence managers can consume limited
resources which must be shared with other request handling threads. As a rule, they do not
serialize, and therefore cannot be stored in the session.
The one-PM-per-request design pattern imposes real constraints on your application's design.
We will encounter them at every turn throughout the rest of this discussion.
The alternatives, however, can cause worse problems in the form of extra coding, increased
complexity, the potential for subtle errors, and a failure to deliver the expected benefits.
For further discussion of the alternatives, see Chapter 10, 'Using JDO in a Web Application' in
Using and Understanding Java Data Objects.
In the one-pm-per-request design pattern, there is a static factory that produces persistence
manager objects. In this example, the
PMLocator
class (contained within the
VLibDataService
source file) provides the factory by delegating to JDO's
PersistenceManagerFactory
class. Each time a request is serviced, a
VLibDataService
object is constructed, and during its initialization, it uses the locator to get a persistence manager. The important point is that the persistence manager factory is constructed only once for each Web container, but the persistence managers are obtained on each HTTP request. JDO is constructed to put most of the time into the construction of a persistence manager factory. The JDO implementation may use connection pooling, object pooling, and in some cases, data pooling, to reduce the computational burden of constantly getting and releasing persistence managers and the persistent objects they manage.
In the VLib example, the
VirtualLibraryEngine
class acts as the factory to produce the
current request's
VLibDataService
. The
VirtualLibraryEngine
closes the
VLibDataService
(which thereby releases the persistence manager) in its
cleanupAfterRequest
method, which the Tapestry framework calls after every request.
The VLib examples shows a clean and simple implementation of the one-pm-per-request pattern.
Encapsulate JDO transactions within the life-cycle of the service
This is really just a corollary of the pattern above. Since JDO 1.0 transactions do not
outlive the persistence manager, if you close the persistence manager you must also end
the transaction. In JDO 2.0, extended optimistic transactions will be supported. For
further discussion of extended optimistic transactions, see the section
'Add extended optimistic transactional semantics' towards the end of this tutorial.
Divide the model into service and data classes
One the one hand, data objects are plain-old-Java-objects (POJOs), and their classes can be simple JavaBeans. Data classes are identified in the JDO metadata. JDO fetches and stores the state of the managed objects of data classes without the need for your code to implement the magic. On the other hand, your code will need to invoke explicit JDO services to control transactions, to define and run queries, to iterate query results, to create new persistent objects, todelete existing persistent objects, and to create value objects.
Therefore, think of your model as a set of data classes wrapped by a service. What goes into and
comes out of the service are value objects (more on this point in the next section). The
service itself uses managed data objects. The following diagram illustrates the point.
In the VLib example, the data classes are
Book
,
Person
, and
Publisher
,
and the application's service is the
VLibDataService
.
Use value objects in the controller and presentation code
JDO's management of persistent objects has its good points and its bad points. On the good side,
a data object looks like a POJO. They are
simple to design, and easy to code and understand. JDO handles all of the code required to
shuffle state between the object and the database. On the bad side, JDO's management of the
object can be a hindrance in some cases.
For example, JDO's management is a hindrance when changes to persistent objects need to be
recorded in memory without having an active JDO transaction. JDO will not allow a managed
object to be changed unless a transaction is active. (Surprisingly, JDO's
nontransactional-write feature is not the solution it appears to be).
JDO's management is also a hindrance when the persistence manager is to be released, but
the data object is still needed for later use. The persistent objects are no longer valid
once the persistent manager has been released (in other words, its
close
method
has been called). At the same time, it is natural in most Web applications to store some
data objects in the session between requests. The Web container may serialize these objects
between requests, and the persistence manager is not around to manage them.
So it would appear that Web applications need data objects when they are not available.
How is this problem solved? The answer is to create value objects which contain all of
the information required for non-managed operations and for later updating of the
corresponding managed object. Some people believe that separate classes should define
the value objects, but in the classic words of Abe White, the architect of Kodo,
'data objects can be their own value objects.' By using the data classes to define both
managed data objects and unmanaged value objects, both the code and its maintenance are simplified.
The entire process of creating a value object, modifying it, and later updating a managed data
object with it is called 'detachment-attachment.' By creating value objects, the controller code
can store
and modify unmanaged data objects and sends them to the service to update managed data objects.
JDO 2.0 will provide the detachment-attachment mechanism. In JDO 1.0, application code must do it.
For the simple cases, it is not hard.
In the VLib example, the
DataObjectService
and the
VLibDataService
collaborate to
insure that only value objects enter and leave the public methods of the
VLibDataService
.
For a simple example like VLib, the case can be made for using value objects only where needed
and using managed data objects everywhere else. This would reduce the amount of garbage
generated by servicing a request. Most real world Web applications are not simple. When
confronting the inevitable complexity of real-world requirements, grand organizing principles
that keep the architecture, design, coding, and debugging simple are generally worth more than
a minor improvement in performance.
Encapsulate mutations of relationships in the service
Data classes like all Java classes define state and behavior. All of the state is defined in
the member fields of the Java class. Some of this state is inherent to the data class. State
stored in primitive fields and state stored in some system classes, such as
Integer
,
Date
and
String
, is the inherent state. It is inherent because the state applies
directly to the object. Other state defines the relationships between data objects. A
Person
may relate to a
Book
which the person has borrowed. An
Invoice
may contain
LineItems
.
The attachment code to handled modified relationships is more complicated than the attachment code
to handle modified inherent attributes. Since in JDO 1.0 the application's code must perform the
attachment, it is convenient to
delegate to the data service all operations that modify the relationships between data objects.
The support for attachment in JDO 2.0 will give us a reason to reevaluate this argument.
When writing a Web application, there is another reason to encapsulate mutations of relationships
in the service. Often the relationships between
data objects are managed by the user through the user interface. In HTML, the objects being
referred to by various controls can be uniquely identified by the identity string of the
persistent object. When the user borrows a book, the natural thing to expect from the HTML
control is the identity string of the new owner. Since the controller code does not know which
person goes with the identity string, it is natural to delegate the task to the data service.
This reason will remain even with the advent of JDO 2.0.
In the
VLibDataService
, there are several methods that modify the relationships between
data objects, such as
borrowBook
,
returnBook
, and
transferBooks
. Each of
these service methods look up persistent
Book
objects and persistent
Person
objects and call the book's
setHolder
and
setOwner
methods.
Define an interface for your value object's operations.
It is often the case that a value object should not change some state that a managed object can and
must change. In the discussion above, the
setHolder
and
setOwner
methods of the
Book
class are not operations for value objects. Combine this with the question of what
is the best value object implementation, and you have the perfect prescription for an interface.
By making all of your controller and presentation code use an interface to the value objects,
you create the means to control what operations the controller and presentation code perform and
what implementation will perform it.
In the case of VLib example, the
Book
,
Person
, and
Publisher
data classes
each have one of the value interfaces,
IBook
,
IPerson
, and
IPublisher
.
Encapsulate transactions within the service when possible
JDO does not, nor should it, encapsulate transactions. Where transactions are bounded and which
code controls the boundaries is a design decision that varies from application to application.
In the case of Web applications, since the life-time of the service and the persistence manager
it uses is congruent with the processing of the HTTP request
(always assuming that you have adopted the one-pm-per-request design pattern), transactions that
JDO 1.0 can enforce must be short lived.
Since the transaction is already confined to the duration of the HTTP request processing, it makes
sense to go further and confine it to the duration of a data service method call. While not
necessary, this confinement simplifies the controller's code. If you find as I do that the most
precious development resource is developer time, it pays to adopt the KISS solution as the first
solution.
In the case of the VLib application, all JDO transactions live within the confines of a
VLibDataService
method call.
Add extended optimistic transactional semantics
Concurrent access to the same data is a real problem in database Web applications. In the
VLib example, two people can be viewing the same book, and independently decide to borrow it.
Without concurrency control, the last borrower wins. Since the view and the request to borrow
it are two separate HTTP requests, JDO transactions do not apply
(always assuming that you are using the one-pm-per-request design pattern.).
What is required can be called 'extended optimistic transactions' (which I suppose we could
shorten to EOTS or 'e-oughts'). With EOTS, the code executing user A's request to borrow the
book detects that the book has been changed since the book has been viewed. It is possible for
the application to implement EOTS using JDO 1.0. There is an example of an EOTS implementation
in the book
Using and Understanding Java Data Objects.
The VLib example does not implement extended optimistic transactional semantics for two reasons.
One, this was and remains a problem in the original Virtual Library example. Two, JDO 2.0 will
provide EOTS for detached objects and it will also provide the tools to write a trivial
implementation of EOTS even when the detached objects are discarded.
So this defect remains in the VLib example and is expected to be corrected with the evolution of
the example as JDO 2.0 becomes available.