Hibernate 6 exception: 'Caused by: org.hibernate.PersistentObjectException: detached entity passed to persist'

Hello everyone,

I’m currently switching from hibernate 5 to hibernate 6.2 and spring boot 3, and I’m facing an issue with hibernate 6 - FYI, the code I’ll provide was working with the old version.

I have the following entities:

public class Tour {
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Id
    @Positive
    private Integer id;

    @ManyToOne(fetch = FetchType.EAGER)
    private Location primaryLocation;
}

public class Location implements PreviousState {
    @Id
    @Positive
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @NotNull
    @Column(nullable = false)
    private String name;

    private transient LocationDTO previousState;

    @PostLoad
    @Override
    public void setPreviousState() {
        var mapper = SpringContext.getBean(LocationMapper.class);
        previousState = mapper.toBasicDTO(this);
    }
}

public class LocationToTag {
    @Id
    @Positive
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @NotNull
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "location_id", nullable = false)
    private Location location;

    @NotNull
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "tag_id", nullable = false)
    private Tag tag;
}

public class Tag {
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Id
    private Integer id;

    @Column(length = 50, nullable = false)
    @Length(min = 1, max = 50)
    @NotNull
    private String name;
}

// PreviousState is just an interface here.
public interface PreviousState {
    void setPreviousState();

    Object getPreviousState();
}

// This only a wrapper over the @Transactional annotation with pre-set values.
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(value = "managementPostgresTransactionManager", propagation = Propagation.REQUIRED)
public @interface ManagementPostgresTransaction {
}

// This is the class for setting the configuration of the management datasource.
@Configuration
@EnableTransactionManagement
public class ManagementPostgresDbConfig {
    @Bean
    @ConfigurationProperties(prefix = "spring.managementpostgresdatasource.hikari")
    @Primary
    public HikariConfig managementReadWriteHikariConfig() {
        return new HikariConfig();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.managementpostgresdatasource.read-only.hikari")
    public HikariConfig managementReadOnlyHikariConfig() {
        return new HikariConfig();
    }

    @Bean
    @Primary
    public DataSource managementPostgresDataSource() {
        TransactionRoutingDataSource routingDataSource = new TransactionRoutingDataSource();
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put(
            TransactionRoutingDataSource.DataSourceType.READ_WRITE,
            new HikariDataSource(managementReadWriteHikariConfig())
        );
        dataSourceMap.put(
            TransactionRoutingDataSource.DataSourceType.READ_ONLY,
            new HikariDataSource(managementReadOnlyHikariConfig())
        );

        routingDataSource.setTargetDataSources(dataSourceMap);
        return routingDataSource;
    }

    @Bean
    @Primary
    public DatabaseSchemaValidator managementPostgresSchemaValidator(
        @Qualifier("managementPostgresDataSource") DataSource managementDataSource
    ) {
        return new DatabaseSchemaValidator(managementDataSource);
    }

    @Bean
    @Primary
    public LocalContainerEntityManagerFactoryBean managementPostgresEntityManager(CommonDatabaseConfig dbConfig) {
        var dataSource = managementPostgresDataSource();
        var entityManagerFactory = new LocalContainerEntityManagerFactoryBean();
        entityManagerFactory.setDataSource(dataSource);
        entityManagerFactory.setPersistenceUnitName("managementPostgres");
        entityManagerFactory.setPackagesToScan("com.models");

        entityManagerFactory.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        var jpaPropertyMap = dbConfig.getNewJpaPropertyMap();
        jpaPropertyMap.put(AvailableSettings.DIALECT, PostgreSQLDialect.class);
        jpaPropertyMap.put(
            "hibernate.integrator_provider",
            (IntegratorProvider) () -> List.of(managementPostgresSchemaValidator(dataSource))
        );
        // This is required for TransactionRoutingDataSource to work
        jpaPropertyMap.put(AvailableSettings.CONNECTION_PROVIDER_DISABLES_AUTOCOMMIT, true);

        // The following will delay getting connections until after TransactionSynchronizationManager is ready
        // More information can be found here: https://stackoverflow.com/a/68078228/8473419
        jpaPropertyMap.put(
            AvailableSettings.CONNECTION_HANDLING,
            PhysicalConnectionHandlingMode.DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION
        );
        entityManagerFactory.setJpaPropertyMap(jpaPropertyMap);
        return entityManagerFactory;
    }

    @Bean
    @Primary
    public PlatformTransactionManager managementPostgresTransactionManager(
        @Qualifier("managementPostgresEntityManager") EntityManagerFactory entityManagerFactory
    ) {
        return new JpaTransactionManager(entityManagerFactory);
    }

    @Bean
    @Primary
    public TransactionTemplate managementPostgresTransactionTemplate(
        @Qualifier("managementPostgresTransactionManager") PlatformTransactionManager transactionManager
    ) {
        return new TransactionTemplate(transactionManager);
    }
}

The mappers here just maps between the class and it’s DTO, nothing more.

The issues that I’m facing now is when I request the DB using the jpa repository to get all the tours by specific ids tours will be fetched with location eager loaded. Now, after the eager loading of the location, the setPreviousState method which is called at @PostLoad will be triggered, now in the location mapper, there a method to fetch the locationToTags related to the location and attach them to the DTO, the call to this method that return the locationToTags by location throws the following exception: Caused by: org.hibernate.PersistentObjectException: detached entity passed to persist: Tour

Notes:

  1. This code was working with hibernate 5.
  2. I don’t save anything anywhere, I’m only fetching data.

This is how the fetch is happening:

@Repository
public interface TourRepository extends JpaRepository<Tour, Integer> {
    List<Tour> findAllByIdIn(Collection<Integer> ids);
}
public class TourService {
    private final TourRepository tourRepository;

    @ManagementPostgresTransaction
    @SuppressWarnings("unchecked")
    public void rebuildTourFromLocationIds(List<? extends Integer> ids) {
        var tourIds = (List<Integer>) ids;
        var tours = tourRepository.findAllByIdIn(tourIds);
    }
}

public interface LocationMapper {

    public LocationDTO toDto(Location location) {
        ... mapping 
        var tourToTagService = SpringContext.getBean(LocationToTagService.class);
        var tagsByType = tourToTagService.getTagListAsDTO(location).stream()
            .collect(Collectors.groupingBy(TagDTO::getType));
        return dto
    }
}

public class LocationToTagService {
    private final LocationToTagRepository locationToTagRepository;

  @ManagementPostgresTransaction
    public List<Tag> getTagList(Location location) {
        // This is the line that is throwing the exception.
        var ltts = locationToTagRepository.findAllByLocationId(location.getId());
        return ltts.stream()
            .map(LocationToTag::getTag)
            .collect(Collectors.toList());
    }

 @ManagementPostgresTransaction
    public List<TagDTO> getTagListAsDTO(Location location) {
        return getTagList(location).stream().map(tagMapper::toDTO).collect(Collectors.toList());
    }
}

Stacktrace:

org.springframework.dao.InvalidDataAccessApiUsageException: detached entity passed to persist: Tour
	at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:289)
	at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:229)
	at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:550)
	at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
	at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242)
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:152)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
	at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:134)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223)
	at jdk.proxy3/jdk.proxy3.$Proxy324.findAllByLocation(Unknown Source)
	at LocationToTagService.getTagList_aroundBody2(LocationToTagService.java:50)
	at LocationToTagService$AjcClosure3.run(LocationToTagService.java:1)
	at .LocationToTagService.getTagList(LocationToTagService.java:50)
	at .LocationToTagService.getTagListAsDTO_aroundBody6(LocationToTagService.java:75)
	at .LocationToTagService$AjcClosure7.run(LocationToTagService.java:1)
	at LocationToTagService.getTagListAsDTO(LocationToTagService.java:75)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:343)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
	at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:391)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:702)
	a LocationToTagService$$SpringCGLIB$$0.getTagListAsDTO(<generated>)
	at LocationMapper.toDto_aroundBody0(LocationMapper.java:97)
	at LocationMapper$AjcClosure1.run(LocationMapper.java:1)
	at .LocationMapper.toDto(LocationMapper.java:96)
	at .LocationMapperImpl.toDTO(LocationMapperImpl.java:248)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:343)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:698)
	at LocationMapperImpl$$SpringCGLIB$$0.ttDTO(<generated>)
	at .Location.setPreviousState(Location.java:231)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at org.hibernate.jpa.event.internal.EntityCallback.performCallback(EntityCallback.java:50)
	at org.hibernate.jpa.event.internal.CallbackRegistryImpl.callback(CallbackRegistryImpl.java:113)
	at org.hibernate.jpa.event.internal.CallbackRegistryImpl.postLoad(CallbackRegistryImpl.java:96)
	at org.hibernate.event.internal.DefaultPostLoadEventListener.onPostLoad(DefaultPostLoadEventListener.java:44)
	at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:127)
	at org.hibernate.sql.results.jdbc.internal.JdbcValuesSourceProcessingStateStandardImpl.lambda$postLoad$0(JdbcValuesSourceProcessingStateStandardImpl.java:182)
	at java.base/java.util.HashMap.forEach(HashMap.java:1421)
	at org.hibernate.sql.results.jdbc.internal.JdbcValuesSourceProcessingStateStandardImpl.postLoad(JdbcValuesSourceProcessingStateStandardImpl.java:175)
	at org.hibernate.sql.results.jdbc.internal.JdbcValuesSourceProcessingStateStandardImpl.finishUp(JdbcValuesSourceProcessingStateStandardImpl.java:164)
	at org.hibernate.sql.results.spi.ListResultsConsumer.consume(ListResultsConsumer.java:206)
	at org.hibernate.sql.results.spi.ListResultsConsumer.consume(ListResultsConsumer.java:33)
	at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.doExecuteQuery(JdbcSelectExecutorStandardImpl.java:362)
	at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.executeQuery(JdbcSelectExecutorStandardImpl.java:168)
	at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.list(JdbcSelectExecutorStandardImpl.java:93)
	at org.hibernate.sql.exec.spi.JdbcSelectExecutor.list(JdbcSelectExecutor.java:31)
	at org.hibernate.loader.ast.internal.SingleIdLoadPlan.load(SingleIdLoadPlan.java:145)
	at org.hibernate.loader.ast.internal.SingleIdLoadPlan.load(SingleIdLoadPlan.java:117)
	at org.hibernate.loader.ast.internal.SingleIdEntityLoaderStandardImpl.load(SingleIdEntityLoaderStandardImpl.java:72)
	at org.hibernate.persister.entity.AbstractEntityPersister.doLoad(AbstractEntityPersister.java:3381)
	at org.hibernate.persister.entity.AbstractEntityPersister.load(AbstractEntityPersister.java:3371)
	at org.hibernate.event.internal.DefaultLoadEventListener.loadFromDatasource(DefaultLoadEventListener.java:602)
	at org.hibernate.event.internal.DefaultLoadEventListener.loadFromCacheOrDatasource(DefaultLoadEventListener.java:588)
	at org.hibernate.event.internal.DefaultLoadEventListener.load(DefaultLoadEventListener.java:557)
	at org.hibernate.event.internal.DefaultLoadEventListener.doLoad(DefaultLoadEventListener.java:550)
	at org.hibernate.event.internal.DefaultLoadEventListener.load(DefaultLoadEventListener.java:202)
	at org.hibernate.event.internal.DefaultLoadEventListener.proxyImplementation(DefaultLoadEventListener.java:402)
	at org.hibernate.event.internal.DefaultLoadEventListener.narrowedProxy(DefaultLoadEventListener.java:395)
	at org.hibernate.event.internal.DefaultLoadEventListener.loadWithRegularProxy(DefaultLoadEventListener.java:274)
	at org.hibernate.event.internal.DefaultLoadEventListener.proxyOrLoad(DefaultLoadEventListener.java:237)
	at org.hibernate.event.internal.DefaultLoadEventListener.doOnLoad(DefaultLoadEventListener.java:106)
	at org.hibernate.event.internal.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:78)
	at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:138)
	at org.hibernate.internal.SessionImpl.fireLoadNoChecks(SessionImpl.java:1231)
	at org.hibernate.internal.SessionImpl.internalLoad(SessionImpl.java:1075)
	at org.hibernate.sql.results.graph.entity.internal.EntitySelectFetchInitializer.initializeInstance(EntitySelectFetchInitializer.java:185)
	at org.hibernate.sql.results.internal.InitializersList.initializeInstance(InitializersList.java:70)
	at org.hibernate.sql.results.internal.StandardRowReader.coordinateInitializers(StandardRowReader.java:111)
	at org.hibernate.sql.results.internal.StandardRowReader.readRow(StandardRowReader.java:87)
	at org.hibernate.sql.results.spi.ListResultsConsumer.consume(ListResultsConsumer.java:199)
	at org.hibernate.sql.results.spi.ListResultsConsumer.consume(ListResultsConsumer.java:33)
	at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.doExecuteQuery(JdbcSelectExecutorStandardImpl.java:362)
	at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.executeQuery(JdbcSelectExecutorStandardImpl.java:168)
	at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.list(JdbcSelectExecutorStandardImpl.java:93)
	at org.hibernate.sql.exec.spi.JdbcSelectExecutor.list(JdbcSelectExecutor.java:31)
	at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.lambda$new$0(ConcreteSqmSelectQueryPlan.java:109)
	at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.withCacheableSqmInterpretation(ConcreteSqmSelectQueryPlan.java:302)
	at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.performList(ConcreteSqmSelectQueryPlan.java:243)
	at org.hibernate.query.sqm.internal.QuerySqmImpl.doList(QuerySqmImpl.java:518)
	at org.hibernate.query.spi.AbstractSelectionQuery.list(AbstractSelectionQuery.java:367)
	at org.hibernate.query.Query.getResultList(Query.java:119)
	at org.springframework.data.jpa.repository.query.JpaQueryExecution$CollectionExecution.doExecute(JpaQueryExecution.java:129)
	at org.springframework.data.jpa.repository.query.JpaQueryExecution.execute(JpaQueryExecution.java:92)
	at org.springframework.data.jpa.repository.query.AbstractJpaQuery.doExecute(AbstractJpaQuery.java:148)
	at org.springframework.data.jpa.repository.query.AbstractJpaQuery.execute(AbstractJpaQuery.java:136)
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:136)
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:120)
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:164)
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:143)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
	at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:77)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
	at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:391)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
	at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:134)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223)
	at jdk.proxy3/jdk.proxy3.$Proxy295.findAllByIdIn(Unknown Source)
	at TourFromLocationService.rebuildTourFromLocationIds_aroundBody0(TourFromLocationService.java:35)
	at TourFromLocationService$AjcClosure1.run(TourFromLocationService.java:1)

Note that whenever you load something, Hibernate checks if dirty entities in the persistence context need to be flushed, so I guess there is some interplay here between this auto-flush which happens while the first load is not fully finished yet.
Please create an issue in the issue tracker(https://hibernate.atlassian.net) with a test case(hibernate-test-case-templates/JPAUnitTestCase.java at main · hibernate/hibernate-test-case-templates · GitHub) that reproduces the issue.

I can’t find the code that could be marking entity as dirty, the only change that I do is over the transient property, which hibernate should not be tracking, right?

I don’t know your full entity model and until I can run a test case that reproduces the situation everything is just an assumption. Just wanted to give you some context, but ultimately I’ll need a reproducer to understand this.

Got it! I’ll write a testing case and create a ticket with it, thank you for your response!!

1 Like