Differences between revisions 4 and 5
Revision 4 as of 2005-03-08 22:37:31
Size: 14923
Editor: HenriDupre
Comment:
Revision 5 as of 2009-09-20 23:20:37
Size: 14923
Editor: localhost
Comment: converted to 1.6 markup
No differences found!

Since this subject is coming often on the mailing list, I created this page for summarizing solutions.

Two approaches are possible for using hibernate with Tapestry: with Spring or write your own engine services to deal with sessions. The Spring approach offers an (almost) transparent session management and declarative transactions.

Hibernate 3

Hibernate 3 offers several major improvements over the 2.1. A preliminary support for Hibernate 3 and Spring is available at http://opensource.atlassian.com/projects/spring/browse/SPR-300

Using Spring in Tapestry

With Spring, the way to go is to use the "OpenSessionInViewFilter" which opens and closes hibernate sessions for you in a transparent way. These lines are required in the web.xml to enable the session management:

web.xml excerpt

        <filter>
                <filter-name>hibernateFilter</filter-name>
                <filter-class>org.springframework.orm.hibernate3.support.OpenSessionInViewFilter</filter-class>
        </filter>

        <filter-mapping>
                <filter-name>hibernateFilter</filter-name>
                <!-- put here your own path for your Tapestry url-pattern -->
                <url-pattern>/exec</url-pattern>
        </filter-mapping>

        <listener>
                <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
        </listener>

For the applicationContext.xml, my file is based on https://betterpetshop.dev.java.net/ example:

applicationContext.xml excerpt

        <!-- ========================= GENERAL DEFINITIONS ========================= -->

        <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
                <property name="location"><value>classpath:jdbc.properties</value></property>
        </bean>

        <!-- Message source for this context, loaded from localized "messages_xx" files -->
        <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
                <property name="basename"><value>messages</value></property>
        </bean>


        <!-- ========================= RESOURCE DEFINITIONS ========================= -->

        <!-- Local DataSource that works in any environment -->
        <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
                <property name="driverClassName"><value>${jdbc.driverClassName}</value></property>
                <property name="url"><value>${jdbc.url}</value></property>
                <property name="username"><value>${jdbc.username}</value></property>
                <property name="password"><value>${jdbc.password}</value></property>
        </bean>

        <!-- Hibernate SessionFactory -->
        <bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
                <property name="dataSource"><ref local="dataSource"/></property>
                <property name="mappingResources">
                        <list>
                                <!-- Write here your list of Hibernate files -->
                                <value>hibernate/catalogue/Article.hbm.xml</value>

                        </list>
                </property>
                <property name="hibernateProperties">
                        <props>
                                <prop key="hibernate.dialect">${hibernate.dialect}</prop>
                                <prop key="hibernate.show_sql">true</prop>
                                <prop key="hibernate.c3p0.minPoolSize">${hibernate.c3p0.minPoolSize}</prop>
                                <prop key="hibernate.c3p0.maxPoolSize">${hibernate.c3p0.maxPoolSize}</prop>
                                <prop key="hibernate.c3p0.timeout">${hibernate.c3p0.timeout}</prop>
                                <prop key="hibernate.c3p0.max_statement">${hibernate.c3p0.max_statement}</prop>
                                <prop key="hibernate.generate_statistics">true</prop>
                                <prop key="hibernate.cache.use_query_cache">true</prop>
                        </props>
                </property>
        </bean>

        <!-- Transaction manager for a single Hibernate SessionFactory (alternative to JTA) -->
        <bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
                <property name="sessionFactory"><ref local="sessionFactory"/></property>
        </bean>



        <!-- ========================= BUSINESS OBJECT DEFINITIONS ========================= -->

        <!-- Petclinic primary business object: Hibernate implementation -->
        <bean id="WebServiceTarget" class="actualis.web.spring.WebManager"
        singleton="false">
                <property name="catalogue"><ref local="catalogueDAO"/></property>
                <property name="commande"><ref local="commandeDAO"/></property>
                <property name="clients"><ref local="clientsDAO"/></property>
        </bean>

        <bean id="poolTargetSource"
                class="org.springframework.aop.target.CommonsPoolTargetSource">
                <property name="targetBeanName"><value>WebServiceTarget</value></property>
                <property name="maxSize"><value>5</value></property>
        </bean>

        <bean id="businessObject"
                class="org.springframework.aop.framework.ProxyFactoryBean">
                <property name="targetSource"><ref local="poolTargetSource"/></property>
        </bean>

        <!-- Transactional proxy for the Petclinic primary business object -->
        <bean id="WebService" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
                <property name="target"><ref local="WebServiceTarget"/></property>
                <property name="transactionManager"><ref local="transactionManager"/></property>
                        <property name="transactionAttributes">
                        <props>
                                <!-- Avoid PROPAGATION_REQUIRED !! It could kill your performances by generating a new transaction on each request !! -->

                                <prop key="get*">PROPAGATION_SUPPORTS,readOnly</prop>
                                <prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>
                                <prop key="load*">PROPAGATION_REQUIRED,readOnly</prop>
                                <prop key="store*">PROPAGATION_REQUIRED</prop>
                        </props>
                </property>
        </bean>

        <bean id="catalogueDAO" class="actualis.web.dao.CatalogueDAO">
                <property name="sessionFactory"><ref local="sessionFactory"/></property>
        </bean>

        <bean id="commandeDAO" class="actualis.web.dao.CommandeDAO">
                <property name="sessionFactory"><ref local="sessionFactory"/></property>
        </bean>

        <bean id="clientsDAO" class="actualis.web.dao.ClientsDAO">
                <property name="sessionFactory"><ref local="sessionFactory"/></property>
        </bean>

        <!-- ========================= WEB INTERCEPTORS DEFINITIONS ========================= -->
        <bean id="urlMapping"
                class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
                <property name="interceptors">
                        <list>
                                <ref bean="openSessionInViewInterceptor"/>
                        </list>
       </property>
        </bean>
        <bean name="openSessionInViewInterceptor"
        class="org.springframework.orm.hibernate3.support.OpenSessionInViewInterceptor">
                <property name="sessionFactory"><ref bean="sessionFactory"/></property>
        </bean>

Hibernate Lazy mode

The last trick is to get the lazy mode working. From one page to another, you are going to lose the session in the hibernate objects referenced, so you'll need to reattach those objects to the current session in order to perform operations on them.

One solution:

  1. Create an ordinary many-to-one Cat to Kittens, lazy=true.
  2. Load Cat in page ListCats, but don't touch Kittens.

  3. In page ListCats, instantiate a new Page instance ViewCat.

  4. Set the Cat property for page ViewCat (ie - viewCat.setCat(cat)).

  5. Page ViewCat is activated and begins rendering.

  6. Page ViewCat tries to list this Cat's Kittens, for example in a Foreach as in <span jwcid="@Foreach" source="ognl:cat.kittens"

value="ognl:kitten">.

Page rendering fails with a LazyInitializationException, even though the OpenSessionInViewFilter opened a session, since the Cat instance is not attached to the open session.

I've gotten around this by adding an attach() method to each of my service implementations. This method invokes getSession().lock( object, LockMode.NONE) to reassociate the specified object. I then manually invoke attach() to reassociate the detached objects for the given page. For example, in ViewCat.pageBeginRender(), I perform a getCatService().attach(getCat()).

For convenience, I've recently created a general-purpose HibernateService, which looks like this:

package com.shawn.model.service;

import org.springframework.orm.hibernate.support.HibernateDaoSupport;
import org.springframework.dao.DataAccessException;
import org.apache.log4j.Logger;
import org.apache.log4j.LogManager;
import net.sf.hibernate.LockMode;
import net.sf.hibernate.HibernateException;

/**
 * @author Shawn M Church
 * @version HibernateServiceImpl, Feb 28, 2005, 4:51:48 PM
 */
public class HibernateServiceImpl extends HibernateDaoSupport implements HibernateService
{
   private final static transient Logger _log = LogManager.getLogger(HibernateServiceImpl.class);

   /**
    * Reattach the specified object to the open Hibernate session
    *
    * @param object
    * @throws org.springframework.dao.DataAccessException
    */
   public void attach( Object object) throws DataAccessException
   {
      try
      {
         getSession().lock( object, LockMode.NONE);
      }
      catch (HibernateException e)
      {
         _log.error(e);
      }
   }
}

This is similar to my earlier solution, but it avoids the redundant attach() method having to exist in each DAO service implementation. So now, I can do this in my Tapestry page (usually in PageBeginRender()):

getHibernateService().attach(getCat());

With DataSqueezer

Another solution consists in tweaking the Richard Hensley's DataSqueezer trick: http://thread.gmane.org/gmane.comp.java.tapestry.user/15398

He creates a custom DataSqueezer that tapestry will invoke automatically. Using this technique, a data object is squeezed into a short form (its id and classname) when tapestry builds a link or a hidden form field. When that url or form field comes back in a later request or form submission, tapestry invokes the datasqueezer to unsqueeze it. The unsqueeze operation re loads the dataobject from the database (or cache) and hence it is in the current session.

There is also a wiki article on the data squeezer: http://wiki.apache.org/jakarta-tapestry/DataSqueezer

Here is my DataQueezer adaptor based on the Wiki article.

  public DataSqueezer createDataSqueezer() {

    ISqueezeAdaptor keySqueezer = new ISqueezeAdaptor() {

      private static final String PREFIX = "k";

      private static final String SEPARATOR = ";";

      private ApplicationContext appContext = null;

      private DataOperations manager = null;

      /**
       * @return the current hibernate session
       */
      private DataOperations getDataOperations() {
        if (manager != null) {
          return manager;
        }

        if (appContext == null) {
          Map global = (Map) getGlobal();
          appContext = (ApplicationContext) global.get(APPLICATION_CONTEXT_KEY);
        } 
        
        if (appContext != null) {
          manager = (DataOperations) appContext
              .getBean("DataOperations");
        }
        return manager;

      }

      public void register(DataSqueezer squeezer) {
        squeezer.register(PREFIX, EnterpriseObject.class, this);
      }

      public String squeeze(DataSqueezer squeezer, Object data)
          throws IOException {
        StringBuffer sb = new StringBuffer();
        sb.append(PREFIX);
        sb.append(data.getClass().getName());
        sb.append(SEPARATOR);
        Serializable id = getDataOperations().getIdentifier(data);
        sb.append(squeezer.squeeze(id));
        return sb.toString();
      }

      public Object unsqueeze(DataSqueezer squeezer, String string)
          throws IOException {
        int pos_sep = string.indexOf(SEPARATOR);
        String classname = string.substring(1, pos_sep);
        String index = string.substring(pos_sep + 1);
        Serializable id = (Serializable) squeezer.unsqueeze(index);
        try {
          return getDataOperations().load(classname, id);
        } catch (ClassNotFoundException e) {
          throw new IOException("Invalid class:" + classname);
        }
      }
    };
    ISqueezeAdaptor[] adapt = { keySqueezer };
    return new DataSqueezer(getResourceResolver(), adapt);
  }

The load is straightforward but I've ran into some troubles on generating the identifiers: on my pages I reference objects and some of them might have been loaded in the previous session! And there is the trick:

  public Serializable getIdentifier(final Object data) {
    return (Serializable) getHibernateTemplate().execute(
        new HibernateCallback() {
          public Object doInHibernate(Session session)
              throws HibernateException {
            //s_logger.debug("session=" + session.hashCode() + " getIdentifier:" + data.toString());
            try {
              return session.getIdentifier(data);
            } catch (HibernateException hex) {
              // ok then the entity was loaded with a different session
              // it is too risky to try to attach it, it may already be loaded
              Object copy = session.merge(data);
              return session.getIdentifier(copy);
            }
          }
        });
  }

Spring Beans in Tapestry Pages

The Spring reference guide describes a convenient way to use Spring beans in Tapestry pages. Here's the link: http://www.springframework.org/docs/reference/webintegration.html#view-tapestry-appctx

The code below is taken almost directly from the Spring reference, but I will include it here to provide relevant support for the above example.

In the HibernateService example, the .java page controller code includes an abstract accessor method which Tapestry enhances to provide access to the corresponding .page file property. For example, the .java code would contain this line:

   public abstract HibernateService getHibernateService();

In the .page file, this code would be used to get the named Spring bean:

   <property-specification name="hibernateService" type="com.shawn.model.service.HibernateService">
      global.springContext.getBean("hibernateService")
   </property-specification>

And the Spring applicationContext.xml would contain this bean declaration:

   <bean id="hibernateService" class="com.shawn.model.service.HibernateServiceImpl">
      <property name="sessionFactory">
         <ref local="sessionFactory"/>
      </property>
   </bean>

This implementation has worked well to integrate Tapestry, Hibernate, and Spring, and it provides a reasonably clean separation between the presentation and persistence layers. The Tapestry-based page code is not aware of Hibernate, and switching persistence layers in the future would involve changing only configuration files. More importantly, Hibernate lazy loading can be used without too much difficulty, and it has so far worked reliably and without any surprises.

FrequentlyAskedQuestions/SpringHibernate (last edited 2009-09-20 23:20:37 by localhost)