The One-PM-per-Request Design


The one-pm-per-request design obtains a persistence manager at the beginning of each request and closes it after returning the response. This design appears to gain little from caching because the persistence manager's cache is open for business for only one request. In this case, appearances can be misleading. A sophisticated JDO implementation will implement a level-two cache below the caching specified by JDO. The level-two cache underlies all of the persistence managers that the implementation's factories produce. As a result, even a newly opened persistence manager can benefit from the cached state accumulated from earlier reads of the datastore. For the sophisticated JDO implementation, very large caching benefits can be obtained because of the level-two cache. Implementations may also pool persistence managers and datastore connections to gain a performance benefit. As a result, the one-pm-per-request design cannot be ruled out simply on the basis of an assumed lack of caching.

In general, to gain the benefit of a level-two cache when updating, the application must use an optimistic transaction. For read-only access to persistent objects, turning on the NontransactionalRead property works equally well. On the other hand, the use of a datastore transaction implies that the state of the object in memory is synchronized with the state in the datastore. Normally, the synchronization is guaranteed by loading the state from the datastore. As a result, using a datastore transaction is likely to void the performance benefit expected from the level-two cache.

In the one-pm-per-request design, the controller servlet closes the persistence manager after the response is generated. As a result, the application must either stop using the persistent data objects or make them unmanaged before closing the persistence manager. If the application drops the persistent objects, it will be up to the JDO implementation whether to reuse the application data objects or allow them to be garbage collected. On the other hand, dropping the objects is not an option when the controller stores these objects in the session as part of the conversational state. In this case, the controller should make the objects unmanaged. The rental Web application saves conversational state for its clients by storing unmanaged application data objects in the HttpSession object associated with the request.

Note

To make a persistent application data object unmanaged, the application calls the makeTransient method in the PersistenceManager interface. See Chapter 3 for details.

After the object becomes unmanaged, its usefulness is limited to one or a few more requests, after which it will be replaced with a newer unmanaged object. In short, closing a persistence manager is likely to turn the persistent objects into garbage either immediately or within a short time. Since a persistence manager is opened and closed on each request in the one-pm-per-request design, every request generates new garbage. As mentioned earlier, current versions of the JVM are optimized for garbage collection, and for that reason, the cost of garbage generation is often an acceptable price for the design's benefits.

Since the persistence manager is closed after every request in the one-pmper-request design, JDO cannot provide a transactional context that spans requests. As a result, if transactional consistency is required across requests, the application designer must roll his own versioning scheme that can support an application-defined optimistic transaction that spans requests. The SupportsVersion interface, found in the com.ysoft.jdo.book.rental.service package, and its implementation in the Rental class used by the rental Web application shows one way to provide a user-defined optimistic concurrency check.

In order to handle updates, to verify the version, or to accommodate the semantics of HTML, the one-pm-per-request design requires that transient application data objects or information about them be stored in the conversational state. In order to know what objects the client is updating, the identity strings of the persistent objects served must be saved. Likewise, in order to verify the version, the version number obtainable from the SupportsVersion interface must be saved. Finally, as mentioned earlier, the semantics of HTML may require, as it does in the rental Web application, that the served state be stored in order to process subsequent requests.

A strength of the one-pm-per-request design is simplicity; however, this simplicity can be lost when persistent objects are stored in the session. If application data objects are stored in the session, then prior to closing the persistence manager, the controller must make them unmanaged. In doing so, the designer must decide how far to follow the chain of persistent references. Care must be taken to avoid accidentally storing a persistent object in the session whose persistence manager has been closed.

The one-pm-per-request design is very scalable because there are a variety of ways that conversational state can be stored. The designer has the choice of using HTML, the session object, or persistent storage. Each choice supports the clustering of servlet containers.

Reviewing the Code of the One-PM-per-Request Example

The Maine Lighthouse Rental Web application is found in the subdirectories under the com/ysoft/jdo/book/rental/servlet directory. The opr subdirectory contains the application's controller ReservationServlet. The util subdirectory contains the ReservationModel and SessionLock classes. The service subdirectory contains the ReservationService.

The ReservationService configures the properties of the PersistenceManagerFactory in the same way for all requests. It turns on the NontransactionalRead, Optimistic, and RetainValues properties. It turns off the RestoreValues, NontransactionalWrite, Multithreaded, and IgnoreCache properties.

NontransactionalRead is turned on because the controller starts a transaction only when the client wants to make or cancel a reservation. The controller does not start a transaction to read persistent state.

The controller uses a JDO transaction when processing an update request. The transactional consistency that spans the view request and the update request is provided by the controller's use of the application-defined SupportsVersion interface. Optimistic JDO transactions are used instead of datastore JDO transactions to make the best use of a level-two cache if it is present.

RetainValues is turned on to prevent unnecessary eviction of persistent state after the transaction's commit. The eviction is not needed since the persistence manager itself will be closed as soon as the response is generated.

Walking Through the Logic That Generates a Response

When a request for dynamic content is received by the servlet container, the container calls the servlet's doGet or doPost method. In the ReservationServlet, the doGet and doPost methods call a common respond method in which all of the response logic is handled.

Obtain a ReservationService and the SessionLock

The respond method begins by constructing a new ReservationService, as shown in the next lines of code:

          // get the data service for this request          service = new ReservationService(); 

In the ReservationService constructor, the code acquires a persistence manager factory from the getPersistenceManagerFactory method in JDOHelper. After getting the factory, the constructor opens a persistence manager in the usual way. Thereafter, the reservation service uses its persistence manager whenever it needs to access JDO.

After obtaining the service, the controller next obtains the session lock using the following lines of code:

 lock = SessionLock.getInstance(request); int action = lock.lock(NO_ACTION, 20); // lock expires in 20 seconds 

The SessionLock class ensures that requests from the same client are queued, but it allows requests from different clients to proceed concurrently. The session lock avoids multithreaded access to a session object or to any objects stored within the session object after the lock is obtained.

The session lock also identifies the concurrent requests from the same client as redundant requests. In these cases, instead of performing the action again and repopulating the model with persistent objects, the control servlet simply regenerates the page using the existing model stored in the session. This allows the controller logic to make any request idempotent during the time that it takes to perform the action and generate the response. Session locks are implemented as leases that expire after a short period of time. The expiration prevents an unresponsive thread from locking up a session indefinitely.

Find or Create the ReservationModel

After getting the service and the lock, the controller next calls the getInstance method of the ModelHandle class to find or create a handle. From the handle, the controller then obtains the model, as shown in the next line of code:

 ReservationModel model = ModelHandle.getInstance(request).getModel(); 

The handle wraps the model and provides some convenience and debugging features. (The ModelHandle class is found in the ReservationServlet source file.) The handle's getInstance method either creates a new handle in the client's session or finds the existing handle in the client's session. The getInstance method also stores the model in the HttpServletRequest object where the JSP that generates the view finds it.

Perform the Action and Populate the Model

After obtaining the model, the respond method determines the action to perform. If the lock method of SessionLock returned NO_ACTION, then the method has determined that there are no other current requests from the client. In this case, the controller calls its getAction method, which examines request parameters and the model to determine the action to be performed, as shown in the next lines of code:

 if (action == NO_ACTION)    {    action = getAction(request, model);    lock.setAction(action);    } 

After determining the action, the controller informs the session lock of the action. In this way, the session lock can identify any subsequent concurrent requests from the same client as redundant. As a result, the controller handles the redundant requests in an idempotent way.

At this point, the respond method performs the action indicated by the selected action code. In the example, the controller has a private method for each action. For a more complex application, it would be better to have a separate class for each action.

Generate the Page View

After the controller uses the reservation service to populate the ReservationModel with persistent objects, it forwards the request to the JSP display page. The JSP then uses the model to generate the HTML. Listing 10-3 shows the JSP code that generates the rows of available rentals. The JSP code iterates over the rows contained in the model. For each row, the JSP begins by putting the starting date of the week in the first column. It then iterates over the nodes of the model, generating in subsequent columns either a blank or a check box with a price. The XML tags shown in Listing 10-3 that begin with the "c:" are the core tags from the JSTL.

Listing 10-3: Excerpt from maine.jsp Where the Rental Table Rows Are Generated

start example
 <c:forEach var="row" items="${model.modelRows}" >    <tr>       <td align="center">          <c:out value="${row.weekString}"/>       </td>       <c:forEach var="node" items="${row.nodes}" >          <td><div align="left">             <c:choose>                <c:when test="${node.modifiable}" >                   reserve                   <input name=                         "<%=ReservationServletConstants.RESERVATION_PARAM%>"                   type="CHECKBOX"                   value="<c:out value="${node.id}"/>"                   <c:if test="${not node.available}">CHECKED</c:if>                   <c:if test="${not model.customerKnown}">DISABLED</c:if>  >                   <c:out value="${node.priceString}" />                </c:when>                <c:otherwise>&nbsp;</c:otherwise>             </c:choose>          </div></td>       </c:forEach>    </tr> </c:forEach> 
end example

When used by the JSP, the ReservationModel usually contains live, that is to say managed, application data objects. In the example, these live application data objects are used indirectly when the JSP accesses the model. The persistence manager contained within the ReservationService is still open and providing transparent persistence to the live objects. As a result, if the model or the JSP is modified to navigate the persistent objects in a different way, the change does not affect the code in the controller or service.

Save Conversational State in the Servlet Session

The rental Web application uses the HttpSession object to store the conversational state. As mentioned earlier, this is not the only choice, nor necessarily the best choice. The rental Web example stores the conversational state in the session because this design is common.

Even though the code favors the session for storing conversational state, some conversational state is stored in the HTML. For example, the check box controls in the JSP maine.jsp hold table cell (node) identifiers in the value attribute. During an update request, these cell identifiers are part of the name-value pairs passed in the request to the controller. In turn, the controller passes them to the model to obtain the corresponding Rental objects. The example's model remembers only the state that it acquires from responding to the last request from the client. Any state prior to the last request can be lost. At the same time, browsers normally cache any number of pages and allow the user to jump around in the request logic. As a result, it is possible for the request to be based on a model state that no longer exists.

There are at least two approaches to handle the possible mismatches that can arise between the request and the model's state. The first approach forces the browser to reload every page from the server. In this approach, various HTML pragmas are placed in the generated pages that prevent the browser from caching the page. Forcing a reload on every page will tend to make the Web site unresponsive in the eyes of the typical user. The second approach is to put a step number in each HTML response and keep the current step number in the model. The browser can therefore reuse cached pages as desired. If the step sent in the next request is not the step expected, then at the controller's discretion, the action performed may be different from the action requested. The rental Web example takes this second approach. The controller increments a count each time a page is served to the client. The count's value is placed in the HTML for its return in the next request.

Before closing the service, the controller finds all the application data objects in the model and makes them unmanaged. This step occurs immediately after generating the response, as the following code shows:

 // forward to display page forward(forwardTo, request, response); // make the persistent objects in the model transient Collection dataObjects = model.getDataObjects(); service.makeTransientAll(dataObjects); 

Generate the Page View for a Redundant Request

The lock method in the SessionLock class breaks all requests into two groups: the original request and redundant requests. When no request owns the lock, the next request that needs it is an original request. Any subsequent request from the same client that seeks the lock while it is owned is a redundant request. As a result of the original request, the controller performs some action and repopulates the model with persistent objects. It then asks the view to generate the HTML page. After the view generates the page, the persistent objects in the model are made unmanaged.

When the lock method identifies a redundant request, the controller skips the actions it performs for original requests and goes straight to the view. The view for the redundant request executes the same logic to generate the page, but unlike the original request, it uses unmanaged data objects in the model. Since the view is simply retracing the logic that generated the page for the original request, the unmanaged objects have all of the persistent state that the view for the redundant request requires.

Clean Up Before Returning from the respond Method

Before returning, the respond method closes the service and releases the session lock in its finally block, as shown in the following code:

 finally    {    if (service != null)       service.close();    if (lock != null)       lock.unlock();    } 

In its close method, the service closes the persistence manager that it was using. When the respond method returns, the request has been handled and a response has been generated.

At this point, the model remains stored in the session object whose life cycle is controlled by the servlet container. If the container serializes the session, the model will be serialized with it. As a result, the ModelHandle class, the ReservationModel class, and all of the classes of objects that can be reached by serializable fields must implement the Serializable interface. During development, it is worthwhile to test session serialization. A debugging and testing servlet can be written whose sole purpose is to serialize the session and then report on any exceptions, as well as the size of the object graph.

Making and Canceling Rental Reservations

When the client makes and/or cancels reservations, the controller determines from the model and the request which reservations need to be toggled. It then gets the corresponding unmanaged Rental objects from the model. As shown in the next line of code, the controller then sends the affected Rental objects to the service to have the corresponding persistent objects changed:

 service.flipReservations(modifiedRentals, model.getCustomer()); 

Within the ReservationService, the flipReservations method, shown in Listing 10-4, begins by starting a transaction. It then gets the identity string from the unmanaged customer. Both the Customer and Rental class used in the Web example implement the SupportsIdentity interface. This interface, which is examined in the next section, defines one method, getIdentityString. If the unmanaged Customer object has never been persistent, then there will not be a corresponding persistent Customer object. Otherwise, the persistent Customer object is fetched using the identity string.

Listing 10-4: The flipReservations Method of the ReservationService

start example
 public void flipReservations(       Collection unmanagedRentals,       Customer unmanagedCustomer) throws       ExtendedOptimisticException,       OptimisticReservationException,       ReservationException    {    try       {    // start the transaction    tx.begin();    // get the persistent Customer    String oidString = unmanagedCustomer.getIdentityString();    // find the persistent customer record    Customer pCustomer = null;    // if the unmanaged Customer object doesn't have an identity string,    // then we have a new customer    if (oidString == null)       pCustomer = unmanagedCustomer;    // otherwise, we find the corresponding persistent Customer object    else       {       try          {          pCustomer = (Customer) pm.getObjectById(pm.newObjectIdInstance(                Customer.class, oidString), true);          }       catch (JDODataStoreException e)          {          throw new ExtendedOptimisticException(                "The system has deleted this customer's record", e);          }       }    // get the persistent Rental objects and do the flip    Iterator iter = unmanagedRentals.iterator();    while (iter.hasNext())       {       Rental uRental = (Rental) iter.next();       oidString = uRental.getIdentityString();       if (oidString == null)          {          throw new IllegalStateException(                "unmanaged Rental object without identity string: " + uRental);          }       Rental pRental = null;       try          {          pRental = (Rental) pm.getObjectById(pm.newObjectIdInstance(                Rental.class, oidString), true);          }       catch (JDODataStoreException e)          {          throw new ExtendedOptimisticException(                "The system has deleted the rental record: " +                uRental, e);          }       // if the persistent versions are not compatible, then throw an error       if (!pRental.isSameVersion(uRental))          {          throw new ExtendedOptimisticException(                "The rental record has been altered by another user, " +                "please review your change and submit it again");          }                if (pRental.getCustomer() == null)          {          // make reservation          pRental.makeReservation(pCustomer);          }       else          {          // cancel reservation          pRental.cancelReservation(pCustomer);          }       }    commitTransaction();    } finally    {    if (tx.isActive())       tx.rollback();    } } 
end example

After determining the value of pCustomer, the flipReservations method iterates the collection of unmanaged Rental objects. Because each of these must have been persistent, each has an identity string that is used to get the corresponding persistent object.

For each Rental object, the version of the unmanaged object is compared to the version of the managed object to verify that they are the same. This occurs in the isSameVersion method of the Rental object. This method is one of the methods specified by the SupportsVersion interface, whose implementation is examined in the next section. If both objects are not based on the same version, then the code throws an ExtendedOptmisticException. In fact, the error shown in Figure 10-5, which occurred because Mary attempted to reserve a lighthouse that Jim had already reserved, arises at this point in the code.

JDO's optimistic transactional semantics guarantee, at transaction commit, that the version of the Rental object that the code modifies in the update request is the latest version in the datastore. On the other hand, the extended optimistic semantics defined in the SupportsVersion interface guarantees that the version changed in the update request is the version served in the view request.

When the view and update versions are the same, the method makes the change to the reservation status of the Rental object in the usual way. The method finishes by committing the transaction. In its finally block, the method ensures that the transaction is not left active in the event that an exception is raised.

The SupportsIdentityString Interface

In the rental Web application, it is essential that application data objects know their persistent identity. Regardless of where the conversational state is stored, the essential piece of information is always the persistent identity of the objects that the client modifies. By using the persistent identity, the controller servlet finds the persistent objects that the client wishes to modify.

The SupportsIdentityString interface requires the getIdentityString method as its only method. By implementing the SupportsIdentityString interface, the application data class ensures that its objects capture their identity strings. In Listing 10-4, the code calls the getIdentityString method on unmanaged Customer and Rental objects to obtain the identity strings that these objects captured when they were persistent. The code in Listing 10-4 then uses the identity strings to find the corresponding persistent objects.

Listing 10-5 shows the code in the Rental class that implements the SupportsIdentityString interface. The code in the InstanceCallbacks methods helps by providing for the automatic capture of the identity string. See Listing 7-2 and the discussion in the section "Capturing the Identity String" in Chapter 7 for more details.

Listing 10-5: Implementation of the SupportsIdentityString Interface in the Rental Class

start example
 public class Rental implements SupportsIdentityString, InstanceCallbacks, ...    {    // other fields are omitted for brevity    private String OIDString;     // unmanaged field    // constructors and additional methods omitted for brevity    // required by the SupportsIdentityString interface    public String getIdentityString()       {       if (OIDString == null)          {          Object oid = JDOHelper.getObjectId(this);          if (oid != null)             OIDString = oid.toString();          }       return OIDString;       }    // Using InstanceCallbacks to support the identity string    public void jdoPreStore()       {       getIdentityString();       }    public void jdoPreDelete()       {       OIDString = null;       }    public void jdoPostLoad()       {       getIdentityString();       }    public void jdoPreClear()       {       getIdentityString();       }    } 
end example

In Listing 10-5, the identity string is captured automatically in the callback methods of the InstanceCallbacks interface. The primary load-bearing callback is the jdoPostLoad method. Alternatively, the application can itself ensure that the identity string is captured by calling the getIdentityString method while the object is persistent. For that reason, implementing the InstanceCallbacks interface is optional when implementing the SupportsIdentityString interface.

The SupportsVersion Interface

The SupportsVersion interface extends both the SupportsIdentityString interface and the InstanceCallbacks interface. Both interfaces are essential for the implementation of the SupportsVersion interface. The SupportsVersion interface requires two methods, isSameVersion and getVersion.

Listing 10-6 shows the implementation of the SupportsVersion interface for the Rental class. For clarity, Listing 10-6 does not show the code in Listing 10-5 that implements the SupportsIdentityString interface.

Listing 10-6: Implementation of the SupportsVersion Interface in the Rental Class

start example
 public class Rental implements SupportsVersion, ...    {    // other managed and unmanaged fields omitted for brevity    private int          userVersion;    // constructors and other methods omitted for brevity    public void jdoPreStore()       {       userVersion++;       // other work in this callback       }    // other InstanceCallbacks methods are omitted for brevity.    public boolean isSameVersion(Object pc)       {       boolean retv = false;       // if two objects have the same persistent identity, then       // compare their versions       if (this.equals(pc))          retv = (userVersion == ((SupportsVersion) pc).getVersion());       return retv;       }    public int getVersion()       {       return userVersion;       }    } 
end example

When implementing the SupportsVersion interface, the jdoPreStore callback is used to increment the userVersion. The jdoPreStore callback is ideal for this purpose, since it is called for modified as well as new persistent objects when the transaction is committing.

The isSameVersion method returns true when the object passed to it is a Rental object with the same identity string and version number as this. The version number refers to the version of the persistent state that was loaded. It does not change when the object becomes dirty or unmanaged. It changes only when the persistent state in the datastore is modified.

Overriding the Equals and hashCode Methods

When an application data class captures its identity string, its equals method can use the identity string to determine object equivalence. Although Java allows each class to define the semantics of equivalence, in the case of application data classes, there are two reasons why it makes good sense to use persistent identity as the touchstone of object equivalence. First, JDO uses persistent identity to define the uniqueness requirement and to define object equivalence within persistent collection fields. Second, when the application must deal with unmanaged application data objects that are generated from persistent objects and that hold changes to apply to persistent objects, questions about persistent identity become fundamental questions that the equals method is ideally suited to answer. For example, in Listing 10-6, the isSameVersion method uses the equals method of the Rental object to determine whether two distinct objects in memory refer to the same persistent state before it determines whether the two objects started with the same version of that state.

Listing 10-7 shows the implementation of the equals and hashCode methods for the Rental object. Both methods use the getIdentityString method shown in Listing 10-5 to obtain identity strings.

Listing 10-7: The equals and hashCode Methods for the Rental Application Data Class

start example
 // two application data objects are equal when and only // when they are members of the same class and // their identity strings are equal public final boolean equals(Object other)    {    if (this == other)       return true;    // return false when other is not    // a Rental object or is null,    // or this has no identity string    String my_id = getIdentityString();    if (!(other instanceof Rental)  || my_id == null)       return false;    String other_id = ((Rental) other).getIdentityString();    return my_id.equals(other_id);    } public final int hashCode()    {    String s = getIdentityString();    return (s != null) ? s.hashCode() : super.hashCode();    } 
end example

Although the application has the freedom to define an equals method, Java imposes a contract on the implementation of the equals method. This contract also applies to the hashCode method. To satisfy the contract for an inheritance tree of application data classes, the equals and hashCode methods should be defined in the least-derived application data class where the persistent identity is fully defined. After all, these methods depend on knowing what the persistent identity is. In the case of datastore identity, the appropriate class is the least-derived application data class in the hierarchy. In the case of application identity, each class in the hierarchy that the JDO metadata associates with a concrete application identity class would override the equals and hashCode methods. Because the equals and hashCode methods are fixed for any application data classes deriving from the application data class that defines them, the code in Listing 10-7 applies the final modifier to the equals and hashCode methods.

Note

To learn about the contract that applies to the equals and hashCode methods, see the Javadoc for the Object class in the Java SDK. For a detailed discussion of application identity in class hierarchies, see Chapter 5.

Using the Rental Swing App with the Rental Web App

If you use both the rental Swing application and the rental Web application at the same time (this is not possible with the reference implementation because the datastore is not multiuser), note that there is some incompatibility between the two versions. The version of the Rental class used for the Swing application does not implement the SupportsVersion interface. It does have the userVersion field for data schema compatibility, but it does nothing with it. The rental Swing application relies on JDO transactional semantics, and therefore the implementation of the SupportsVersion interface has been omitted for simplicity. Likewise, the versions used in the Swing application do not implement the SupportsIdentity interface. As a result, the rental Swing application can use the version of the application data classes that the rental Web application uses, but the opposite is not true. Therefore, to use the two architectures together, build the targets in the following order: clean-out, rental-servlet-opr, rental-gui.




Using and Understanding Java Data Objects
Using and Understanding Java Data Objects
ISBN: 1590590430
EAN: 2147483647
Year: 2005
Pages: 156
Authors: David Ezzio

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net