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

  1. Ensure the entity's tenant field matches the session tenant before flushing — reload or merge within the same tenant's session
  2. If an admin/root tenant must write other tenants' rows, implement CurrentTenantIdentifierResolver.isRoot(currentTenant) to return true for it
  3. Stop reusing detached instances across tenant contexts; keep entity instances per tenant
  4. 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
  5. 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

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


AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22). Data as JSON: /api/errors/2932a2ed5de89d9e. Report an issue: GitHub.