javaspringspring-boothibernatejpa

Hibernate StaleObjectStateException After Upgrading to 6.6.3: Row was updated or deleted by another transaction or unsaved-value mapping was incorrect


I have two PostgreSQL tables: one for Product and another for gallery images with SEO details. Here are the schemas:

create table if not exists test_product
(
    id      bigserial   not null,
    version bigserial   not null,
    name    text        not null,
    sku     varchar(50) not null,
    primary key (id)
);

create table if not exists test_product_image_gallery_seo
(
    id         bigserial not null,
    version    bigserial not null,
    product_id bigint    not null,
    image      text      not null,
    alt_text   text      not null,
    primary key (id),
    constraint fk_product_id foreign key (product_id) references test_product (id),
    constraint test_unique_seo unique (product_id, image)
);

In my Spring Boot application, I have two Hibernate ORM entities representing these tables. Below are their definitions:

TestProduct


@Entity(name = "test_product")
@Table(name = "test_product")
public class TestProduct extends BehemothORM {

    @Column(
        name = ProductContract.NAME,
        nullable = false,
        unique = true,
        columnDefinition = "TEXT"
    )
    private String name;

    @Column(
        name = ProductContract.SKU,
        nullable = false,
        unique = true,
        columnDefinition = "VARCHAR"
    )
    private String sku;

    @OneToMany(
        mappedBy = "product",
        fetch = FetchType.EAGER,
        targetEntity = TestProductImageGallerySEO.class,
        cascade = CascadeType.ALL,
        orphanRemoval = true
    )
    private List<TestProductImageGallerySEO> imageGallerySEOList;
}

TestProductGalleryImageSEO

@Entity(name = "test_product_gallery_image_seo")
@Table(
    name = "test_product_gallery_image_seo",
    uniqueConstraints = {
        @UniqueConstraint(
            name = "test_unique_seo",
            columnNames = {
                ProductImageGallerySEOContract.PRODUCT_ID,
                ProductImageGallerySEOContract.IMAGE
            }
        )
    }
)
public class TestProductImageGallerySEO extends BehemothORM {

    @Column(
        name = "image", 
        columnDefinition = "TEXT", 
        nullable = false
    )
    private String image;

    @Column(
        name = "alt_text", 
        columnDefinition = "TEXT", 
        nullable = false
    )
    private String altText;

    @ToString.Exclude
    @ManyToOne(
        targetEntity = TestProduct.class,
        fetch = FetchType.EAGER
    )
    @JoinColumn(
        name = ProductImageGallerySEOContract.PRODUCT_ID,
        columnDefinition = "BIGSERIAL",
        nullable = false,
        foreignKey = @ForeignKey(
            name = "fk_product_id"
        )
    )
    @GsonExclude
    private TestProduct product;
    
    @Transient
    private boolean deleted = false;
}

Here's the logic where I add or update gallery images for a product:

 @Transactional()
    public int modifyGalleryImages(@NotNull TestProductImageGallerySEOPayload seoPayload) {

        // gets the current product as entity from database
        TestProduct product = this.testProductDAOController.retrieveEntity(seoPayload.getProductId());

        for (TestProductImageGallerySEO imageGallerySEO : seoPayload.getGallerySEOList()) 
        {
            imageGallerySEO.setProduct(product);
            this.addNewEntity(imageGallerySEO);
        }

        return ActionCode.UPDATE_SUCCESS;
    }

This code was working previously. However, after upgrading Hibernate versions this exact code is failing without any changes made.

After upgrading Hibernate from version 6.3.1 to 6.6.3, I'm getting the following error:

org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.bloomscorp.loom.seo.orm.TestProductImageGallerySEO#0]

This error occurs when trying to save a new TestProductImageGallerySEO entity using addNewEntity. Here is the seoPayload

{
    "productId": 37503,
    "gallerySEOList": [
        {
            "id": 0,
            "version": 0,
            "image": "uvw.jpg",
            "altText": "",
            "deleted": false
        },
        {
            "id": 0,
            "version": 0,
            "image": "https://abc.jpg",
            "altText": "",
            "deleted": false
        },
        {
            "id": 0,
            "version": 0,
            "image": "https://xyz.jpg",
            "altText": "",
            "deleted": false
        }
    ]
}

Additional Context:

if anybody wants to know what BehemothORM and addNewEntity does, these are basically our own set of libraries that we use to speed up the development process. Here is the defination of these two

BehemothORM BehemothORM is a base class with common fields like id and version

addNewEntity addNewEntity is a wrapper around JpaRepository.save()

Questions:

  1. Why does this error occur now after upgrading Hibernate?
  2. How can I fix this error while retaining the current logic?

Thanks in advance!

Edit

As requested by @M.Deinum here is the complete stacktrace

jakarta.servlet.ServletException: Request processing failed: org.springframework.orm.ObjectOptimisticLockingFailureException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.bloomscorp.loom.seo.orm.TestProductImageGallerySEO#0]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1022)
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914)
    at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)
    at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
    at com.bloomscorp.nverse.NVerseRequestFilter.doFilterInternal(NVerseRequestFilter.java:53)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
    at com.bloomscorp.nverse.NVerseExceptionHandlerFilter.doFilterInternal(NVerseExceptionHandlerFilter.java:58)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
    at com.bloomscorp.nverse.NVerseHttpRequestFilter.doFilterInternal(NVerseHttpRequestFilter.java:20)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
    at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:108)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:219)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191)
    at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113)
    at org.springframework.web.servlet.handler.HandlerMappingIntrospector.lambda$createCacheFilter$3(HandlerMappingIntrospector.java:195)
    at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113)
    at org.springframework.web.filter.CompositeFilter.doFilter(CompositeFilter.java:74)
    at org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration$CompositeFilterChainProxy.doFilter(WebMvcSecurityConfiguration.java:230)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:352)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:268)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:482)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344)
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:389)
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:896)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741)
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
    at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190)
    at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)
    at java.base/java.lang.Thread.run(Thread.java:1583)
Caused by: org.springframework.orm.ObjectOptimisticLockingFailureException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.bloomscorp.loom.seo.orm.TestProductImageGallerySEO#0]
    at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:304)
    at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:232)
    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:243)
    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:164)
    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:244)
    at jdk.proxy2/jdk.proxy2.$Proxy265.save(Unknown Source)
    at com.bloomscorp.behemoth.dao.controller.AbstractAddEntityDAOController.addNewEntity(AbstractAddEntityDAOController.java:22)
    at com.bloomscorp.loom.seo.dao.controller.TestProductImageGallerySEODAOController.modifyGalleryImages(TestProductImageGallerySEODAOController.java:78)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    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:751)
    at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:751)
    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:751)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:703)
    at com.bloomscorp.loom.seo.dao.controller.TestProductImageGallerySEODAOController$$SpringCGLIB$$0.modifyGalleryImages(<generated>)
    at com.bloomscorp.loom.seo.controller.ProductImageGallerySEOController.lambda$modifyGalleryImages$1(ProductImageGallerySEOController.java:126)
    at com.bloomscorp.behemoth.controller.AbstractPostEntityController.post(AbstractPostEntityController.java:95)
    at com.bloomscorp.behemoth.controller.AbstractPostEntityController.postEntityEnhancedResponse(AbstractPostEntityController.java:280)
    at com.bloomscorp.behemoth.controller.AbstractPostEntityController.postEntity(AbstractPostEntityController.java:140)
    at com.bloomscorp.behemoth.controller.AbstractPostEntityController.postEntity(AbstractPostEntityController.java:167)
    at com.bloomscorp.loom.seo.controller.ProductImageGallerySEOController.modifyGalleryImages(ProductImageGallerySEOController.java:117)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    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:751)
    at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:89)
    at com.bloomscorp.nverse.validator.NVerseValidationAspect.validateByDomainValidator(NVerseValidationAspect.java:67)
    at com.bloomscorp.nverse.validator.NVerseValidationAspect.validateDomainContext(NVerseValidationAspect.java:123)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:637)
    at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:627)
    at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:71)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:173)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:751)
    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.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:751)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:703)
    at com.bloomscorp.loom.seo.controller.ProductImageGallerySEOController$$SpringCGLIB$$0.modifyGalleryImages(<generated>)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:892)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:798)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1081)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:974)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)
    ... 61 more

Also below is the controller code which invokes the modifyGalleryImages function,

    @ResponseBody
    @PostMapping(
        value = "modify/test/gallery-images",
        consumes = Constant.REQUEST_TYPE_APPLICATION_JSON,
        produces = Constant.RESPONSE_TYPE_APPLICATION_JSON
    )
    public RainTreeResponse modifyGalleryImages(
        NVerseHttpRequestWrapper request,
        @RequestBody TestProductImageGallerySEOPayload gallerySEOPayload
    ) {
        return this.postEntity(
            request,
            "modifyGalleryImages",
            CODE_SUCU,
            UNAUTH_GALLERY_IMAGE_REQUEST,
            NEW_GALLERY_IMAGE_CREATED,
            this.testValidator,
            new TestProductImageGallerySEOPayloadSanitizer(),
            gallerySEOPayload,
            () -> this.testSEODAOController.modifyGalleryImages(
                this.getAuthorityResolver().resolveUserInformationFromAuthorizationToken(
                    request.getHeader(Constant.REQUEST_HEADER_AUTHORIZATION)
                ),
                gallerySEOPayload
            )
        );
    }

Please ignore the library level codes as those are already tested. The controller is invoking the modifyGalleryImages method once a request hits the controller.


Solution

  • It seems that new version of Hibernate does not recognize ID field value of 0 to be a new record. Setting its value to null will solve it.