# StaleObjectStateException in multi-tenancy on table

**URL:** <https://discourse.hibernate.org/t/staleobjectstateexception-in-multi-tenancy-on-table/11599>\
**Category:** Hibernate ORM\
**Created:** [June 26, 2025, 3:01pm UTC](https://discourse.hibernate.org/t/staleobjectstateexception-in-multi-tenancy-on-table/11599 "2025-06-26T15:01:16Z")\
**Posts on this page:** 7\
**Page:** 1

<div class="post-metadata">

**Author:** ![tourist](https://avatars.discourse-cdn.com/v4/letter/t/f4b2a3/32.png) [@tourist](https://discourse.hibernate.org/u/tourist)\
**Post date:** [June 26, 2025, 3:01pm UTC](https://discourse.hibernate.org/t/staleobjectstateexception-in-multi-tenancy-on-table/11599/1 "2025-06-26T15:01:16Z")

</div>

Hi! I have an application with multi-tenancy at the record level.  
There was a need to update the dependency version Hibernate 6.3.0.CR1 → 6.6.13.Final. I did everything according to the migration guide.  
After the update, I noticed strange behavior of the application when saving the entity.

Becase entities have a large description, I will give a small example.  
For example, exists two entites **Shop** and **Product** with next Java description

```auto
class Shop {
  @Id
  @Column(name = "shop_uuid")
  @Builder.Default
  private UUID shopUuid = UUID.randomUUID();

  @TenantId
  @Column(name = "tenant_code", nullable = false)
  private String tenantCode;

  @OneToMany(fetch = FetchType.LAZY)
  @JoinColumn(name = "shop_uuid", referencedColumnName = "shop_uuid")
  private List<Product> products;
}

```

```auto
class Product {
  @Id
  @GeneratedValue(strategy = GenerationType.UUID)
  @Column(name = "...")
  private UUID productUuid;

  @Column(name = "product_amount")
  private Integer amount;

  @ManyToOne(fetch = FetchType.EAGER)
  @JoinColumn(name = "shop_uuid", referencedColumnName = "shop_uuid", nullable = false)
  private Shop shop;
}

```

In addition, there are two classes that help work with multi-tenancy

```auto
public class TenantContext {
    private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal();

    private TenantContext() {
    }

    public static String getCurrentTenant() {
        return (String) CURRENT_TENANT.get();
    }

    public static void setCurrentTenant(String tenant) {
        CURRENT_TENANT.set(tenant);
    }

    public static void clear() {
        CURRENT_TENANT.remove();
    }
}

```

```auto
public class TenantIdentifierResolver implements CurrentTenantIdentifierResolver, HibernatePropertiesCustomizer {

    public String resolveCurrentTenantIdentifier() {
        return <resolving from TenantContext.class>;
    }

    public boolean validateExistingCurrentSessions() {
        return false;
    }

    public void customize(Map<String, Object> hibernateProperties) {
        hibernateProperties.put("hibernate.tenant_identifier_resolver", this);
    }

    public boolean isRoot(Object tenantId) {
        ...
    }
}

```

Next comes a cut from the logic in the most compressed format. There are two situations that used to work, and now one of them does not.

```auto
[SUCCESS] tenantCode from 'shop' equals 'another'
...
TenantContext.setCurrentTenant("another"); 
Product product = productRepository.findByAmount(5).get();
TenantContext.setCurrentTenant("another");
productRepository.save(product);
...

```

```auto
[FAILED] tenantCode from 'shop' equals 'another'
...
TenantContext.setCurrentTenant("default");
Product product = productRepository.findByAmount(5).get();
TenantContext.setCurrentTenant("another");
productRepository.save(product);
...

```

```auto
Caused by: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): []
        at app//org.hibernate.event.internal.DefaultMergeEventListener.entityIsDetached(DefaultMergeEventListener.java:426)
        at app//org.hibernate.event.internal.DefaultMergeEventListener.merge(DefaultMergeEventListener.java:214)
        at app//org.hibernate.event.internal.DefaultMergeEventListener.doMerge(DefaultMergeEventListener.java:152)
        at app//org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:136)
        at app//org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:89)
        at app//org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:127)
        at app//org.hibernate.internal.SessionImpl.fireMerge(SessionImpl.java:854)
        at app//org.hibernate.internal.SessionImpl.merge(SessionImpl.java:840)
        at java.base@17.0.14/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base@17.0.14/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
        at java.base@17.0.14/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base@17.0.14/java.lang.reflect.Method.invoke(Method.java:569)
        at app//org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:320)
        at app/jdk.proxy3/jdk.proxy3.$Proxy267.merge(Unknown Source)
        at app//org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:639)
        at java.base@17.0.14/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base@17.0.14/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
        at java.base@17.0.14/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base@17.0.14/java.lang.reflect.Method.invoke(Method.java:569)
        at app//org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359)
        at app//org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new$0(RepositoryMethodInvoker.java:277)
        at app//org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:170)
        at app//org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:158)
        at app//org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:515)
        at app//org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:284)
        at app//org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:731)
        at app//org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
        at app//org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:174)
        at app//org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:149)
        at app//org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
        at app//org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:69)
        at app//org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
        at app//org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:380)
        at app//org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
        at app//org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
        at app//org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:138)
        ... 19 more

```

---

<div class="post-metadata">

**Author:** ![beikov](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.hibernate.org/beikov/32/258_2.png) [@beikov](https://discourse.hibernate.org/u/beikov)\
**Post date:** [June 26, 2025, 4:11pm UTC](https://discourse.hibernate.org/t/staleobjectstateexception-in-multi-tenancy-on-table/11599/2 "2025-06-26T16:11:05Z")

</div>

> [@tourist](#):
>
> ```auto
> [FAILED] tenantCode from 'shop' equals 'another'
> ...
> TenantContext.setCurrentTenant("default");
> Product product = productRepository.findByAmount(5).get();
> TenantContext.setCurrentTenant("another");
> productRepository.save(product);
> ...
> 
> ```

Are you invoking this code in a transaction? Also, what’s your goal with this code?  
I don’t know what Spring Data does these days, so if you seek further help, you will have to ask the question without the Spring parts, but know that there is no need to `EntityManager#merge` an entity in a transaction that you looked up with e.g. `EntityManager#createQuery`, because entities are “managed” in this stateful `EntityManager`. This means that dirty changes are flushed automatically just before committing a transaction.

---

<div class="post-metadata">

**Author:** ![tourist](https://avatars.discourse-cdn.com/v4/letter/t/f4b2a3/32.png) [@tourist](https://discourse.hibernate.org/u/tourist)\
**Post date:** [June 26, 2025, 5:33pm UTC](https://discourse.hibernate.org/t/staleobjectstateexception-in-multi-tenancy-on-table/11599/3 "2025-06-26T17:33:22Z")

</div>

Not in transaction. Unfortunately, Spring it’s part of my context and I don’t use **EntityManager** directly.  
The point is that the error started to be thrown after migration to Hibernate 6.6. On later versions everything is ok

---

<div class="post-metadata">

**Author:** ![gavinking](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.hibernate.org/gavinking/32/1305_2.png) [@gavinking](https://discourse.hibernate.org/u/gavinking)\
**Post date:** [June 26, 2025, 8:49pm UTC](https://discourse.hibernate.org/t/staleobjectstateexception-in-multi-tenancy-on-table/11599/4 "2025-06-26T20:49:59Z")

</div>

This is almost certainly a result of this change:

> **[Jira](https://hibernate.atlassian.net/browse/HHH-16830)**

which was requested by an extremely long list of users.

---

<div class="post-metadata">

**Author:** ![tourist](https://avatars.discourse-cdn.com/v4/letter/t/f4b2a3/32.png) [@tourist](https://discourse.hibernate.org/u/tourist)\
**Post date:** [June 27, 2025, 6:45am UTC](https://discourse.hibernate.org/t/staleobjectstateexception-in-multi-tenancy-on-table/11599/5 "2025-06-27T06:45:17Z")

</div>

Okay, I get it, does this mean that this logic won’t work anymore and I can’t update an entity in a tenant other than the one where the find() operation was performed?  
Reason of this it’s relation and EAGER fetching, If you set lazy-loading, everything will work.

---

<div class="post-metadata">

**Author:** ![gavinking](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.hibernate.org/gavinking/32/1305_2.png) [@gavinking](https://discourse.hibernate.org/u/gavinking)\
**Post date:** [June 27, 2025, 7:09am UTC](https://discourse.hibernate.org/t/staleobjectstateexception-in-multi-tenancy-on-table/11599/6 "2025-06-27T07:09:13Z")

</div>

You could use `StatelessSession` I suppose.

Or you could do this kind of operation as the root temamt.

---

<div class="post-metadata">

**Author:** ![beikov](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.hibernate.org/beikov/32/258_2.png) [@beikov](https://discourse.hibernate.org/u/beikov)\
**Post date:** [June 27, 2025, 8:08am UTC](https://discourse.hibernate.org/t/staleobjectstateexception-in-multi-tenancy-on-table/11599/7 "2025-06-27T08:08:38Z")

</div>

> [@tourist](#):
>
> Not in transaction.

So you have a detached entity, belonging to tenant “default” and you want to be able to update that in the context of the tenant “another”. That sounds like you’re trying to circumvent the security/isolation that Hibernate ORM provides you with its multi-tenancy feature.  
You can do that by switching to the root tenant like Gavin suggested, but really, why would you want to allow that?
