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
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;
}
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
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();
}
}
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.
[SUCCESS] tenantCode from 'shop' equals 'another'
...
TenantContext.setCurrentTenant("another");
Product product = productRepository.findByAmount(5).get();
TenantContext.setCurrentTenant("another");
productRepository.save(product);
...
[FAILED] tenantCode from 'shop' equals 'another'
...
TenantContext.setCurrentTenant("default");
Product product = productRepository.findByAmount(5).get();
TenantContext.setCurrentTenant("another");
productRepository.save(product);
...
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