Spring @Transactional not applied for GORM methods in java

233 Views Asked by At

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?

3

There are 3 best solutions below

0
injecteer On BEST ANSWER

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
2
Python Dev On

The problem is that the @Transaction annotation works due to the proxy created by Spring, but when Spring invokes the @PostConstruct method, the proxies have not been created yet. You can check Spring bean lifecycle cheatseet, note that first @PostConstro methods are called and then BeanProstProcessors after initialisation create proxies.

4
Anish B. On

Firstly, @PostConstruct annotation will not work with @Transactional annotation because the @PostConstruct is called before the proxy objects are created for that instance (or class). In your case, there is no Hibernate Session proxy object created. So, the error is obvious and @Transactional won't work if there is no Session object.