HHH-15483 allow @TenantId properties of type UUID, Long, Integer, etc

implementation is a bit nasty but it works
This commit is contained in:
Gavin King 2022-09-06 10:27:37 +02:00
parent 6cf9d2d480
commit 601e82620d
11 changed files with 455 additions and 18 deletions

View File

@ -77,4 +77,14 @@ public String getDefaultFilterCondition() {
return defaultFilterCondition;
}
/**
* Called before binding a JDBC parameter
*
* @param value the argument to the parameter, as set via {@link org.hibernate.Filter#setParameter(String, Object)}
* @return the argument that will actually be bound to the parameter
*/
public Object processArgument(Object value) {
return value;
}
}

View File

@ -16,6 +16,7 @@
import org.hibernate.Filter;
import org.hibernate.MappingException;
import org.hibernate.engine.spi.FilterDefinition;
import org.hibernate.engine.spi.LoadQueryInfluencers;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.internal.util.StringHelper;
@ -167,9 +168,8 @@ private FilterPredicate generateFilterPredicate(FilterAliasGenerator aliasGenera
if ( CollectionHelper.isNotEmpty( filterParameterNames ) ) {
for ( int paramPos = 0; paramPos < filterParameterNames.size(); paramPos++ ) {
final String parameterName = filterParameterNames.get( paramPos );
final JdbcMapping jdbcMapping = enabledFilter
.getFilterDefinition()
.getParameterJdbcMapping( parameterName );
final FilterDefinition filterDefinition = enabledFilter.getFilterDefinition();
final JdbcMapping jdbcMapping = filterDefinition.getParameterJdbcMapping( parameterName );
final Object parameterValue = enabledFilter.getParameter( parameterName );
if ( parameterValue == null ) {
throw new MappingException( String.format( "unknown parameter [%s] for filter [%s]", parameterName, filterName ) );
@ -180,8 +180,8 @@ private FilterPredicate generateFilterPredicate(FilterAliasGenerator aliasGenera
&& !jdbcMapping.getJavaTypeDescriptor().isInstance( parameterValue ) ) {
final Iterator<?> iterator = ( (Iterable<?>) parameterValue ).iterator();
if ( iterator.hasNext() ) {
final Object value = iterator.next();
final FilterJdbcParameter jdbcParameter = new FilterJdbcParameter( jdbcMapping, value );
final Object element = iterator.next();
final FilterJdbcParameter jdbcParameter = new FilterJdbcParameter( jdbcMapping, element );
filterPredicate.applyParameter( jdbcParameter );
while ( iterator.hasNext() ) {
@ -195,7 +195,8 @@ private FilterPredicate generateFilterPredicate(FilterAliasGenerator aliasGenera
}
}
else {
filterPredicate.applyParameter( new FilterJdbcParameter( jdbcMapping, parameterValue ) );
final Object argument = filterDefinition.processArgument( parameterValue );
filterPredicate.applyParameter( new FilterJdbcParameter( jdbcMapping, argument) );
}
final String marker = ":" + filterNames[ i ] + "." + parameterName;

View File

@ -73,15 +73,17 @@ public Map<String,?> getParameters() {
* of the passed value did not match the configured type.
*/
public Filter setParameter(String name, Object value) throws IllegalArgumentException {
Object argument = definition.processArgument(value);
// Make sure this is a defined parameter and check the incoming value type
JdbcMapping type = definition.getParameterJdbcMapping( name );
if ( type == null ) {
throw new IllegalArgumentException( "Undefined filter parameter [" + name + "]" );
}
if ( value != null && !type.getJavaTypeDescriptor().isInstance( value ) ) {
if ( argument != null && !type.getJavaTypeDescriptor().isInstance( argument ) ) {
throw new IllegalArgumentException( "Incorrect type for parameter [" + name + "]" );
}
parameters.put( name, value );
parameters.put( name, argument );
return this;
}

View File

@ -11,7 +11,6 @@
import org.hibernate.boot.spi.InFlightMetadataCollector;
import org.hibernate.boot.spi.MetadataBuildingContext;
import org.hibernate.engine.spi.FilterDefinition;
import org.hibernate.mapping.BasicValue;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.Formula;
import org.hibernate.mapping.PersistentClass;
@ -55,7 +54,23 @@ public void bind(
FILTER_NAME,
"",
singletonMap( PARAMETER_NAME, tenantIdType )
)
) {
// unfortunately the old APIs only accept String for a tenantId, so parse it
@Override
public Object processArgument(Object value) {
if (value==null) {
return null;
}
else if (value instanceof String) {
return getParameterJdbcMapping( PARAMETER_NAME )
.getJavaTypeDescriptor()
.fromString((String) value);
}
else {
return value;
}
}
}
);
}
else {

View File

@ -6,11 +6,13 @@
*/
package org.hibernate.tuple;
import org.hibernate.MappingException;
import org.hibernate.PropertyValueException;
import org.hibernate.Session;
import org.hibernate.annotations.TenantId;
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.type.descriptor.java.JavaType;
/**
* Value generation implementation for {@link TenantId}.
@ -21,11 +23,13 @@ public class TenantIdGeneration implements AnnotationValueGeneration<TenantId>,
private String entityName;
private String propertyName;
private Class<?> propertyType;
@Override
public void initialize(TenantId annotation, Class<?> propertyType, String entityName, String propertyName) {
this.entityName = entityName;
this.propertyName = propertyName;
this.propertyType = propertyType;
}
@Override
@ -45,24 +49,30 @@ public ValueGenerator<?> getValueGenerator() {
@Override
public Object generateValue(Session session, Object owner, Object currentValue) {
String identifier = session.getTenantIdentifier();
SessionFactoryImplementor sessionFactory = (SessionFactoryImplementor) session.getSessionFactory();
JavaType<Object> descriptor = sessionFactory.getTypeConfiguration().getJavaTypeRegistry()
.findDescriptor(propertyType);
if ( descriptor==null ) {
throw new MappingException( "unsupported tenant id property type: " + propertyType.getName() );
}
String tenantId = session.getTenantIdentifier(); //unfortunately this is always a string in old APIs
if ( currentValue != null ) {
CurrentTenantIdentifierResolver resolver =
((SessionFactoryImplementor) session.getSessionFactory())
.getCurrentTenantIdentifierResolver();
if ( resolver!=null && resolver.isRoot( session.getTenantIdentifier() ) ) {
CurrentTenantIdentifierResolver resolver = sessionFactory.getCurrentTenantIdentifierResolver();
if ( resolver!=null && resolver.isRoot(tenantId) ) {
// the "root" tenant is allowed to set the tenant id explicitly
return currentValue;
}
if ( !currentValue.equals(identifier) ) {
String currentTenantId = descriptor.toString(currentValue);
if ( !currentTenantId.equals(tenantId) ) {
throw new PropertyValueException(
"assigned tenant id differs from current tenant id: "
+ currentValue + "!=" + identifier,
+ currentTenantId + "!=" + tenantId,
entityName, propertyName
);
}
}
return identifier;
return tenantId == null ? null : descriptor.fromString(tenantId); //convert to the model type
}
@Override

View File

@ -0,0 +1,31 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.orm.test.tenantlongid;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import org.hibernate.annotations.TenantId;
@Entity
public class Account {
@Id @GeneratedValue Long id;
@TenantId Long tenantId;
@ManyToOne(optional = false)
Client client;
public Account(Client client) {
this.client = client;
}
Account() {}
}

View File

@ -0,0 +1,37 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.orm.test.tenantlongid;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import org.hibernate.annotations.TenantId;
import java.util.HashSet;
import java.util.Set;
@Entity
public class Client {
@Id
@GeneratedValue
Long id;
String name;
@TenantId
Long tenantId;
@OneToMany(mappedBy = "client")
Set<Account> accounts = new HashSet<>();
public Client(String name) {
this.name = name;
}
Client() {}
}

View File

@ -0,0 +1,129 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.orm.test.tenantlongid;
import org.hibernate.PropertyValueException;
import org.hibernate.boot.SessionFactoryBuilder;
import org.hibernate.boot.spi.MetadataImplementor;
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.ServiceRegistry;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryProducer;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.hibernate.testing.orm.junit.Setting;
import org.hibernate.tuple.TenantIdBinder;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.hibernate.cfg.AvailableSettings.HBM2DDL_DATABASE_ACTION;
import static org.junit.jupiter.api.Assertions.*;
@SessionFactory
@DomainModel(annotatedClasses = { Account.class, Client.class })
@ServiceRegistry(
settings = {
@Setting(name = HBM2DDL_DATABASE_ACTION, value = "create-drop")
}
)
public class TenantLongIdTest implements SessionFactoryProducer {
private static final Long mine = 1L;
private static final Long yours = 2L;
Long currentTenant;
@AfterEach
public void cleanup(SessionFactoryScope scope) {
scope.inTransaction( session -> {
session.createQuery("delete from Account").executeUpdate();
session.createQuery("delete from Client").executeUpdate();
});
}
@Override
public SessionFactoryImplementor produceSessionFactory(MetadataImplementor model) {
final SessionFactoryBuilder sessionFactoryBuilder = model.getSessionFactoryBuilder();
sessionFactoryBuilder.applyCurrentTenantIdentifierResolver( new CurrentTenantIdentifierResolver() {
@Override
public String resolveCurrentTenantIdentifier() {
return currentTenant.toString();
}
@Override
public boolean validateExistingCurrentSessions() {
return false;
}
} );
return (SessionFactoryImplementor) sessionFactoryBuilder.build();
}
@Test
public void test(SessionFactoryScope scope) {
currentTenant = mine;
Client client = new Client("Gavin");
Account acc = new Account(client);
scope.inTransaction( session -> {
session.persist(client);
session.persist(acc);
} );
scope.inTransaction( session -> {
assertNotNull( session.find(Account.class, acc.id) );
assertEquals( 1, session.createQuery("from Account").getResultList().size() );
} );
assertEquals(mine, acc.tenantId);
currentTenant = yours;
scope.inTransaction( session -> {
assertNull( session.find(Account.class, acc.id) );
assertEquals( 0, session.createQuery("from Account").getResultList().size() );
session.disableFilter(TenantIdBinder.FILTER_NAME);
assertNotNull( session.find(Account.class, acc.id) );
assertEquals( 1, session.createQuery("from Account").getResultList().size() );
} );
}
@Test
public void testErrorOnInsert(SessionFactoryScope scope) {
currentTenant = mine;
Client client = new Client("Gavin");
Account acc = new Account(client);
acc.tenantId = yours;
try {
scope.inTransaction( session -> {
session.persist(client);
session.persist(acc);
} );
fail("should have thrown");
}
catch (Throwable e) {
assertTrue( e.getCause() instanceof PropertyValueException );
}
}
@Test
public void testErrorOnUpdate(SessionFactoryScope scope) {
currentTenant = mine;
Client client = new Client("Gavin");
Account acc = new Account(client);
scope.inTransaction( session -> {
session.persist(client);
session.persist(acc);
acc.tenantId = yours;
client.tenantId = yours;
client.name = "Steve";
} );
//TODO: it would be better if this were an error
scope.inTransaction( session -> {
Account account = session.find(Account.class, acc.id);
assertNotNull(account);
assertEquals( mine, acc.tenantId );
assertEquals( "Steve", acc.client.name );
assertEquals( mine, acc.client.tenantId );
} );
}
}

View File

@ -0,0 +1,33 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.orm.test.tenantuuid;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import org.hibernate.annotations.TenantId;
import java.util.UUID;
@Entity
public class Account {
@Id @GeneratedValue Long id;
@TenantId UUID tenantId;
@ManyToOne(optional = false)
Client client;
public Account(Client client) {
this.client = client;
}
Account() {}
}

View File

@ -0,0 +1,38 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.orm.test.tenantuuid;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import org.hibernate.annotations.TenantId;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
@Entity
public class Client {
@Id
@GeneratedValue
Long id;
String name;
@TenantId
UUID tenantId;
@OneToMany(mappedBy = "client")
Set<Account> accounts = new HashSet<>();
public Client(String name) {
this.name = name;
}
Client() {}
}

View File

@ -0,0 +1,131 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.orm.test.tenantuuid;
import org.hibernate.PropertyValueException;
import org.hibernate.boot.SessionFactoryBuilder;
import org.hibernate.boot.spi.MetadataImplementor;
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.ServiceRegistry;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryProducer;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.hibernate.testing.orm.junit.Setting;
import org.hibernate.tuple.TenantIdBinder;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.UUID;
import static org.hibernate.cfg.AvailableSettings.HBM2DDL_DATABASE_ACTION;
import static org.junit.jupiter.api.Assertions.*;
@SessionFactory
@DomainModel(annotatedClasses = { Account.class, Client.class })
@ServiceRegistry(
settings = {
@Setting(name = HBM2DDL_DATABASE_ACTION, value = "create-drop")
}
)
public class TenantUuidTest implements SessionFactoryProducer {
private static final UUID mine = UUID.randomUUID();
private static final UUID yours = UUID.randomUUID();
UUID currentTenant;
@AfterEach
public void cleanup(SessionFactoryScope scope) {
scope.inTransaction( session -> {
session.createQuery("delete from Account").executeUpdate();
session.createQuery("delete from Client").executeUpdate();
});
}
@Override
public SessionFactoryImplementor produceSessionFactory(MetadataImplementor model) {
final SessionFactoryBuilder sessionFactoryBuilder = model.getSessionFactoryBuilder();
sessionFactoryBuilder.applyCurrentTenantIdentifierResolver( new CurrentTenantIdentifierResolver() {
@Override
public String resolveCurrentTenantIdentifier() {
return currentTenant.toString();
}
@Override
public boolean validateExistingCurrentSessions() {
return false;
}
} );
return (SessionFactoryImplementor) sessionFactoryBuilder.build();
}
@Test
public void test(SessionFactoryScope scope) {
currentTenant = mine;
Client client = new Client("Gavin");
Account acc = new Account(client);
scope.inTransaction( session -> {
session.persist(client);
session.persist(acc);
} );
scope.inTransaction( session -> {
assertNotNull( session.find(Account.class, acc.id) );
assertEquals( 1, session.createQuery("from Account").getResultList().size() );
} );
assertEquals(mine, acc.tenantId);
currentTenant = yours;
scope.inTransaction( session -> {
assertNull( session.find(Account.class, acc.id) );
assertEquals( 0, session.createQuery("from Account").getResultList().size() );
session.disableFilter(TenantIdBinder.FILTER_NAME);
assertNotNull( session.find(Account.class, acc.id) );
assertEquals( 1, session.createQuery("from Account").getResultList().size() );
} );
}
@Test
public void testErrorOnInsert(SessionFactoryScope scope) {
currentTenant = mine;
Client client = new Client("Gavin");
Account acc = new Account(client);
acc.tenantId = yours;
try {
scope.inTransaction( session -> {
session.persist(client);
session.persist(acc);
} );
fail("should have thrown");
}
catch (Throwable e) {
assertTrue( e.getCause() instanceof PropertyValueException );
}
}
@Test
public void testErrorOnUpdate(SessionFactoryScope scope) {
currentTenant = mine;
Client client = new Client("Gavin");
Account acc = new Account(client);
scope.inTransaction( session -> {
session.persist(client);
session.persist(acc);
acc.tenantId = yours;
client.tenantId = yours;
client.name = "Steve";
} );
//TODO: it would be better if this were an error
scope.inTransaction( session -> {
Account account = session.find(Account.class, acc.id);
assertNotNull(account);
assertEquals( mine, acc.tenantId );
assertEquals( "Steve", acc.client.name );
assertEquals( mine, acc.client.tenantId );
} );
}
}