Tapestry Magic #9: Integrating with hibernate and multiple database support

Tapestry5 already has a module for integration with hibernate. This module is restricted to only one database. In this post I will create a small module which can support multiple databases. I am not going to provide all the facilities that tapestry-hibernate module already provides but just enough to solve the multiple database access problem.

In order to support multiple database, we will use a session factory identifier or a factoryId to identify a SessionFactory. This will be used as the service id to identify the service. This way we can use @InjectService("factoryId") or @Named("factoryId")@Inject

We define a SessionFactorySource which will be responsible for creating and managing SessionFactorys

public interface SessionFactorySource {
   public SessionFactory getSessionFactory(String factoryID);
   
   public SessionFactory getSessionFactory(Class<?> entityClass);
   
   public Session createSession(Class<?> entityClass);
   
   public Session createSession(String factoryID);

   public String getFactoryID(Class<?> entityClass);
}

The getSessionFactory() methods return the SessionFactory for a particular factoryId or entity class. The createSession() method is responsible for creating a new session. The getFactoryId() method provides a mapping from entity class to session factory id. The implementation of this service takes a collection of configurations for configuring different SessionFactorys. The configuration is

public class SessionFactoryConfiguration {
   private final String[] packageNames;
   private final String id;

   public SessionFactoryConfiguration(final String[] packageNames, final String id) {
      this.packageNames = packageNames;
      this.id = id;
   }

   public String[] getPackageNames() {
      return packageNames;
   }

   public final String getSymbol() {
      return id;
   }
   
   public void configure(Configuration configuration){
      
   }
}

In the configuration we can specify the package names containing the domain objects/entities and the factory id to be used to identify a particular SessionFactory. There is a configure() method which can be used to configure the SessionFactory.

The implementation of SessionFactorySource is as under

public class SessionFactorySourceImpl implements SessionFactorySource,
   RegistryShutdownListener {

   private final Map<String, SessionFactory> symbolMap = new HashMap<String, SessionFactory>();
   private final Map<Class<?>, String> entityMap = new HashMap<Class<?>, String>();
   private final ClassNameLocator classNameLocator;

   public SessionFactorySourceImpl(final ClassNameLocator classNameLocator,
       final List<SessionFactoryConfiguration> configurations) {
      this.classNameLocator = classNameLocator;
      for(final SessionFactoryConfiguration configuration : configurations){
         setupSessionFactory(configuration);
      }
   }

   private void setupSessionFactory(
      final SessionFactoryConfiguration configuration) {
      final Configuration hibernateConfig = new Configuration();
      final List<Class<?>> entities = loadEntityClasses(configuration);

      // Load entity classes
      for(final Class<?> entityClass : entities){
         hibernateConfig.addAnnotatedClass(entityClass);
         entityMap.put(entityClass, configuration.getSymbol());
      }

      configuration.configure(hibernateConfig);
      
      final SessionFactory sf = hibernateConfig.buildSessionFactory();
      if(configuration.getSymbol() != null){
         symbolMap.put(configuration.getSymbol(), sf);
      }
   }

   private List<Class<?>> loadEntityClasses(
      final SessionFactoryConfiguration configuration) {
      final ClassLoader classLoader = Thread.currentThread()
         .getContextClassLoader();

      final List<Class<?>> entityClasses = new ArrayList<Class<?>>();

      for(final String packageName : configuration.getPackageNames()){
         for(final String className : classNameLocator
            .locateClassNames(packageName)){
            try{
               Class<?> entityClass = null;
               entityClass = classLoader.loadClass(className);
               if(entityClass.getAnnotation(javax.persistence.Entity.class) != null ||
                   entityClass.getAnnotation(javax.persistence.MappedSuperclass.class) != null){
                  entityClasses.add(entityClass);
               }
            }catch(ClassNotFoundException e){
               throw new RuntimeException(e);
            }
         }
      }
      return entityClasses;
   }

   public SessionFactory getSessionFactory(final String factoryID) {
      SessionFactory sf = symbolMap.get(factoryID);
      if(sf == null){
         throw new RuntimeException("No session factory found for factoryID: "
            + factoryID);
      }
      return sf;
   }

   public SessionFactory getSessionFactory(final Class<?> factoryID) {
      SessionFactory sf = getSessionFactory(entityMap.get(factoryID));
      if(sf == null){
         throw new RuntimeException("No session factory found for entity: "
            + factoryID);
      }
      return sf;
   }

   public void registryDidShutdown() {
      for(final SessionFactory sessionFactory : symbolMap.values()){
         sessionFactory.close();
      }
   }

   public Session createSession(Class<?> entityClass) {
      return createSession(getFactoryID(entityClass));
   }

   public Session createSession(String factoryID) {
      final Session session = getSessionFactory(factoryID).openSession();
      return session;
   }

   public String getFactoryID(Class<?> entityClass) {
      return entityMap.get(entityClass);
   }
}

In the constructor we loop over all the configurations and set up SessionFactorys. We add to it all the classes annotated with @Entity and @MappedSuperclass in the specified packages. We keep a map of SessionFactory and factoryIds. We also keep a map of all the entity classes and their associated SessionFactorys. The other methods are just for retrieving values from these maps.

Next we define a per-thread service for managing sessions.


public interface HibernateSessionManager {
   
   public Session getSession(Class<?> entityClass);
   
   public Session getSession(String factoryID);
   
   public Session getSession();
}

The methods are used to retrieve Session based on factoryId or entityType. The method without parameter retrieves the session for default factory id (specified by the symbol DEFAULT_FACTORY_ID).
The implementation is as under

public class HibernateSessionManagerImpl implements HibernateSessionManager,
   ThreadCleanupListener {

   private SessionFactorySource sessionFactorySource;
   private String defaultFactoryID;
   private Map<String, Session> sessions = new HashMap<String, Session>();

   public HibernateSessionManagerImpl(
      @Symbol(TapestryHibernateMultipleConstants.DEFAULT_FACTORY_ID) String defaultFactoryID,
      SessionFactorySource sessionFactorySource) {
      this.sessionFactorySource = sessionFactorySource;
      this.defaultFactoryID = defaultFactoryID;
   }

   public Session getSession(Class<?> entityClass) {
      return getSession(sessionFactorySource.getFactoryID(entityClass));
   }
   
   public Session getSession(String factoryID) {
      factoryID = nvl(factoryID);
      Session session = sessions.get(factoryID);
      if( session == null){
         session = createSession(factoryID);
      }      
      return session;
   }

   public Session getSession() {
      return getSession(defaultFactoryID);
   }

   public void threadDidCleanup() {
      for(final Session session: sessions.values()){
         session.close();
      }
      sessions.clear();
   }
   
   private String nvl(String factoryID) {
      return (factoryID == null || "".equals(factoryID)) ? defaultFactoryID : factoryID;
   }

   public Session createSession(String factoryID) {
      factoryID = nvl(factoryID);
      final Session session = sessionFactorySource.createSession(factoryID);
      sessions.put(factoryID, session);
      return session;
   }

   public Session createSession() {
      return createSession(defaultFactoryID);
   }
   
   public void setSession(String factoryID, Session session) {
      sessions.put(nvl(factoryID), session);      
   }   

}

Now a new session can be accessed by injecting HibernateSessionManager and using getSession(factoryId) or getSession(entityClass) methods. In order to access the session as a service, we will need a Shadow builder for SessionFactory which will access its method getService(factoryId) to create a Session service.

//Interface
public interface SessionShadowBuilder {
   Session build(HibernateSessionManager sm, String sessionFactoryId);
}

//Implementation
public class SessionShadowBuilderImpl implements SessionShadowBuilder {
   private final ClassFactory classFactory;

   public SessionShadowBuilderImpl(@Builtin ClassFactory classFactory) {
      this.classFactory = classFactory;
   }

   @SuppressWarnings("unchecked")
   public Session build(HibernateSessionManager sm, String sessionFactoryId) {
      Class sourceClass = sm.getClass();
      ClassFab cf = classFactory.newClass(Session.class);

      cf.addField("_source", Modifier.PRIVATE | Modifier.FINAL, sourceClass);
      cf.addConstructor(new Class[] { sourceClass }, null, "_source = $1;");

      BodyBuilder body = new BodyBuilder();
      body.begin();

      body.addln("%s result = _source.getSession(\"%s\");", sourceClass.getName(), sessionFactoryId);

      body.addln("if (result == null)");
      body.begin();
      body
            .addln(
                  "throw new NullPointerException(%s.buildMessage(_source, \"getSession(%s)\"));",
                  getClass().getName(), sessionFactoryId);
      body.end();
      body.addln("return result;");
      body.end();

      MethodSignature sig = new MethodSignature(Session.class, "_delegate",
            null, null);
      cf.addMethod(Modifier.PRIVATE, sig, body.toString());

      String toString = String.format("<Shadow: getSession(%s) of HibernateSessionManager>",
            sessionFactoryId);

      cf.proxyMethodsToDelegate(Session.class, "_delegate()", toString);

      Class shadowClass = cf.createClass();
      try {
         Constructor cc = shadowClass.getConstructors()[0];
         Object instance = cc.newInstance(sm);
         return (Session)instance;
      } catch (Exception ex) {
         // Should not be reachable
         throw new RuntimeException(ex);
      }

   }

   public static final String buildMessage(Object source, String propertyName) {
      return String
            .format(
                  "Unable to delegate method invocation to property '%s' of %s, because the property is null.",
                  propertyName, source);
   }
}

This service creates a proxy for Session which delegates every call to a session created using HibernateSessionFactory#getSession(factoryId).

Finally we make the contributions in our Module class

public class TapestryHibernateMultipleModule {

   public static void bind(ServiceBinder binder) {
      binder.bind(SessionFactorySource.class, SessionFactorySourceImpl.class);
      binder.bind(SessionShadowBuilder.class, SessionShadowBuilderImpl.class);
   }

   public void contributeFactoryDefaults(
         MappedConfiguration<String, String> defaults) {
      defaults.add(TapestryHibernateMultipleConstants.DEFAULT_FACTORY_ID,
            "default");
   }

   @Scope(ScopeConstants.PERTHREAD)
   public HibernateSessionManager buildHibernateSessionManager(
         @Symbol(TapestryHibernateMultipleConstants.DEFAULT_FACTORY_ID) String defaultFactoryID,
         SessionFactorySource sessionFactorySource,
         PerthreadManager threadManager) {
      HibernateSessionManagerImpl sm = new HibernateSessionManagerImpl(
            defaultFactoryID, sessionFactorySource);
      threadManager.addThreadCleanupListener(sm);
      return sm;
   }
   
   @ServiceId("default")
   public Session buildDefaultSession(
         @Symbol(TapestryHibernateMultipleConstants.DEFAULT_FACTORY_ID) String defaultFactoryID,
         SessionShadowBuilder sessionShadowBuilder,
         HibernateSessionManager sessionManager){
      return sessionShadowBuilder.build(sessionManager, defaultFactoryID);
   }

}

For using this module, we have to contribute a HibernateSessionConfiguration in the Module class


   public static void contributeFactoryDefaults(MappedConfiguration<String,String> configuration){
      configuration.add(TapestryHibernateMultipleConstants.DEFAULT_FACTORY_ID, "default");
   }

   @Contribute(SessionFactorySource.class)
   public void providerSessionFactorySource(
         Configuration<SessionFactoryConfiguration> configuration) {
      configuration.add(
            new SessionFactoryConfiguration(
                  new String[] { "com.googlecode.tawus.hibernate.models" },
                  "default") {
               @Override
               public void configure(
                     org.hibernate.cfg.Configuration configuration) {
                  Properties prop = new Properties();
                  prop.put("hibernate.dialect",
                        "org.hibernate.dialect.HSQLDialect");
                  prop.put("hibernate.connection.driver_class",
                        "org.hsqldb.jdbcDriver");
                  prop
                        .put("hibernate.connection.url",
                              "jdbc:hsqldb:mem:testdb");
                  prop.put("hibernate.connection.username", "sa");
                  prop.put("hibernate.connection.password", "");
                  prop.put("hibernate.connection.pool_size", "1");
                  prop.put("hibernate.connection.autocommit", "false");
                  prop.put("hibernate.hbm2ddl.auto", "create-drop");
                  prop.put("hibernate.show_sql", "true");
                  prop.put("hibernate.current_session_context_class", "thread");
                  configuration.addProperties(prop);
               }
            });
      
      configuration.add(
            new SessionFactoryConfiguration(
                  new String[] { "com.googlecode.tawus.hibernate.models2" },
                  "second") {
               @Override
               public void configure(
                     org.hibernate.cfg.Configuration configuration) {
                  Properties prop = new Properties();
                  prop.put("hibernate.dialect",
                        "org.hibernate.dialect.HSQLDialect");
                  prop.put("hibernate.connection.driver_class",
                        "org.hsqldb.jdbcDriver");
                  prop
                        .put("hibernate.connection.url",
                              "jdbc:hsqldb:hsql://localhost/firstdb");
                  prop.put("hibernate.connection.username", "sa");
                  prop.put("hibernate.connection.password", "");
                  prop.put("hibernate.connection.pool_size", "1");
                  prop.put("hibernate.connection.autocommit", "false");
                  prop.put("hibernate.hbm2ddl.auto", "create-drop");
                  prop.put("hibernate.show_sql", "true");
                  prop.put("hibernate.current_session_context_class", "thread");
                  configuration.addProperties(prop);
               }
            });

   }
   
   @ServiceId("second")
   public Session buildFinacleSession(
         SessionShadowBuilder sessionShadowBuilder,
         HibernateSessionManager sessionManager){
      return sessionShadowBuilder.build(sessionManager, "second");
   }

TapestryHibernateMultipleConstants is a simple class containing symbolic constants

public class TapestryHibernateMultipleConstants {
   public static final String DEFAULT_FACTORY_ID = "default";
}

Now we can get the sessions in the page as

   public class TestPage {
      @InjectService("default")
      private Session session;

      @InjectService("second")
      private Session otherSession;

   }

If we have only one sessionfactory we can continue using @Inject Session.

About these ads

Tagged: ,

9 thoughts on “Tapestry Magic #9: Integrating with hibernate and multiple database support

  1. Toby May 17, 2011 at 6:40 PM Reply

    Would be something for the next official Tapestry-Hibernate release!

  2. tawus October 5, 2011 at 1:47 PM Reply

    Hi Oliver

    I have added the code for it but there may be other parts missing as the intention was to show the concept. If I get hold of the code, I will mail it to you. You can also check hibernate module in tawus repository at github

    • olip October 5, 2011 at 2:58 PM Reply

      thanks for your reply. It would be great If you could send me the code.

  3. Borut Bolčina November 23, 2011 at 12:51 PM Reply

    Hello, what is the status of integrating this to official release of tapestry hibernate module? We are at the beggining of a major project and support for connecting to multiple databases is a must for us. Tawus, can you integrate this code above into the main codebase now? Are you already a commiter? If so, congratulations as your posts are really fantastic!

    • tawus November 23, 2011 at 1:53 PM Reply

      Thanks Borut

      I am going to do that soon. But the integration is quite different now. It is at https://github.com/tawus/tapestry5. I am using it in a new project and will commit it as soon as I am satisfied it is stable enough

  4. Nq October 22, 2014 at 8:05 PM Reply

    What is the difference with this library (https://github.com/tawus/tawus/tree/master/tawus-hibernate) and the modifications on tapestry-hibernate module? And is it possible that you have not committed anything yet on (https://github.com/tawus/tapestry5)?

    I’m trying to make my own library to support multiple databases and I’m looking into your github, which will be of great help.

    Thanks

    • Nq October 24, 2014 at 7:10 PM Reply

      Ok, I have found it in the 5.3 branch, my bad. Is this something you use in production, or is this unfinished?

      • tawus November 9, 2014 at 2:22 PM

        Hi Nathan,

        It is in production in a couple of applications.

    • tawus November 9, 2014 at 2:41 PM Reply

      tawus-hibernate is built from scratch while tapestry-hibernate clone is modifications to the core tapestry-hibernate branch. Both are for tapestry 5.3. tapestry-hibernate modifications have some breaking changes so I never merged it with the core branch.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 91 other followers

%d bloggers like this: