0

I have a typical Spring Boot (2.2.5.RELEASE) + Hibernate application. When trying to save an entity I get the following exception:

Caused by: org.hibernate.PersistentObjectException: detached entity passed to persist: org.myorg.ConfigurationInfoEntry
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:127)
at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:828)
at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:795)
at org.hibernate.engine.spi.CascadingActions$7.cascade(CascadingActions.java:298)
at org.hibernate.engine.internal.Cascade.cascadeToOne(Cascade.java:490)
at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:415)
at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:216)
at org.hibernate.engine.internal.Cascade.cascade(Cascade.java:149)
at org.hibernate.event.internal.AbstractSaveEventListener.cascadeBeforeSave(AbstractSaveEventListener.java:428)
at org.hibernate.event.internal.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:266)
at org.hibernate.event.internal.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:196)
at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:139)
at org.hibernate.event.internal.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:192)
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:135)
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:62)
at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:804)
at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:789)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:314)
at com.sun.proxy.$Proxy185.persist(Unknown Source)
at org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:554)
at org.springframework.data.jpa.repository.support.SimpleJpaRepository.saveAndFlush(SimpleJpaRepository.java:569)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:371)
at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:204)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:657)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:621)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:605)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:80)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:366)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:99)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:139)
... 95 common frames omitted

Trying to save is done in the following method (configurationEntryRepository is a JpaRepository):

@Transactional(rollbackFor = Throwable.class)
public void saveConfiguration(IConfigurationType configurationType) {
    final ConfigurationEntry entry = configurationEntryRepository.findByConfigurationType(configurationType);

    final ConfigurationEntry configurationEntry = new ConfigurationEntry(entry.getInfoEntry());
    configurationEntryRepository.saveAndFlush(configurationEntry);
}

The ConfigurationEntry has a Many-To-One relationsship to a ConfigurationInfoEntry:

@Entity
public final class ConfigurationEntry {
    @JoinColumn(name = "INFO_ENTRY")
    @ManyToOne(cascade = CascadeType.ALL)
    private ConfigurationInfoEntry infoEntry;
}

Given that I fetch the InfoEntry from the database before I don't get why the entity is detached. As you can see, everything should be running within one transaction, as the method saveConfiguration is appropriatly annotated.

However, when I change ConfigurationEntry to cascade without persist, i.e. changing to:

@Entity
public final class ConfigurationEntry {
    @JoinColumn(name = "INFO_ENTRY")
    @ManyToOne(cascade = { CascadeType.MERGE, CascadeType.REMOVE, CascadeType.DETACH, CascadeType.REFRESH } )
    private ConfigurationInfoEntry infoEntry;
}

everything works as expected. This leads me to believe that the JpaRepository uses two different transactions between the read call and the saveAndFlush() operation, leading to a detached entity after the read. However, given that the whole method is annotated with transactional, I don't really get why.

Here is the configuration for my data source and JPA related beans:

@Bean(value = "datasource")
public DataSourceFactoryBeanHikari dataSourceFactoryBeanHikari() {
    DataSourceFactoryBeanHikari dataSourceFactoryBeanHikari = new DataSourceFactoryBeanHikari();
    dataSourceFactoryBeanHikari.setMaxPoolSize(30);
    return dataSourceFactoryBeanHikari;
}

@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
    JpaTransactionManager txManager = new JpaTransactionManager();
    txManager.setEntityManagerFactory(entityManagerFactory);
    return txManager;
}

@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
    return new JdbcTemplate(dataSource);
}

@Bean("entityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
    final LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
    em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
    em.setPackagesToScan("org.myorg");

    final Map<String, Object> properties = new HashMap<>();
    properties.put(AvailableSettings.DIALECT, "org.hibernate.dialect.SQLServerDialect");
    properties.put(AvailableSettings.URL, "jdbc:sqlserver://localhost:1433");
    properties.put(AvailableSettings.SHOW_SQL, true);
    properties.put(AvailableSettings.FORMAT_SQL, true);
    properties.put(AvailableSettings.HBM2DDL_AUTO, "none");
    em.setJpaPropertyMap(properties);
    em.setDataSource(dataSource);
    em.afterPropertiesSet();

    return em;
}
dirkk
  • 6,160
  • 5
  • 33
  • 51
  • Is this uni or bi-directional relation? – Lesiak May 07 '20 at 08:34
  • @Lesiak uni-directional, i.e. ConfigurationInfoEntry has no reference to ConfigurationEntry – dirkk May 07 '20 at 09:07
  • Check if you have transaction aspect-managed TransactionStatus in scope: ` System.out.println(TransactionAspectSupport.currentTransactionStatus());`. It throws exception if there is no active transaction. Have you enabled annotation-based transaction management? – Lesiak May 07 '20 at 11:07
  • @Lesiak No, there is no active transaction (throws NoTransactionException: No transaction aspect-managed TransactionStatus in scope). But annotation-based transaction management should be enable, first because that should be the Spring Boot default and also when I excplictely enable it using "@EnableTransactionManagement" in my application – dirkk May 08 '20 at 11:53
  • Indeed, it is auto-configured. Any chance you are calling this method from other method in the same class? – Lesiak May 08 '20 at 12:25
  • Indeed I do and that's it! I didn't know this could be an issue (for reference: Here is explained why: https://stackoverflow.com/questions/3423972/spring-transaction-method-call-by-the-method-within-the-same-class-does-not-wo). I now annotated my calling methods with transactional and it works as expected. However, I would have expected it to works also simply with "@EnableTransactionManagement(mode= AdviceMode.ASPECTJ)", but this does nothing for me. – dirkk May 08 '20 at 20:13
  • btw, if you want to provide an answer I am happy to accept it as it solves my issue – dirkk May 08 '20 at 20:14
  • I think it takes more to enable AspectJ. See javadoc: Note also that in this case the spring-aspects module JAR must be present on the classpath, with compile-time weaving or load-time weaving applying the aspect to the affected classes – Lesiak May 08 '20 at 20:51

1 Answers1

2

Summary of the discussion in thread:

Checked if there is an active aspect-managed transaction

System.out.println(
  TransactionAspectSupport.currentTransactionStatus()
);

It turned out that there is none. The code above threw

NoTransactionException: No transaction aspect-managed TransactionStatus in scope

Checked the reasons of @Transactional not being observed

As @EnableTransactionManagement is auto-configured by Spring Boot, the next possibility was incorrect use of Spring AOP proxies.

Spring AOP wraps the object, and the methods are not intercepted when called from the same instance.

This was indeed the case.

See Spring @Transaction method call by the method within the same class, does not work?

Lesiak
  • 22,088
  • 2
  • 41
  • 65