In my application I tend to create value objects with records to increase type safety. Thanks to @EmbeddedId this is already much more convenient nowadays. However, when I try to combine @TenantId with @Embedded the application crashes with an exception: error processing @AttributeBinderType annotation ‘@org.hibernate.annotations.TenantId()’
Trying my own converter from UUID to TenantId wasn’t successful either. Any recommendations besides using UUID as a type?
@beikov unfortunately, even with hibernate version 7.1.8.Final this does not work. I tried the following approaches:
@Entity
public class Todo {
@EmbeddedId
private Id id;
@TenantId
@Embedded
private OrganizationId organizationId;
private String description;
public record Id(UUID id) {
public static Id create() {
return new Id(UUID.randomUUID());
}
}
}
//And OrganizationId defined as:
public record OrganizationId(UUID id) {}
@TenantId
@Convert(converter = OrganizationIdConverter.class)
private OrganizationId organizationId;
@Converter(autoApply = true)
public static class OrganizationIdConverter implements AttributeConverter<OrganizationId, UUID> {
@Override
public UUID convertToDatabaseColumn(OrganizationId attribute) {
return attribute != null ? attribute.id() : null;
}
@Override
public OrganizationId convertToEntityAttribute(UUID dbData) {
return dbData != null ? new OrganizationId(dbData) : null;
}
}
I get a different exception at startup: Caused by: jakarta.persistence.PersistenceException: Unable to build Hibernate SessionFactory [persistence unit: default] ; nested exception is java.lang.NullPointerException: Cannot invoke “org.hibernate.metamodel.mapping.JdbcMapping.getJavaTypeDescriptor()” because “jdbcMapping” is null
Let me know, if you need further information or if I should push my test project to a Github repository.
This mapping simply can’t work. You’re mixing things here. Try something like this:
@Entity
public class Todo {
@EmbeddedId
private Id id;
@Embedded
private OrganizationId organizationId;
private String description;
public record Id(UUID id) {
public static Id create() {
return new Id(UUID.randomUUID());
}
}
}
@Embeddable
public record OrganizationId(@TenantId UUID id) {}
Another point I figured out, with the current approach the CurrentTenantIdentifierResolver still needs to return a UUID (not OrganizationId). I think the reason is that under the hood we’re still dealing with a UUID. Anyway I’ll accept the solution - I can still fully type the rest of the application.
Just for the sake of completeness:
@Component
public class TenantIdentifierResolver implements CurrentTenantIdentifierResolver<UUID>, HibernatePropertiesCustomizer {
@Override
public UUID resolveCurrentTenantIdentifier() {
return TenantContext.getCurrentTenant().id();
}
@Override
public boolean validateExistingCurrentSessions() {
return false;
}
@Override
public void customize(Map<String, Object> hibernateProperties) {
hibernateProperties.put(MultiTenancySettings.MULTI_TENANT_IDENTIFIER_RESOLVER, this);
}
}