diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/FilterDefinition.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/FilterDefinition.java index ccaa4a5809..a7593a9c1c 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/FilterDefinition.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/FilterDefinition.java @@ -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; + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/FilterHelper.java b/hibernate-core/src/main/java/org/hibernate/internal/FilterHelper.java index fb0d00be68..83ba858b92 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/FilterHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/FilterHelper.java @@ -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; diff --git a/hibernate-core/src/main/java/org/hibernate/internal/FilterImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/FilterImpl.java index 381cee4d43..55c8a43787 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/FilterImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/FilterImpl.java @@ -73,15 +73,17 @@ public Map 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; } diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/TenantIdBinder.java b/hibernate-core/src/main/java/org/hibernate/tuple/TenantIdBinder.java index 9993751e48..8b141d8316 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/TenantIdBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/TenantIdBinder.java @@ -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 { diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/TenantIdGeneration.java b/hibernate-core/src/main/java/org/hibernate/tuple/TenantIdGeneration.java index f1815f940b..967e26a738 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/TenantIdGeneration.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/TenantIdGeneration.java @@ -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, 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 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 diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/tenantlongid/Account.java b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantlongid/Account.java new file mode 100644 index 0000000000..88b827d375 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantlongid/Account.java @@ -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 . + */ +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() {} +} + diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/tenantlongid/Client.java b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantlongid/Client.java new file mode 100644 index 0000000000..da18cadeed --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantlongid/Client.java @@ -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 . + */ +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 accounts = new HashSet<>(); + + public Client(String name) { + this.name = name; + } + + Client() {} +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/tenantlongid/TenantLongIdTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantlongid/TenantLongIdTest.java new file mode 100644 index 0000000000..e0fc772ee4 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantlongid/TenantLongIdTest.java @@ -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 . + */ +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 ); + } ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/tenantuuid/Account.java b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantuuid/Account.java new file mode 100644 index 0000000000..fb32b11549 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantuuid/Account.java @@ -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 . + */ +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() {} +} + diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/tenantuuid/Client.java b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantuuid/Client.java new file mode 100644 index 0000000000..7eeab1f352 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantuuid/Client.java @@ -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 . + */ +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 accounts = new HashSet<>(); + + public Client(String name) { + this.name = name; + } + + Client() {} +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/tenantuuid/TenantUuidTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantuuid/TenantUuidTest.java new file mode 100644 index 0000000000..2d3532f67b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantuuid/TenantUuidTest.java @@ -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 . + */ +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 ); + } ); + } +}