How to make Hibernate and Spring track transactions consistently? - spring

When using TransactionTemplate in an Executor thread, it appears unable to consistently find the current transaction, resulting in problems like this:
14:20:56.022 [pool-2-thread-1] DEBUG HibernateTransactionManager - Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_SERIALIZABLE
14:20:56.022 [pool-2-thread-1] DEBUG HibernateTransactionManager - Opened new Session [SessionImpl(1516598186<open>)] for Hibernate transaction
14:20:56.022 [pool-2-thread-1] DEBUG HibernateTransactionManager - Preparing JDBC Connection of Hibernate Session [SessionImpl(1516598186<open>)]
14:20:56.023 [pool-2-thread-1] DEBUG HibernateTransactionManager - Exposing Hibernate transaction as JDBC [org.springframework.orm.hibernate5.HibernateTransactionManager$$Lambda$1238/0x00000008009dfc40#5cc5f4]
14:20:56.026 [pool-2-thread-1] DEBUG HibernateTransactionManager - Found thread-bound Session [SessionImpl(1516598186<open>)] for Hibernate transaction
14:20:56.026 [pool-2-thread-1] DEBUG HibernateTransactionManager - Participating in existing transaction
14:20:56.052 [pool-2-thread-1] DEBUG SessionFactoryUtils - Flushing Hibernate Session on transaction synchronization
14:20:56.054 [pool-2-thread-1] DEBUG HibernateTransactionManager - Initiating transaction rollback after commit exception
javax.persistence.TransactionRequiredException: no transaction is in progress
at org.hibernate.internal.AbstractSharedSessionContract.checkTransactionNeededForUpdateOperation(AbstractSharedSessionContract.java:445)
at org.hibernate.internal.SessionImpl.checkTransactionNeededForUpdateOperation(SessionImpl.java:3478)
at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1394)
at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1389)
at org.springframework.orm.hibernate5.SessionFactoryUtils.flush(SessionFactoryUtils.java:113)
at org.springframework.orm.hibernate5.SpringSessionSynchronization.beforeCommit(SpringSessionSynchronization.java:95)
at org.springframework.transaction.support.TransactionSynchronizationUtils.triggerBeforeCommit(TransactionSynchronizationUtils.java:97)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.triggerBeforeCommit(AbstractPlatformTransactionManager.java:916)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:727)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:711)
at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:152)
at org.springframework.transaction.support.TransactionOperations.executeWithoutResult(TransactionOperations.java:67)
at com.example.MyClass.doProcessing(MyClass.java:109)
at com.example.MyClass.lambda$scheduleJob$4(MyClass.java:93)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:829)
14:20:56.054 [pool-2-thread-1] DEBUG HibernateTransactionManager - Rolling back Hibernate transaction on Session [SessionImpl(1516598186<open>)]
14:20:56.056 [pool-2-thread-1] DEBUG HibernateTransactionManager - Closing Hibernate Session [SessionImpl(1516598186<open>)] after transaction
JPA is configured manually, because I have multiple DataSources in the application.
Relevant beans:
#Bean
#TelemetryDb
public LocalContainerEntityManagerFactoryBean telemetryEntityManagerFactory(
EntityManagerFactoryBuilder builder,
#TelemetryDb DataSource dataSource
) {
return builder.dataSource(dataSource)
.packages("com.example.model.telemetry")
.persistenceUnit("telemetry")
.properties(getVendorProperties(dataSource))
.jta(false)
.build();
}
#Bean
#TelemetryDb
#Order(Ordered.HIGHEST_PRECEDENCE)
public PlatformTransactionManager telemetryTransactionManager(#TelemetryDb EntityManagerFactory factory) {
return new HibernateTransactionManager((SessionFactory) factory);
}
#Configuration
#EnableJpaRepositories(
basePackageClasses = PackageMarker.class,
includeFilters = #ComponentScan.Filter(type = FilterType.CUSTOM, value = TelemetryFilter.class),
entityManagerFactoryRef = "telemetryEntityManagerFactory",
transactionManagerRef = "telemetryTransactionManager"
)
public static class Telemetry { }
The properties are:
hibernate.auto_quote_keyword=true
hibernate.current_session_context_class=org.springframework.orm.hibernate5.SpringSessionContext
hibernate.dialect=com.example.PostgreSQL12Dialect
And some code that gives the above log:
#Service
#RequriedArgsConstructor
public class MyClass {
private final MyRepository repository;
private ExecutorService executor = Executors.newSingleThreadExecutor();
private SessionFactory sessionFactory;
private TransactionTemplate transactionTemplate;
#Autowired
public void setTransactionManager(#TelemetryDb PlatformTransactionManager manager) {
this.sessionFactory = ((HibernateTransactionManager) manager).getSessionFactory();
this.transactionTemplate = new TransactionTemplate(manager);
this.transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
}
public scheduleJob(long entityId) {
Boolean submit = transactionTemplate.execute(status -> {
MyEntity entity = repository.findById(entity).orElseThrow();
if (entity.isProcessing()) {
return false;
} else {
entity.setProcessing(true);
return true;
}
});
if (Boolean.TRUE.equals(submit)) {
executor.submit(() -> doProcessing(entityId));
}
}
protected void doProcessing(long entityId) {
transactionTemplate.executeWithoutResult(status -> {
MyEntity entity = repository.findById(entity).orElseThrow();
entity.setBlobField(sessionFactory.getCurrentSession().getLobHelper().createBlob(new byte[]{});
entity.setProcessing(false);
status.flush();
});
}
}
Note that the usage of TransactionTemplate outside of the executor thread completes with no issues. Note also that the createBlob() and flush() do not complain about a missing transaction.
I assume I have missed something in the configuration, but I have been trying various permutations of it, and the doProcessing method, but cannot get anything to work.
After further debugging, it appears that the getCurrentSession() call causes SpringSessionContext to look for, and then create, a session using a SessionFactoryImpl as the key rather than the LocalContainerEntityManagerFactoryBean proxy.
There doesn't seem to be any way to control the construction of the context within Hibernate. I think if I could obtain the raw SessionFactoryImpl to pass to HibernateTransactionManager then that would fix it.
I tried that (using EntityManagerFactory.unrwap) but that broke a whole load of other stuff, because when Spring Data calls createQuery it looks for the current session using the bean proxy.

It appears that SpringSessionContext doesn't work as intended. Either there was a change in Hibernate and/or there wasn't sufficient integration testing.
There is a workaround. Ignore SessionFactory.getCurrentSession() and use the TransactionSynchronizationManager directly:
#PersistenceUnit(unitName = "telemetry")
private EntityManagerFactory entityManagerFactory;
private #NonNull Session getCurrentSession() {
Object resource = TransactionSynchronizationManager.getResource(entityManagerFactory);
if (resource == null) {
throw new IllegalStateException("No current transaction");
}
return ((SessionHolder) resource).getSession();
}
That only works within an existing session. More code would be required to create a new one to match the full behaviour.
Unsetting hibernate.current_session_context_class ensures you will always get an exception when trying to use the incompatible interface, rather than getting surprise extra sessions.

Related

Spring Boot not rolling back transaction on exception

I have a straightforward code that saves something to the DB and then throws an exception to see if the changes get rollback.
ResourceA.java
#Autowire ServiceA serviceA;
#PUT
#Path("/{Id:[0-9]+}")
public ObjectA updateSomethingResource(..) {
return serviceA.upateSomethingService(..);
}
ServiceA.java
#Transactional(rollbackFor=Exception.class)
public ObjectA upateSomethingService(EntitlementRequest entitlementRequest) throws ServiceException {
ObjectA objectA = getObjectFromDB(..);
objectA.setName("New Name");
dao.save(objectA)
throw new ServiceException("error"); //ServiceException extends Exception
}
When executing this and I check the DB the name of the object is still New Name. I would have excepted it to be rolled back to what it was originally.
To understand what is happening I have tried looking at what log I can enable and the only one that I managed to enable is
logging.level.org.springframework.transaction.interceptor=TRACE
which gives:
TRACE o.s.t.i.TransactionInterceptor - Getting transaction for [xxxxx.upateSomethingService]
TRACE o.s.t.i.TransactionInterceptor - Completing transaction for [xxxxx.upateSomethingService] after exception: xxx.ServiceException
So the transaction manager does see the exception but nothing gets rollback.
Is there something I need to enable somewhere to have rollback actually working?
What other log can I enable that could show me exactly what is happening?
UPDATE:
I have managed to show ore logs with logging.level.org.springframework=DEBUG
DEBUG o.s.jdbc.datasource.DataSourceUtils - Fetching JDBC Connection from DataSource
DEBUG o.s.jdbc.datasource.DataSourceUtils - Fetching JDBC Connection from DataSource
DEBUG o.s.j.s.JdbcTransactionManager - Creating new transaction with name [xxx.service.upateSomethingService]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-java.lang.Exception
DEBUG o.s.j.s.JdbcTransactionManager - Acquired Connection [1091371323, URL=jdbc:mysql://localhost:3306/table?allowMultiQueries=true, UserName=root#localhost, MySQL Connector/J] for JDBC transaction
DEBUG o.s.j.s.JdbcTransactionManager - Switching JDBC Connection [1091371323, URL=jdbc:mysql://localhost:3306/table?allowMultiQueries=true, UserName=root#localhost, MySQL Connector/J] to manual commit
DEBUG o.s.jdbc.datasource.DataSourceUtils - Fetching JDBC Connection from DataSource
DEBUG o.s.j.s.JdbcTransactionManager - Initiating transaction rollback
DEBUG o.s.j.s.JdbcTransactionManager - Rolling back JDBC transaction on Connection [1091371323, URL=jdbc:mysql://localhost:3306/table?allowMultiQueries=true, UserName=root#localhost, MySQL Connector/J]
DEBUG o.s.j.s.JdbcTransactionManager - Releasing JDBC Connection [1091371323, URL=jdbc:mysql://localhost:3306/table?allowMultiQueries=true, UserName=root#localhost, MySQL Connector/J] after transaction
Looking at these logs.. spring is actually telling me it 'is' rolling back the transaction.. but nothing gets changed in the DB ?
Something odd that I just noticed is that right after dao.save(objectA) I can already see the name being changed in the DB.. so somehow MyBatis is auto committing and doesn't look like it is using the same connection has the one I see in the logs
This is how I setup the Databasource and configure MyBatis
(I have 2 datasource this is why I'm using this instead of just using application.properties properties.)
#Bean(name = MYBATIS_DATASOURCE_ONE, destroyMethod = "")
public DataSource dataSource(....) throws SQLException {
PooledDataSource dataSource = new PooledDataSource();
dataSource.setDriver(driverclassname);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
[...]
return dataSource;
}
#Bean(name = A_SESSION_FACTORY, destroyMethod = "")
#Primary
public SqlSessionFactory sqlSessionFactory(#Qualifier(MYBATIS_DATASOURCE_A) DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mybatis/mapper/*.xml"));
final SqlSessionFactory sqlSessionFactory = factoryBean.getObject();
return sqlSessionFactory;
}
You latest update made it clearer... What is happening I think is that Spring is not using your manually defined DataSource when instantiating the DataSourceTransactionManager. It is probably using a BasicDataSource or something similar (one that has been created using the properties from your application.properties
MyBatis documentation clearly specifies that for Transaction to work the DataSourceTransactionManager needs to have the exact same Datasource the rest of your app is using otherwise it won't work.
Add this to your configuration and the transaction should start working
#Bean
public DataSourceTransactionManager transactionManager(#Qualifier(MYBATIS_DATASOURCE_A) DataSource dataSource) {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
Normally, you wouldn't need to manually define this Bean because Spring would automatically do it for you. But since you are using your own DataSource you need to do it.
If you want to verify this. Don't declare this bean and put a breakpoint in the DataSourceTransactionManager Constructor.
You should notice that the DataSource is not the same instance as the one you have declared yourself.

Transactional test in Spring Boot - how to acquire current session?

Spring documentation warns about False Positives in Transactional tests and suggest the following:
// ...
#Autowired
SessionFactory sessionFactory;
#Transactional
#Test // no expected exception!
public void falsePositive() {
updateEntityInHibernateSession();
// False positive: an exception will be thrown once the Hibernate
// Session is finally flushed (i.e., in production code)
}
#Transactional
#Test(expected = ...)
public void updateWithSessionFlush() {
updateEntityInHibernateSession();
// Manual flush is required to avoid false positive in test
sessionFactory.getCurrentSession().flush();
}
// ...
I have the following base class:
#SpringBootTest
#Transactional
#AutoConfigureMockMvc
public abstract class BaseSpringBootTest {
and a class that extends it where I want to apply this practice of injecting the sessionFactory:
public class EmployeeRepositoryTest extends BaseSpringBootTest {
#Autowired
SessionFactory sessionFactory
but I am getting:
NoSuchBeanDefinitionException: No qualifying bean of type 'org.hibernate.SessionFactory' available
I also tried injecting
#Autowired
EntityManagerFactory entityManagerFactory;
and then calling:
SessionFactory sessionFactory = entityManagerFactory.unwrap(SessionFactory.class);
sessionFactory.getCurrentSession();
but this throws the following Exception:
org.hibernate.HibernateException: No CurrentSessionContext configured!
How do I get a reference to currentSession in a test, so that I can finally call:
sessionFactory.getCurrentSession().flush();
as documented in Spring Boot documentation?

Hibernate5 SessionImpl is not recognizing transaction started by JpaTransactionManager

Hibernate5 SessionImpl is not recognizing transaction started by JpaTransactionManager
I recently migrated a legacy java web application to spring-boot, Hibernate. Jersey was already used in legacy app, so continued with spring-boot-starter-jersey. So the stack is:
RestfulService(Jersey) -> Spring Service -> Hibernate DAO (legacy code).
Both Spring service and Hibernate DAOs are used through respective public interface and has #Transactional on public method of implementation class.
Hibernate DAO looks like:
#Repository
public class UserDaoImpl implements UserDao {
#Autowired
#Qualifier("entityManagerFactory")
EntityManagerFactory entityManagerFactory;
public SessionFactory getSessionFactory() {
return entityManagerFactory.unwrap(SessionFactory.class);
}
#Override
public User getUserByUserName(String userName) throws Exception {
User user = null;
Criteria criteria = getSessionFactory().getCurrentSession().createCriteria(User.class);
criteria.add(Restrictions.eq("userName", userName));
user = (User) criteria.uniqueResult();
return user;
}
}
With JpaTransactionManager I was getting exception :
javax.persistence.TransactionRequiredException: no transaction is in progress.
javax.persistence.TransactionRequiredException: no transaction is in progress
at org.hibernate.internal.SessionImpl.checkTransactionNeeded(SessionImpl.java:3505)
at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1427)
at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1423)
at org.springframework.orm.hibernate5.SessionFactoryUtils.flush(SessionFactoryUtils.java:147)
at org.springframework.orm.hibernate5.SpringSessionSynchronization.beforeCommit(SpringSessionSynchronization.java:95)
at org.springframework.transaction.support.TransactionSynchronizationUtils.triggerBeforeCommit(TransactionSynchronizationUtils.java:96)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.triggerBeforeCommit(AbstractPlatformTransactionManager.java:922)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:730)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:714)
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:532)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:304)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:98)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688)
at com.iotu.serviceImpl.UserServiceImpl$$EnhancerBySpringCGLIB$$d0c10f8c.checkLoginDetails(<generated>)
at com.iotu.webservices.UserWS.userAuthentication(UserWS.java:1467)
at com.iotu.webservices.UserWS$$FastClassBySpringCGLIB$$66e6577d.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:684)
at com.iotu.webservices.UserWS$$EnhancerBySpringCGLIB$$99ca5efe.userAuthentication(<generated>)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.glassfish.jersey.server.model.internal.ResourceMethodInvocationHandlerFactory.lambda$static$0(ResourceMethodInvocationHandlerFactory.java:76)
at org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher$1.run(AbstractJavaResourceMethodDispatcher.java:148)
at org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher.invoke(AbstractJavaResourceMethodDispatcher.java:191)
Switched to HibernateTransactionManager and Hibernate is able to recognize transaction in progress. No exception thrown.
#Configuration
#EnableJpaRepositories("com.app.repository")
public class AppConfiguration {
#Bean
public HibernateTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
SessionFactory sf = entityManagerFactory.unwrap(SessionFactory.class);
return new HibernateTransactionManager(sf);
}
But it has a down side. For newer features i am using spring data repository. With HibernateTransactionManager, the JpaRepository's save method is NOT working on entity. Its not firing the update sql (No error on console). findXX methods are working fine.
#Repository
public interface LessonCategoryRepository extends JpaRepository<LessonCategory, Long> {
public List<LessonCategory> findByCategoryAndLanguage(#Param("category") String category, #Param("language") String language);
}
When I switch to JpaTransactionManager, updates through JpaRepository are working fine but start getting the javax.persistence.TransactionRequiredException: no transaction is in progress for services calls that use legacy DAO classes..
To summarize:
With HibernateTransactionManager: Hibernate5 recognizes current transaction but JpaRepository is not persisting data.
With JpaTransactionManager: Hibernate5 session does NOT recognizes current transaction but JpaRepository is persisting data.
Can I make JpaTransactionManager to work with existing hibernate DAO classes? Or
can I make HibernateTransactionManager to work with JpaRepository? .. so that I can use legacy DAO classes as is but can continue using the JpaRepositories.

InvalidDataAccessApiUsageException: Executing an update/delete query Spring XML to Java config

I'm trying to convert spring xml configuration to java configuration. This works perfectly through XML configuration. However, it throws the following exception if I use java config initializer. This happens when it tries to run JQL. The application starts properly though (with all JPA mapping initialized).
org.springframework.dao.InvalidDataAccessApiUsageException: Executing an update/delete query; nested exception is javax.persistence.TransactionRequiredException: Executing an update/delete query
at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:410) [spring-orm-4.1.5.RELEASE.jar:4.1.5.RELEASE]
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:216) [spring-orm-4.1.5.RELEASE.jar:4.1.5.RELEASE]
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:417) [spring-orm-4.1.5.RELEASE.jar:4.1.5.RELEASE]
at org.springframework.dao.support.ChainedPersistenceExceptionTranslat
Following is my persistence initializer class. Bit of reading suggested me this is related to transactions are not being started properly. I've put debug points to each of these methods but transactionManager method never gets executed during server startup or any later time. I'm not sure what am I doing wrong :(. Same code based works perfectly when persistence is initialized through persistence.xml.
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories(basePackages = "au.mypkg")
public class DatabaseConfig {
#Bean(name = "dataSource")
#Primary
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:jboss/datasources/mydb");
}
#PersistenceContext(unitName = "persistenceUnit")
#Primary
#Bean(name = "entityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory() throws Exception {
..........
#Primary
#Bean(name = "transactionManager")
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
final JtaTransactionManager transactionManager = new JtaTransactionManager();
transactionManager.setTransactionManagerName(JBOSS_TRANSACTION_MANANGER);
return transactionManager;
}
Error occurs when accessing this method on Dao
public void updateById(final Long id) {
final String sqlQuery = "UPDATE testtable w SET w.LAST_ACCESSED = :date WHERE w.testtable_ID = :testid";
final Query query = dao.createNativeQuery(sqlQuery);
query.setParameter("date", new Date());
query.setParameter("testid", id);
query.executeUpdate();
}
I had the some problem and I resolved it by just adding #Transactional annotation on the service method that perform delete or update.
In my case it was a method that call a repository method which execute a delete by jpql like this by I think it can solve you problem too:
#Modifying
#Query("delete from OtherPayment otherPayment " +
"where otherPayment.otherPaymentParam.id = :otherPaymentParamId")
void deleteByOtherPaymentParamId(#Param("otherPaymentParamId") Long otherPaymentParamId);
Finally figured out what was going on with it. The reason this wasn't hitting the debug point was, EnableTransactionManagement imposes to auto-configure transactions for you. So if your transaction manager configured with default name which is in my case, it wouldn't try to call my method to configure transactions. The only way to get around with this is to use a different name for your transaction manager and pass that ref as a parameter on enableJPARepositories annotation.If you use one of the default names, it wouldn't make this call.
#Primary
#Bean(name = "myRealTransactionManager")
public PlatformTransactionManager transactionManager() {
final JtaTransactionManager transactionManager = new JtaTransactionManager();
transactionManager.setTransactionManagerName(JBOSS_TRANSACTION_MANANGER);
return transactionManager;
}
.. and then
#EnableJpaRepositories(basePackages = "au.mypkg", transactionManagerRef = "myRealTransactionManager"
The other problem was I have had used setdatasource as opposed to setJtaDataSource on LocalContainerEntityManagerFatoryBean.

Spring/Hibernate #Transactional on method prevents update SQL

Spring v4.2.5 Release
Hibernate v5.1.0.Final
I have a Junit test method which performs a load, updates a property and calls saveOrUpdate(bean).
It's behaving oddly in that adding #Transactional to the method signature prevents the update SQL from being performed (No SQL generated in log).
Remove the #Transactional and the update SQL is generated and the database updated.
#Configuration
#EnableTransactionManagement
#PropertySource(
{
"classpath:jdbc.properties",
"classpath:hibernate.properties"
})
#ComponentScan(value = "com.savant.test.spring.donorservice.core.dao")
public class ApplicationContext {
#Bean(destroyMethod = "close")
#Autowired
public DataSource dataSource() {
// Hikari is a connection pool manager.
HikariDataSource dataSource = new HikariDataSource();
dataSource.setUsername(env.getProperty("jdbc.username"));
dataSource.setPassword(env.getProperty("jdbc.password"));
dataSource.setJdbcUrl(env.getProperty("jdbc.url"));
dataSource.setDriverClassName(env.getProperty("jdbc.driverClassName"));
dataSource.setIsolateInternalQueries(true);
System.out.println(dataSource);
dataSource.setConnectionTestQuery("SELECT count(*) from system.onerow");
dataSource.setMaximumPoolSize(3);
dataSource.setAutoCommit(false);
return dataSource;
}
#Bean
#Autowired
public LocalSessionFactoryBean sessionFactory(DataSource datasouce) {
LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
sessionFactory.setDataSource(datasouce);
sessionFactory.setPackagesToScan(package_to_scan);
sessionFactory.setHibernateProperties(hibernateProperties());
return sessionFactory;
}
private Properties hibernateProperties() {
Properties hibernateProperties = new Properties();
hibernateProperties.put(hibernate_dialect, env.getProperty(hibernate_dialect));
hibernateProperties.put(hibernate_current_session_context_class, env.getProperty(hibernate_current_session_context_class));
hibernateProperties.put(hibernate_connection_autocommit, env.getProperty(hibernate_connection_autocommit));
hibernateProperties.put(hibernate_format_sql, env.getProperty(hibernate_format_sql));
hibernateProperties.put(hibernate_hbm2ddl_auto, env.getProperty(hibernate_hbm2ddl_auto));
hibernateProperties.put(hibernate_show_sql, env.getProperty(hibernate_show_sql));
// hibernateProperties.put(hibernate_connection_provider_class, env.getProperty(hibernate_connection_provider_class));
return hibernateProperties;
}
#Bean
#Autowired
public HibernateTransactionManager transactionManager(SessionFactory sessionFactory) {
HibernateTransactionManager txManager = new HibernateTransactionManager(sessionFactory);
return txManager;
}
The entities have been auto-generated using Netbeans 'Entity classes from Database'.
The main Entity has
A one-to-one relationship with FetchType.EAGER
A one-to-many relationship with FetchType.EAGER (it was LAZY - read below).
The test method looks like this.
#Test
#Transactional
public void c_testUpdateAddress1() {
System.out.println("findById");
String id = donorId;
Donor donor = donorDao.findById(id);
donor.setAbogrp(" O");
for (DonorAddress da : donor.getDonorAddressCollection()) {
da.setAddr1("Updated line");
System.out.println(da.getDonorAddressPK().getAddrtype() + " " + da.getAddr1());
}
System.out.println("Update");
Donor savedDonor = donorDao.save(donor);
}
Without #Transactional The update SQL is generated and the database
is updated.
With #Transactional The update SQL is not generated, does not appear
in the log. There are no exceptions, stepping over the Save method
in my Dao implementation everything appears fine. The bean passed in
has the correct values (updated field values), the bean returned has
the updated field values - just no SQL generated.
#Override
public Donor save(Donor bean) {
getSession().saveOrUpdate(bean);
return bean;
}
The reason I need #Transactional is to allow the address to the LAZY.
Without #Transactional I can't access the address as LAZY due to exception "failed to lazily initialize a collection of role: could not initialize proxy - no Session"
Which is as expected.
A transaction is started as soon as a #Transactional method is detected and committed as soon as that method call ends. Which in the case of a test is after the end of the test method. So during your tests you will not see the SQL.
Also when using #Transactional on a Spring based test it will by default do a rollback instead of a commit. See here in the reference guide on the default and how to change it.
Answer was provided by M Deinum as a comment

Resources