hibernate/hibernate-orm · error · PropertyValueException
assigned tenant id differs from current tenant id [{} != {}]
Error message
assigned tenant id differs from current tenant id [{} != {}] What it means
@TenantId assigns the current session tenant identifier to a tenant-discriminator attribute on insert and update. If the entity already carries a tenant id that differs from the session's current tenant — and the CurrentTenantIdentifierResolver does not report the current tenant as a 'root' tenant allowed to write foreign tenants — flush fails with this PropertyValueException. It is a data-integrity guard against cross-tenant writes.
Source
Thrown at hibernate-core/src/main/java/org/hibernate/generator/internal/TenantIdGeneration.java:68
@Override
public Class<?> getGeneratedType() {
return generatedType;
}
@Override
public Object generate(SharedSessionContractImplementor session, Object owner, Object currentValue, EventType eventType) {
final var sessionFactory = session.getSessionFactory();
final Object tenantId = session.getTenantIdentifierValue();
if ( currentValue != null ) {
final var resolver = sessionFactory.getCurrentTenantIdentifierResolver();
if ( resolver != null && resolver.isRoot( tenantId ) ) {
// the "root" tenant is allowed to set the tenant id explicitly
return currentValue;
}
else {
final var tenantIdJavaType = sessionFactory.getTenantIdentifierJavaType();
if ( !tenantIdJavaType.areEqual( currentValue, tenantId ) ) {
throw new PropertyValueException(
"assigned tenant id differs from current tenant id ["
+ tenantIdJavaType.toString( currentValue )
+ " != "
+ tenantIdJavaType.toString( tenantId ) + "]",
entityName,
propertyName
);
}
}
}
return tenantId;
}
}
View on GitHub (pinned to fad1729dce)
Solutions
- Ensure the entity's tenant field matches the session tenant before flushing — reload or merge within the same tenant's session
- If an admin/root tenant must write other tenants' rows, implement CurrentTenantIdentifierResolver.isRoot(currentTenant) to return true for it
- Stop reusing detached instances across tenant contexts; keep entity instances per tenant
- If the mismatch is a false positive, align the tenant identifier Java type (e.g. String vs UUID) across resolver, SessionFactory, and entity field so areEqual compares consistently
- Clear or refresh the stale tenant field before merging
Example fix
// before — order loaded under tenant "acme", flushed in a session bound to "globex"
sessionGlobex.merge(order); // PropertyValueException: assigned tenant id differs
// after — verify tenant ownership before writing
if (!Objects.equals(order.getTenant(), sessionGlobex.getTenantIdentifierValue())) {
throw new SecurityException("cross-tenant write blocked");
}
sessionGlobex.merge(order); Defensive patterns
Strategy: validation
Validate before calling
// Call before persist/merge of entities carrying @TenantId
static void assertSameTenant(Object entityTenantValue, SharedSessionContract session) {
Object current = session.getTenantIdentifierValue();
if (entityTenantValue != null && !entityTenantValue.equals(current)) {
throw new IllegalStateException("entity tenant [" + entityTenantValue
+ "] does not match session tenant [" + current + "]");
}
} Try / catch
try {
session.merge(order);
session.flush();
}
catch (PropertyValueException e) {
if (e.getMessage() != null && e.getMessage().startsWith("assigned tenant id differs")) {
// cross-tenant write attempt: abort and audit, never blind-retry
throw new TenantAccessException(order.getTenant(), session.getTenantIdentifierValue(), e);
}
throw e;
} Prevention
- Open and use one session per tenant request; never share sessions across tenant switches
- Implement CurrentTenantIdentifierResolver.isRoot() for legitimate super-tenant/admin writers
- Add integration tests that switch tenants mid-conversation
- Keep the tenant id Java type consistent across resolver, SessionFactory, and entity field
When it happens
Trigger: Flushing an entity whose @TenantId field holds a value != session.getTenantIdentifierValue(): loading under tenant A and merging/updating in a session bound to tenant B; pre-populated or deserialized entities with a stale tenant field; a resolver whose isRoot(...) returns false for an admin tenant that legitimately writes other tenants' rows.
Common situations: Multi-tenant request processing where the tenant context (ThreadLocal/request scope) changed between load and save; background jobs replaying detached entities under a system tenant; misconfigured CurrentTenantIdentifierResolver.isRoot(); tenant identifier Java type mismatches (String vs UUID) so areEqual never matches.
Related errors
- all @TenantId fields must have the same type: <parameterType
- @TenantId attribute must be mapped to a single column or for
- SessionFactory configured for multi-tenancy, but no tenant i
- Row-level security enabled, but no tenant identifier specifi
- cannot recreate collection while filter is enabled: " + coll
AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22).
Data as JSON: /api/errors/2932a2ed5de89d9e.
Report an issue: GitHub.