javaspringspring-boothibernategrails-orm

Spring @Transactional not applied for GORM methods in java


In my Spring-Boot app I'm using GORM via:

implementation 'org.grails:gorm-hibernate5-spring-boot:8.0.3'

All domain classes are annotated with GORM @Entity and are initialized with new HibernateDatastore( cfg, classes ) and work like a charm.

I also created a delegating helper class to call dynamic GORM methods from java:

class JavaGORMHelper {
  
  static <T extends GormEntity> void withTransaction( Class<T> clazz, Consumer<TransactionStatus> action ) {
    clazz.withTransaction{ action it }
  }
  
  static <T extends GormEntity> T findBy( Class<T> clazz, String what, Object... args ) {
    clazz."findBy$what"( *args )
  }

  // other 40 methods
}

Now I want to call those GORM methods from java service and for that I would use Spring's @Transactional:

import org.springframework.transaction.annotation.Transactional;

@Service
public class JobService {

  @PostConstruct
  @Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW)
  public void init() {  
    JavaGORMHelper.list(Job.class).forEach(job -> foo( job ));
  }
}

The code is throwing a No Session found for current thread exception:

[main] 16:23:37.303 ERROR o.s.boot.SpringApplication - Application run failed
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'jobController': Unsatisfied dependency expressed through field 'jobService'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jobService': Invocation of init method failed; nested exception is org.springframework.dao.DataAccessResourceFailureException: Could not obtain current Hibernate Session; nested exception is org.hibernate.HibernateException: No Session found for current thread
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:659)
    ... 20++ common frames omitted
Caused by: org.springframework.dao.DataAccessResourceFailureException: Could not obtain current Hibernate Session; nested exception is org.hibernate.HibernateException: No Session found for current thread
    at org.grails.orm.hibernate.GrailsHibernateTemplate.getSession(GrailsHibernateTemplate.java:335)
    at org.grails.orm.hibernate.GrailsHibernateTemplate.doExecute(GrailsHibernateTemplate.java:284)
    at org.grails.orm.hibernate.GrailsHibernateTemplate.execute(GrailsHibernateTemplate.java:241)
    at org.grails.orm.hibernate.GrailsHibernateTemplate.execute(GrailsHibernateTemplate.java:120)
    ... 32++ common frames omitted
Caused by: org.hibernate.HibernateException: No Session found for current thread
    at org.grails.orm.hibernate.GrailsSessionContext.currentSession(GrailsSessionContext.java:112)
    at org.hibernate.internal.SessionFactoryImpl.getCurrentSession(SessionFactoryImpl.java:508)
    at org.grails.orm.hibernate.GrailsHibernateTemplate.getSession(GrailsHibernateTemplate.java:333)
    ... 56 common frames omitted

I tried exposing transactionManager and sessionFactory from HibernateDatastore as spring beans with no success.

If I wrap the method call into JavaGORMHelper.withTransaction(Job.class, tx -> {..}); it works fine.

Also it works fine if I convert the java service to groovy and use @grails.gorm.transactions.Transactional.

How to make the @Transactional work for GORM invocations from java? What am I missing?


Solution

  • It turns out, that no additional configuration steps for the domain classes are needed.

    If the domain classes are located within the project, they are found and initialized automatically and the @Transactional also works fine:

    
    @SpringBootApplication(exclude = HibernateJpaAutoConfiguration.class)
    @RestController
    public class Application {
    
      final Random RND = new Random();
      
      public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
      }
      
      @EventListener(ApplicationReadyEvent.class)
      @Transactional
      public void init() {  
        System.out.println( "Count before = " + Person.cnt() );
        
        Person p1 = new Person();
        p1.setName("aaa");
        p1.setAge(11);
        p1.save();
        
        System.out.println( "Count after = " + Person.cnt() );
      }  
    }
    

    There are some gotchas on the way though...

    1st, the JPA-auto-configurer clashes with the counterpart of hibernate, hence the exclusion of the former inside @SpringBootApplication.

    2nd, if the groovy and java classes reside in the same project and joint compilation is used, the domain class implementing GormEntity:

    @Entity
    class Person implements GormEntity<Person> {
      long id
      String name
      int age
    }
    

    fails with CompileException like:

    > Task :compileGroovy
    C:\workspace\gorm-spring-repro\build\tmp\compileGroovy\groovy-java-stubs\gorm\spring\repro\Person.java:9: error: Person is not abstract and does not override abstract method addTo(String,Object) in Person
    @grails.gorm.annotation.Entity() public class Person
                                            ^
    1 error
    

    To work the problem around I moved the domain classes into a separate sub-project and added the following static delegating method to the domain class, as Java doesn't see the trait static methods from GormStaticApi:

    @Entity
    class Person implements GormEntity<Person> {
      ...
      static int cnt() {
        count()
      }
    }
    

    Then upon starting the setup works like charm:

    2024-02-15 12:10:49.044  INFO 26048 --- [           main] gorm.spring.repro.Application            : Started Application in 3.876 seconds (JVM running for 4.251)
    Hibernate: select count(person0_.id) as col_0_0_ from person person0_ limit ?
    Count before = 0
    Hibernate: insert into person (id, version, age, name) values (default, ?, ?, ?)
    Hibernate: insert into person (id, version, age, name) values (default, ?, ?, ?)
    Hibernate: select count(person0_.id) as col_0_0_ from person person0_ limit ?
    Count after = 2