Saturday, August 31, 2013

@PrePersist and @PreUpdate - not working with Hibernate 4 Session and Spring

    @PrePersist and @PreUpdate annotations have their roots in JPA specification. Hibernate implements JPA specification. However, if we decide to use SessionFactory and Session instead of EntityManagerFactory and EntityManager, we will realize that mentioned annotations are ignored. Is there a way to solve the problem?
Of course - there is. Or better - there are solutions. The simplest approach is to switch to EntityManager. However, sometimes we would like to stay with good, old Hibernate Session. In that scenario we can add Hibernate EntityManager to our project:
<dependency>
   <groupId>org.hibernate</groupId>
   <artifactId>hibernate-entitymanager</artifactId>
   <version>4.2.2.Final</version>
</dependency> 
and register Hibernate JPA Event Listeners (they are defined in 'hibernate-entitymanager') manually - source. But proposed solution is for old version of Hibernate. Since Hibernate 4 some changes in code have been performed. I had to skim through the Hibernate 4 code in order to find out the way of proper registration of necessary event listeners. Here it is:
LocalSessionFactoryBuilder sfb = new LocalSessionFactoryBuilder(dataSource());
sfb.scanPackages("dream.product");
ReflectionManager reflectionManager = sfb.getReflectionManager();
SessionFactoryImpl sessionFactory = ((SessionFactoryImpl) sfb.buildSessionFactory());
EntityCallbackHandler callbackHandler = new EntityCallbackHandler();
callbackHandler.add(reflectionManager.toXClass(EntityWithJPACallbacks.class), reflectionManager);
EventListenerRegistry registry = sessionFactory.getServiceRegistry().getService(EventListenerRegistry.class);
// for @PrePersist:
registry.setListeners(EventType.PERSIST, new EJB3PersistEventListener(callbackHandler));
// for @PreUpdate
registry.setListeners(EventType.FLUSH_ENTITY, new EJB3FlushEntityEventListener(callbackHandler)); 
After that I was able to use those two beauties: @PrePersist and @PreUpdate.

5 comments :

  1. Hi, I am trying to implement your proposed solution, but i want this solution to be system wide not any particular entity specific 'EntityWithJPACallbacks'. Can you suggest some way to achieve that.

    ReplyDelete
    Replies
    1. You can use 'Reflections' library:

      org.reflections
      reflections
      0.9.9-RC1


      and

      Reflections reflections = new Reflections("model");
      for (Class entityClass : reflections.getTypesAnnotatedWith(Entity.class)) {
      callbackHandler.add(reflectionManager.toXClass(entityClass), reflectionManager);
      }

      where "model" is the package with your entities and Entity.class is the annotation used to mark your entities. If you want to scan a full classpath you can go with no-arg constructor of the 'Reflections', however, it may be really inefficient especially if you have hundreds of classes.

      Delete
  2. Please remember that the class is identified by a fully qualified name + ClassLoader used to load the class. It means that if you have two classes with the same name which are loaded by different ClassLoaders - these classes are different. To sum up, if you have problems with the code from above, please try to use:

    Reflections reflections = new Reflections(this.getClass().getClassLoader());

    rather than:

    Reflections reflections = new Reflections("model");

    ReplyDelete
  3. hey Ɓukasz, can you please little more details about above code, like where we have write the code such it will be registered with hibernate session factory. Source code will be lot more helpful

    ReplyDelete
    Replies
    1. You need to search your code and find the place where HibernateSessionFactory is created. There should be just one place if we deal with monolithic application. Typically, just one instance of HibernateSessionFactory per application context is created. Once you find that place you can do the magic:

      sessionFactory -> found by you
      EntityCallbackHandler callbackHandler = new EntityCallbackHandler();
      callbackHandler.add(reflectionManager.toXClass(EntityWithJPACallbacks.class), reflectionManager);
      EventListenerRegistry registry = sessionFactory.getServiceRegistry().getService(EventListenerRegistry.class);
      registry.setListeners(EventType.PERSIST, new EJB3PersistEventListener(callbackHandler));
      registry.setListeners(EventType.FLUSH_ENTITY, new EJB3FlushEntityEventListener(callbackHandler));

      P.S.
      In my example there are two libraries involved: Spring and Hibernate. If you want to do the same in plain Hibernate - you can also achieve that:
      EntityCallbackHandler and EventListenerRegistry come from Hibernate. You just need to instantiate ReflectionManager on your own. The answer how to do that is within Spring's LocalSessionFactoryBuilder and Spring's Configuration classes.

      Delete