Quite some time ago, I had a chance to try out one of the coolest features of Hibernate when I had to insert and update multiple entities. By multiple, I mean thousands of entities and obviously this was a slow process. The solution which I was experimenting with was the JDBC Batching.
This is a really great feature of Hibernate. I don’t want to give another detailed explanation about this because there are plenty of them on the web. One of these is made by Vlad Mihalcea. I really suggest reading this one to get familiar with the basics before going forward.
As I mentioned previously, this is one of the most useful features when it comes to performance, related to inserting and updating entities. However, what Hibernate offered until now is to enable the JDBC Batching globally in the whole application. Luckily, there was a feature request to the Hibernate devs to implement the batching in a session basis which is already implemented and released with Hibernate 5.2. The main problem with the global switch was that it caused some performance degradation which was unacceptable in my case. Unfortunately I didn’t have the time to properly analyze what caused the performance issue in specific cases so I stopped experimenting with JDBC Batching. At that time, I though “a session level batching would be a great feature” and now it is in place. This makes it possible to utilize the Hibernate batching for only a set of use cases.
Let’s see how we can use this new session level batch size support.
With Hibernate 5.2, a new method is introduced to the SharedSessionContract interface which is called setJdbcBatchSize. This interface is extended by the Session interface, so if somehow we can get a Session object, this method can be called. I’m going to show you some ideas about using this new feature. The implementation will be based on Spring and is available on GitHub.
I’m starting with the Spring configuration, because I think it’s necessary to give an explanation on it. The current Spring release versions are not supporting Hibernate 5.2 properly, so you need to use some special classes which I’m going to show.
The configuration for Hibernate with EntityManager may look like the following:
<bean id="jpaVendorAdapter" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"> <property name="showSql" value="false" /> <property name="generateDdl" value="false" /> <property name="database" value="MYSQL" /> </bean> <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="jpaVendorAdapter" ref="jpaVendorAdapter" /> <property name="packagesToScan"> <list> <value>com.arnoldgalovics.blog</value> </list> </property> </bean>
If you try to start the application with this bean definitions and with the new Hibernate, you will get the following error:
Caused by: java.lang.NoSuchMethodError: org.hibernate.Session.getFlushMode()Lorg/hibernate/FlushMode; at org.springframework.orm.jpa.vendor.HibernateJpaDialect.prepareFlushMode(HibernateJpaDialect.java:187) at org.springframework.orm.jpa.vendor.HibernateJpaDialect.beginTransaction(HibernateJpaDialect.java:173) at org.springframework.orm.jpa.JpaTransactionManager.doBegin(JpaTransactionManager.java:380)
Currently, none of the Spring releases are supporting this Hibernate version, but with a little trick, this can be bypassed easily. Just a short explanation; the Session interface has been changed and the getFlushMode method now returns a javax.persistence.FlushModeType rather than the expected org.hibernate.FlushMode. Fortunately, there is a method to get the Hibernate FlushMode, called getHibernateFlushMode. To overcome the error, we should create a custom HibernateJpaDialect which acquires the FlushMode according to the new API. This can be done by creating an extension for the HibernateJpaDialect class and override the prepareFlushMode method. Note that to implement the same behavior as originally, we should copy the superclass implementation for this and just call the new getHibernateFlushMode method. The extension should look like this:
public class CustomHibernateJpaDialect extends HibernateJpaDialect { private static final long serialVersionUID = -3483592073952499113L; @Override protected FlushMode prepareFlushMode(final Session session, final boolean readOnly) throws PersistenceException { final FlushMode flushMode = session.getHibernateFlushMode(); if (readOnly) { // We should suppress flushing for a read-only transaction. if (!flushMode.equals(FlushMode.MANUAL)) { session.setFlushMode(FlushMode.MANUAL); return flushMode; } } else { // We need AUTO or COMMIT for a non-read-only transaction. if (flushMode.lessThan(FlushMode.COMMIT)) { session.setFlushMode(FlushMode.AUTO); return flushMode; } } // No FlushMode change needed... return null; } }
The NoSuchMethodError is fixed with this custom JpaDialect, but now the HibernateJpaVendorAdapter should use this one instead of the default HibernateJpaDialect. Unfortunately this is not configurable, but we can create a new JpaVendorAdapter based on the existing one which returns the correct CustomHibernateJpaDialect. We just need to extend the HibernateJpaDialect and override the getJpaDialect method.
public class CustomJpaVendorAdapter extends HibernateJpaVendorAdapter { private final HibernateJpaDialect jpaDialect = new CustomHibernateJpaDialect(); @Override public HibernateJpaDialect getJpaDialect() { return jpaDialect; } }
Now, we can register our custom JpaVendorAdapter which we have adapted to the new Hibernate version. After this, the ApplicationContext should start up correctly. Note that this custom fix is required only until Spring releases a new version which supports Hibernate 5.2 (this will be 4.3 GA). This is only covering the batching part of Hibernate, so you can expect more errors due to API changes and I wouldn’t recommend using this in a production environment.
Now we have a spring context which is working, let’s take a look at on the session level batching usage. The naive implementation may look like this for setting this property value.
public class BatchingService { @PersistenceContext private EntityManager entityManager; @Transactional public void updateWithSessionBatching() { final Session session = entityManager.unwrap(Session.class); session.setJdbcBatchSize(15); // do something } }
This is working fine, however for every use case, we have to use the same boilerplate code. From code quality point of view, we want to prevent this and make it somehow reusable. One can think of a method which sets this up, but that would be the same boilerplate code as this (or just a little bit better :-)).
What I can imagine is an annotation which has a field to set the batch size and it’s automatically handled. Let’s say we have an annotation called @JdbcBatchSize with the following definition:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface JdbcBatchSize { int value(); }
The usage would be similar to the following:
@Transactional @JdbcBatchSize(15) public void updateWithSessionBatching() { // do something }
This looks much cleaner than the previous one but now we have to process it somehow. This can be done by Spring’s AOP framework. I won’t go into details here as well because you can check out Spring’s documentation about this part.
There are multiple choices here to implement the processing, I chose to use a MethodInterceptor with a AbstractPointcutAdvisor. The MethodInterceptor looks like the following:
public class JdbcBatchSizeMethodInterceptor implements MethodInterceptor { @PersistenceContext private EntityManager entityManager; @Override public Object invoke(final MethodInvocation invocation) throws Throwable { final Session session = entityManager.unwrap(Session.class); session.setJdbcBatchSize(invocation.getMethod().getAnnotation(JdbcBatchSize.class).value()); return invocation.proceed(); } }
The interceptor basically does the same as in the naive implementation, sets the batch size for the current session.
Edit: Oliver Gierke just mentioned, that the jdbc batch size should be reset after an exceptional or successful execution. Unfortunately, this is not possible at the moment because of a wrong signature of the org.hibernate.SharedSessionContract#setJdbcBatchSize method. However I’ve created a ticket for the modification and will post the modified code.
Edit 2: Until the fix is released, I came up with a Reflection based solution, which is available on GitHub. The modified MethodInterceptor is the following:
public class JdbcBatchSizeMethodInterceptor implements MethodInterceptor { private static final Logger logger = LoggerFactory.getLogger(JdbcBatchSizeMethodInterceptor.class); @PersistenceContext private EntityManager entityManager; @Override public Object invoke(final MethodInvocation invocation) throws Throwable { final Session session = entityManager.unwrap(Session.class); final Integer originalBatchSize = session.getJdbcBatchSize(); session.setJdbcBatchSize(invocation.getMethod().getAnnotation(JdbcBatchSize.class).value()); Object result = null; try { result = invocation.proceed(); entityManager.flush(); } finally { try { final Field field = AbstractSharedSessionContract.class.getDeclaredField("jdbcBatchSize"); field.setAccessible(true); ReflectionUtils.setField(field, session, originalBatchSize); } catch (final Exception e) { logger.error("Error when setting original jdbc batch size for the session.", e); } } return result; } }
Advisor:
public class JdbcBatchSizeAdvisor extends AbstractPointcutAdvisor { private static final long serialVersionUID = -8466931120118556794L; private final StaticMethodMatcherPointcut pointcut = new StaticMethodMatcherPointcutAdvisor() { private static final long serialVersionUID = 4458743456797695036L; @Override public boolean matches(final Method method, final Class<?> targetClass) { return method.isAnnotationPresent(JdbcBatchSize.class) && method.isAnnotationPresent(Transactional.class); } }; @Autowired private JdbcBatchSizeMethodInterceptor interceptor; @Override public Pointcut getPointcut() { return pointcut; } @Override public Advice getAdvice() { return interceptor; } @Override public int getOrder() { return Ordered.LOWEST_PRECEDENCE; } }
The main point of this class is the annotation check for the method calls and the ordering of the aspects. If you think about it, it will be clear. To acquire the current session, we should be in a transactional context. The @Transactional annotation works with the Spring AOP framework also which is using proxies. This means that if our aspect has the priority over the transactional aspect, we won’t be in transactional context, thus we can’t get the current session so we have to somehow make sure that the ordering is correct. Setting the ordering for the transactional aspect can be done with the order attribute in the transaction-manager tag.
<tx:annotation-driven transaction-manager="transactionManager" order="0" proxy-target-class="true" />
Lower order value means it has more priority over the bigger one. You can check out the javadoc of the Ordered interface. This ordering is also defined for the JdbcBatchSizeAdvisor with the lowest precedence to make sure that the transactional context is set up.
After this, the new @JdbcBatchSize annotation can be used together with the @Transactional annotation to set up the batch size for the current session.
The implementation is available on my GitHub page.
Edit 3: With Hibernate 5.2.1 and Spring 4.3.1 available, the Reflection part for setting the JDBC Batch size back can be removed as well as the CustomJpaVendorAdapter. The updated code is available on GitHub.
Very good article. Thanks for sharing!
The compatibility issues of Spring Framework with Hibernate 5.2 are going to be fixed with the upcoming 4.3 GA. In fact they already are in the latest snapshots. Also the Spring Data team has already applied all the mitigations necessary to deal with the breaking changes that this Hibernate release shipped. Both the Spring Framework release and the Spring Data one will be out by some time next week.
The interceptor approach looks interesting. I think you might want to reset the batch size to the value it had before you manipulate it in both succeeding and exception cases. Otherwise the code calling your method will persistently have changed the batch size for the current thread.
Yes, I mentioned the 4.3 GA in the post as well.
The resetting is a great idea, the interceptor can be modified easily to this behavior.
I just didn’t want people to blindly copy broken code ;). You might know it can be fixed, I know it would have to be to be really usable, the casual reader will just copy the half-working code.
Yes, I agree. Also, probably it was my mistake not mentioning that I will fix this issue. 🙂
Just committed the modification for the MethodInterceptor.
https://github.com/galovics/hibernate-session-batching/commit/b2f22df052ca02fbe9dc25f14757024b5f732482
Thanks for the feedback Oliver.
Just turned out that my implementation was wrong because, it is not possible to set back null value, due to the wrong signature of the setter method. It only accepts primitive integers.
I’ll create a ticket for it in Hibernate to fix the issue.
Great article, keep ’em coming! 🙂