5

From Spring Framework Version 5.2 whenever a method is marked @Transactional(readOnly=true) I should be expecting:

  • Session.setDefaultReadOnly(true).
  • flushMode is set to MANUAL/NEVER but not COMMIT( as per this post and this answer).

However when I am trying with readOnly=true both of the above conditions are not as it is expected.

@Configuration
@EnableTransactionManagement
public class PersistenceJPAConfig {

    @Bean
    @Primary
    DataSource dataSource() throws PropertyVetoException {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost:3306/db_example");
        config.setDriverClassName(Driver.class.getName());

        config.setConnectionTestQuery("SELECT 1");
        config.setUsername("root");
        config.setPassword("root1234");



        config.addDataSourceProperty("cachePrepStmts", "true");
        config.addDataSourceProperty("prepStmtCacheSize", "250");
        config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
        config.addDataSourceProperty("useServerPrepStmts", "true");
        config.setMaximumPoolSize(11);
        return new HikariDataSource(config);
    }

    @Bean
    @Primary
    public EntityManagerFactory entityManagerFactory() throws PropertyVetoException {
        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        vendorAdapter.setGenerateDdl(true);
        vendorAdapter.setDatabasePlatform("org.hibernate.dialect.MySQL5Dialect");
        vendorAdapter.setShowSql(true);

        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(vendorAdapter);
        factory.setPackagesToScan("com.example.hibernatedemo");
        factory.setDataSource(dataSource());
        Properties properties = new Properties();
        properties.setProperty("hibernate.generate_statistics", "true");
        factory.setJpaProperties(properties);
        factory.afterPropertiesSet();

        return factory.getObject();
    }

    @Bean
    PlatformTransactionManager transactionManager() throws PropertyVetoException {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory());
        txManager.setDataSource(dataSource());
        return txManager;
    }
}

     @Service
     public class PostService {
        .
        .
        .
        @Transactional(readOnly = true)
        public List<Post> getByTitles(List<String> titles) {
            List<Post> posts = postDao.getByTitle(titles);
            System.out.println(entityManager.getFlushMode());
            Session session = (Session) entityManager.getDelegate();
            System.out.println(session.isDefaultReadOnly());
            printLoadedState(posts);
            return posts;
        }
     }

     @Repository
     public class PostDao {
        .
        .
        .
        public List<Post> getByTitle(List<String> titles) {
           Session session = (Session) entityManager.getDelegate();
           return session.createQuery("select p from Post p where title IN :titles", Post.class)
                .setParameter("titles", titles).getResultList();
        }
     }

Here are the logs:

[2019-11-25 18:37:30] [http-nio-8091-exec-2] DEBUG o.s.orm.jpa.JpaTransactionManager.getTransaction -  [  ] Creating new transaction with name [com.example.hibernatedemo.service.PostService.getByTitles]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly
[2019-11-25 18:37:30] [http-nio-8091-exec-2] DEBUG o.s.jdbc.datasource.DataSourceUtils.prepareConnectionForTransaction -  [  ] Setting JDBC Connection [HikariProxyConnection@676721780 wrapping com.mysql.cj.jdbc.ConnectionImpl@66522ead] read-only
[2019-11-25 18:37:30] [http-nio-8091-exec-2] DEBUG o.h.e.t.internal.TransactionImpl.<init> -  [  ] On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
[2019-11-25 18:37:30] [http-nio-8091-exec-2] DEBUG o.h.e.t.internal.TransactionImpl.begin -  [  ] begin
.
.
.
flushMode: COMMIT
readOnly: false
.
.
.
[2019-11-25 18:37:33] [http-nio-8091-exec-2] DEBUG o.s.orm.jpa.JpaTransactionManager.processCommit -  [  ] Initiating transaction commit
[2019-11-25 18:37:33] [http-nio-8091-exec-2] DEBUG o.s.orm.jpa.JpaTransactionManager.doCommit -  [  ] Committing JPA transaction on EntityManager [SessionImpl(1945987926<open>)]
[2019-11-25 18:37:33] [http-nio-8091-exec-2] DEBUG o.h.e.t.internal.TransactionImpl.commit -  [  ] committing
[2019-11-25 18:37:33] [http-nio-8091-exec-2] DEBUG o.s.jdbc.datasource.DataSourceUtils.resetConnectionAfterTransaction -  [  ] Resetting read-only flag of JDBC Connection [HikariProxyConnection@676721780 wrapping com.mysql.cj.jdbc.ConnectionImpl@66522ead]

pom.xml:

    <parent>
        <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

Please add comment if anymore information is needed. I may be missing out on a basic config or something. Any help is appreciated.

Mohd Waseem
  • 1,244
  • 2
  • 15
  • 36
  • Are you calling getByTitles method from the same class? – Ahmet Amasyalı Nov 25 '19 at 13:25
  • @AhmetAmasyalı i have updated the question. But why does it matter in this case? – Mohd Waseem Nov 25 '19 at 16:02
  • 1
    AhmetAmasyalı was probably thinking about "The default advice mode for processing @Transactional annotations is proxy, which allows for interception of calls through the proxy only. Local calls within the same class cannot get intercepted that way." https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html – Heuriskos Nov 25 '19 at 16:08
  • Have you tried annotation getByTitle with `@Transactional`? – Heuriskos Nov 25 '19 at 16:11
  • @AlexB In this case even if both the methods are in the same class it doesn't matter because once PostServiceProxy has been created and a transaction is started, it should considered readOnly at that moment. Have you tried annotation getByTitle with @Transactional? I tried, it didn't work. as I said while starting the txn in Proxy my readOnly property should have been processed. – Mohd Waseem Nov 25 '19 at 16:22
  • Does the code you have posted match what you are running, as I note you have `@Service` on PostDao when it should be `@Repository` if you were following the article. Have you also got a `@EnableTransactionManagement` annotation? – Heuriskos Nov 25 '19 at 16:28
  • How you're calling it does matter. If a session is already open as read-write the transactional method won't attempt to "escalate" it to readOnly for just that query (in my experience...). For the flushmode you should be looking at session..get*Hibernate*FlushMode() – Affe Nov 25 '19 at 16:38
  • @AlexB updated postDao, added Config class, – Mohd Waseem Nov 25 '19 at 16:48
  • You're still not really posting a minimum working example. You're omitting a lot of code in between your configuration methods. If you're using Spring Boot, you should also be able to delete most of those methods and just have the annotation `@SpringBootApplication`. If you make sure your application conforms as close as possible to a generic boot application, it is easier to flush out any bugs. – Heuriskos Nov 25 '19 at 16:52
  • @AlexB i have added my full config file. i had a lot of comment which i was removing in order to post a cleaner file to undertsand. – Mohd Waseem Nov 25 '19 at 16:56

1 Answers1

-1

Well, I look at the source codes one more time. It should work if HibernateJpaDialect is set explicitly to JpaTransactionManager (which you already did through setting HibernateJpaVendorAdapter to LocalContainerEntityManagerFactoryBean) and after this fix which is released at 5.1 RC1.

Is it possible that the @Transactional method that you test is somehow called by another @Transactional method of some bean and that outer @Transactional method is read-only=false ? Because the setting of outer @Transactional method will simply override the inner one. (Assume the inner one does not the set as PROPAGATION_REQUIRES_NEW).

On the other hand , you can also try to change the @Transactional method that you test to PROPAGATION_REQUIRES_NEW to see if readOnly = true can take effect.

Ken Chan
  • 84,777
  • 26
  • 143
  • 172
  • Neither a transaction manager type, nor a hibernate version have to do with the issue here. The problem is with Spring itself, which, at least for Hibernate, doesn't provide the optimizations, which could be provided for readonly=true case. This article describes the issue in details: https://vladmihalcea.com/spring-read-only-transaction-hibernate-optimization/ – Exterminator13 Nov 25 '19 at 19:50
  • @ Exterminator13 Wrong, the problem in your quoted link is already solved and rolled into [HibernateTransactionManager](https://github.com/spring-projects/spring-framework/issues/21494#issuecomment-453473546).It can also be verified if you look at the [`HibernateTransactionManager` source code](https://github.com/spring-projects/spring-framework/blob/2d86f221ce9e4df99aec801ae226ed228f5b64ac/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateTransactionManager.java#L512) So , it is related to the transaction manager type – Ken Chan Nov 25 '19 at 19:50
  • As I remember, for Hibernate, JpaTransactionManager uses HibernateTransactionManager under the hood. Given that, I don't think, that a transaction manager is the cause of the issue. – Exterminator13 Nov 25 '19 at 19:51
  • @Exterminator13 , "JpaTransactionManager uses HibernateTransactionManager under the hood" , another wrong statement from you . `JpaTransactionManager` never use `HibernateTransactionManager` , I kindly suggest you to refresh your memory by looking at the source code to find the truth first... – Ken Chan Nov 25 '19 at 19:54
  • KenChan By switching to HibernateTransactionManager and LocalSessionFactoryBean my @Transactional is breaking with exception. (ava.lang.ClassCastException: org.springframework.orm.jpa.EntityManagerHolder cannot be cast to org.springframework.orm.hibernate5.SessionHolder). I guess, I need to explore native HIbernate beans working first. But i wonder why do we need to setup hibernate native beans when solution was simple, if I am doing session.setDefaultReadOnly(true) , I am able to achieve the desired result. – Mohd Waseem Nov 26 '19 at 16:18
  • @MohdWaseem I look at the source codes one more time and `JpaTransactionManager` should also work if `HibernateJpaVendorAdapter` is set explicitly to the `LocalContainerEntityManagerFactoryBean` which you already do in your setup. Anyway , please see my updated answer. – Ken Chan Nov 27 '19 at 16:10
  • I am not calling from another @Transactional method. I tried REQUIRES_NEW but didn't work. https://github.com/mwaseem-mnnit/hibernate-demo/tree/master/complete here is link to the repo. in PostController i have getByTitle function "post/get/title" if this helps. – Mohd Waseem Dec 02 '19 at 18:09