HHH-17117 allow @TenantId to form part of composite key

Signed-off-by: Gavin King <gavin@hibernate.org>
This commit is contained in:
Gavin King 2024-08-26 16:15:59 +02:00
parent 77a34e6312
commit d90807f9e4
14 changed files with 251 additions and 86 deletions

View File

@ -26,6 +26,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
* @author Gavin King
*/
@ValueGenerationType(generatedBy = TenantIdGeneration.class)
@IdGeneratorType(TenantIdGeneration.class)
@AttributeBinderType(binder = TenantIdBinder.class)
@Target({METHOD, FIELD})
@Retention(RUNTIME)

View File

@ -1358,24 +1358,6 @@ public class InFlightMetadataCollectorImpl implements InFlightMetadataCollector,
}
}
private static AnnotatedClassType getAnnotatedClassType2(ClassDetails classDetails) {
if ( classDetails.hasDirectAnnotationUsage( Entity.class ) ) {
return AnnotatedClassType.ENTITY;
}
else if ( classDetails.hasDirectAnnotationUsage( Embeddable.class ) ) {
return AnnotatedClassType.EMBEDDABLE;
}
else if ( classDetails.hasDirectAnnotationUsage( jakarta.persistence.MappedSuperclass.class ) ) {
return AnnotatedClassType.MAPPED_SUPERCLASS;
}
else if ( classDetails.hasDirectAnnotationUsage( Imported.class ) ) {
return AnnotatedClassType.IMPORTED;
}
else {
return AnnotatedClassType.NONE;
}
}
@Override
public void addMappedSuperclass(Class<?> type, MappedSuperclass mappedSuperclass) {
if ( mappedSuperClasses == null ) {
@ -2022,10 +2004,10 @@ public class InFlightMetadataCollectorImpl implements InFlightMetadataCollector,
}
}
}
stopProcess = failingSecondPasses.size() == 0 || failingSecondPasses.size() == endOfQueueFkSecondPasses.size();
stopProcess = failingSecondPasses.isEmpty() || failingSecondPasses.size() == endOfQueueFkSecondPasses.size();
endOfQueueFkSecondPasses = failingSecondPasses;
}
if ( endOfQueueFkSecondPasses.size() > 0 ) {
if ( !endOfQueueFkSecondPasses.isEmpty() ) {
throw originalException;
}
}
@ -2212,7 +2194,8 @@ public class InFlightMetadataCollectorImpl implements InFlightMetadataCollector,
handleIdentifierValueBinding(
entityBinding.getIdentifier(),
dialect,
(RootClass) entityBinding
(RootClass) entityBinding,
entityBinding.getIdentifierProperty()
);
}
@ -2225,22 +2208,20 @@ public class InFlightMetadataCollectorImpl implements InFlightMetadataCollector,
handleIdentifierValueBinding(
( (IdentifierCollection) collection ).getIdentifier(),
dialect,
null,
null
);
}
}
private void handleIdentifierValueBinding(
KeyValue identifierValueBinding,
Dialect dialect,
RootClass entityBinding) {
KeyValue identifierValueBinding, Dialect dialect, RootClass entityBinding, Property identifierProperty) {
// todo : store this result (back into the entity or into the KeyValue, maybe?)
// This process of instantiating the id-generator is called multiple times.
// It was done this way in the old code too, so no "regression" here; but
// it could be done better
try {
final Generator generator = identifierValueBinding.createGenerator( dialect, entityBinding );
final Generator generator = identifierValueBinding.createGenerator( dialect, entityBinding, identifierProperty );
if ( generator instanceof ExportableProducer ) {
( (ExportableProducer) generator ).registerExportables( getDatabase() );
}

View File

@ -25,7 +25,6 @@ import org.hibernate.engine.spi.SelfDirtinessTracker;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.engine.spi.Status;
import org.hibernate.event.spi.EventSource;
import org.hibernate.id.Assigned;
import org.hibernate.id.CompositeNestedGeneratedValueGenerator;
import org.hibernate.id.IdentifierGenerationException;
import org.hibernate.internal.CoreLogging;

View File

@ -51,24 +51,26 @@ public class TenantIdGeneration implements BeforeExecutionGenerator {
@Override
public Object generate(SharedSessionContractImplementor session, Object owner, Object currentValue, EventType eventType) {
final SessionFactoryImplementor sessionFactory = session.getSessionFactory();
final JavaType<Object> tenantIdentifierJavaType = sessionFactory.getTenantIdentifierJavaType();
final Object tenantId = session.getTenantIdentifierValue();
if ( currentValue != null ) {
final CurrentTenantIdentifierResolver<Object> resolver = sessionFactory.getCurrentTenantIdentifierResolver();
final CurrentTenantIdentifierResolver<Object> resolver =
sessionFactory.getCurrentTenantIdentifierResolver();
if ( resolver != null && resolver.isRoot( tenantId ) ) {
// the "root" tenant is allowed to set the tenant id explicitly
return currentValue;
}
if ( !tenantIdentifierJavaType.areEqual( currentValue, tenantId ) ) {
throw new PropertyValueException(
"assigned tenant id differs from current tenant id: " +
tenantIdentifierJavaType.toString( currentValue ) +
"!=" +
tenantIdentifierJavaType.toString( tenantId ),
entityName,
propertyName
);
else {
final JavaType<Object> 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 File

@ -103,15 +103,16 @@ public class FilterImpl implements Filter, Serializable {
* of the passed value did not match the configured type.
*/
public Filter setParameter(String name, Object value) throws IllegalArgumentException {
Object argument = definition.processArgument(value);
final Object argument = definition.processArgument( value );
// Make sure this is a defined parameter and check the incoming value type
JdbcMapping type = definition.getParameterJdbcMapping( name );
final JdbcMapping type = definition.getParameterJdbcMapping( name );
if ( type == null ) {
throw new IllegalArgumentException( "Undefined filter parameter [" + name + "]" );
throw new IllegalArgumentException( "Undefined filter parameter '" + name + "'" );
}
if ( argument != null && !type.getJavaTypeDescriptor().isInstance( argument ) ) {
throw new IllegalArgumentException( "Incorrect type for parameter [" + name + "]" );
throw new IllegalArgumentException( "Argument assigned to filter parameter '" + name
+ "' is not of type '" + type.getJavaTypeDescriptor().getTypeName() + "'" );
}
if ( parameters == null ) {
parameters = new TreeMap<>();
@ -133,14 +134,15 @@ public class FilterImpl implements Filter, Serializable {
if ( values == null ) {
throw new IllegalArgumentException( "Collection must be not null" );
}
JdbcMapping type = definition.getParameterJdbcMapping( name );
final JdbcMapping type = definition.getParameterJdbcMapping( name );
if ( type == null ) {
throw new HibernateException( "Undefined filter parameter [" + name + "]" );
throw new HibernateException( "Undefined filter parameter '" + name + "'" );
}
if ( !values.isEmpty() ) {
final Object element = values.iterator().next();
if ( !type.getJavaTypeDescriptor().isInstance( element ) ) {
throw new HibernateException( "Incorrect type for parameter [" + name + "]" );
throw new IllegalArgumentException( "Argument assigned to filter parameter '" + name
+ "' is not of type '" + type.getJavaTypeDescriptor().getTypeName() + "'" );
}
}
if ( parameters == null ) {

View File

@ -457,7 +457,7 @@ public class SessionFactoryImpl extends QueryParameterBindingTypeResolverImpl im
for ( PersistentClass model : bootMetamodel.getEntityBindings() ) {
if ( !model.isInherited() ) {
final KeyValue id = model.getIdentifier();
final Generator generator = id.createGenerator( dialect, (RootClass) model );
final Generator generator = id.createGenerator( dialect, (RootClass) model, model.getIdentifierProperty() );
if ( generator instanceof Configurable ) {
final Configurable identifierGenerator = (Configurable) generator;
identifierGenerator.initialize( sqlStringGenerationContext );

View File

@ -11,7 +11,6 @@ import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -546,10 +545,6 @@ public class Component extends SimpleValue implements MetaAttributable, Sortable
this.isKey = isKey;
}
public boolean hasPojoRepresentation() {
return componentClassName!=null;
}
/**
* Returns the {@link Property} at the specified position in this {@link Component}.
*
@ -661,37 +656,33 @@ public class Component extends SimpleValue implements MetaAttributable, Sortable
}
@Override
public Generator createGenerator(Dialect dialect, RootClass rootClass) throws MappingException {
public Generator createGenerator(Dialect dialect, RootClass rootClass, Property property) {
if ( builtIdentifierGenerator == null ) {
builtIdentifierGenerator = buildIdentifierGenerator( dialect, rootClass );
builtIdentifierGenerator =
getCustomIdGeneratorCreator().isAssigned()
? buildIdentifierGenerator( dialect, rootClass )
: super.createGenerator( dialect, rootClass, property );
}
return builtIdentifierGenerator;
}
private Generator buildIdentifierGenerator( Dialect dialect, RootClass rootClass) throws MappingException {
if ( !getCustomIdGeneratorCreator().isAssigned() ) {
return super.createGenerator( dialect, rootClass );
}
final Class<?> entityClass = rootClass.getMappedClass();
final Class<?> attributeDeclarer = getAttributeDeclarer( rootClass, entityClass );
final CompositeNestedGeneratedValueGenerator.GenerationContextLocator locator =
new StandardGenerationContextLocator( rootClass.getEntityName() );
private Generator buildIdentifierGenerator(Dialect dialect, RootClass rootClass) {
final CompositeNestedGeneratedValueGenerator generator =
new CompositeNestedGeneratedValueGenerator( locator, getType() );
new CompositeNestedGeneratedValueGenerator(
new StandardGenerationContextLocator( rootClass.getEntityName() ),
getType()
);
final List<Property> properties = getProperties();
for ( int i = 0; i < properties.size(); i++ ) {
final Property property = properties.get( i );
if ( property.getValue().isSimpleValue() ) {
final SimpleValue value = (SimpleValue) property.getValue();
if ( !value.getCustomIdGeneratorCreator().isAssigned() ) {
// skip any 'assigned' generators, they would have been handled by
// the StandardGenerationContextLocator
// skip any 'assigned' generators, they would have been
// handled by the StandardGenerationContextLocator
generator.addGeneratedValuePlan( new ValueGenerationPlan(
value.createGenerator( dialect, rootClass ),
getType().isMutable() ? injector( property, attributeDeclarer ) : null,
value.createGenerator( dialect, rootClass, property ),
getType().isMutable() ? injector( property, getAttributeDeclarer( rootClass ) ) : null,
i
) );
}
@ -700,23 +691,27 @@ public class Component extends SimpleValue implements MetaAttributable, Sortable
return generator;
}
private Class<?> getAttributeDeclarer(RootClass rootClass, Class<?> entityClass) {
final Class<?> attributeDeclarer; // what class is the declarer of the composite pk attributes
// IMPL NOTE : See the javadoc discussion on CompositeNestedGeneratedValueGenerator wrt the
// various scenarios for which we need to account here
/**
* Return the class that declares the composite pk attributes,
* which might be an {@code @IdClass}, an {@code @EmbeddedId},
* of the entity class itself.
*/
private Class<?> getAttributeDeclarer(RootClass rootClass) {
// See the javadoc discussion on CompositeNestedGeneratedValueGenerator
// for the various scenarios we need to account for here
if ( rootClass.getIdentifierMapper() != null ) {
// we have the @IdClass / <composite-id mapped="true"/> case
attributeDeclarer = resolveComponentClass();
return resolveComponentClass();
}
else if ( rootClass.getIdentifierProperty() != null ) {
// we have the "@EmbeddedId" / <composite-id name="idName"/> case
attributeDeclarer = resolveComponentClass();
return resolveComponentClass();
}
else {
// we have the "straight up" embedded (again the Hibernate term) component identifier
attributeDeclarer = entityClass;
// we have the "straight up" embedded (again the Hibernate term)
// component identifier: the entity class itself is the id class
return rootClass.getMappedClass();
}
return attributeDeclarer;
}
private Setter injector(Property property, Class<?> attributeDeclarer) {

View File

@ -27,6 +27,11 @@ public interface KeyValue extends Value {
boolean isUpdateable();
Generator createGenerator(Dialect dialect, RootClass rootClass);
@Deprecated(since = "7.0")
default Generator createGenerator(Dialect dialect, RootClass rootClass) {
return createGenerator( dialect, rootClass, null );
}
Generator createGenerator(Dialect dialect, RootClass rootClass, Property property);
}

View File

@ -375,10 +375,10 @@ public abstract class SimpleValue implements KeyValue {
}
@Override
public Generator createGenerator(Dialect dialect, RootClass rootClass) {
public Generator createGenerator(Dialect dialect, RootClass rootClass, Property property) {
if ( generator == null ) {
if ( customIdGeneratorCreator != null ) {
generator = customIdGeneratorCreator.createGenerator( new IdGeneratorCreationContext( rootClass ) );
generator = customIdGeneratorCreator.createGenerator( new IdGeneratorCreationContext( rootClass, property ) );
}
}
return generator;
@ -1011,9 +1011,11 @@ public abstract class SimpleValue implements KeyValue {
private class IdGeneratorCreationContext implements GeneratorCreationContext {
private final RootClass rootClass;
private final Property property;
public IdGeneratorCreationContext(RootClass rootClass) {
public IdGeneratorCreationContext(RootClass rootClass, Property property) {
this.rootClass = rootClass;
this.property = property;
}
@Override
@ -1048,7 +1050,7 @@ public abstract class SimpleValue implements KeyValue {
@Override
public Property getProperty() {
return rootClass.getIdentifierProperty();
return property;
}
@Override

View File

@ -598,7 +598,7 @@ public abstract class AbstractCollectionPersister
}
private BeforeExecutionGenerator createGenerator(RuntimeModelCreationContext context, IdentifierCollection collection) {
final Generator generator = collection.getIdentifier().createGenerator( context.getDialect(), null );
final Generator generator = collection.getIdentifier().createGenerator( context.getDialect(), null, null );
if ( generator.generatedOnExecution() ) {
throw new MappingException("must be an BeforeExecutionGenerator"); //TODO fix message
}

View File

@ -0,0 +1,26 @@
package org.hibernate.orm.test.tenantidpk;
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;
@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.tenantidpk;
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;
@Id @TenantId
UUID tenantId;
@OneToMany(mappedBy = "client")
Set<Account> accounts = new HashSet<>();
public Client(String name) {
this.name = name;
}
Client() {}
}

View File

@ -0,0 +1,115 @@
/*
* 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.tenantidpk;
import org.hibernate.PropertyValueException;
import org.hibernate.binder.internal.TenantIdBinder;
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.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.UUID;
import static org.hibernate.cfg.AvailableSettings.JAKARTA_HBM2DDL_DATABASE_ACTION;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.fail;
@SessionFactory
@DomainModel(annotatedClasses = { Account.class, Client.class })
@ServiceRegistry(
settings = {
@Setting(name = JAKARTA_HBM2DDL_DATABASE_ACTION, value = "create-drop")
}
)
public class TenantPkTest 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<UUID>() {
@Override
public UUID resolveCurrentTenantIdentifier() {
return currentTenant;
}
@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.createSelectionQuery("where id=?1", Account.class)
.setParameter(1, acc.id)
.getSingleResultOrNull() );
assertEquals( 1, session.createQuery("from Account").getResultList().size() );
} );
assertEquals(mine, acc.tenantId);
currentTenant = yours;
scope.inTransaction( session -> {
assertNull( session.createSelectionQuery("where id=?1", Account.class)
.setParameter(1, acc.id)
.getSingleResultOrNull() );
assertEquals( 0, session.createQuery("from Account").getResultList().size() );
session.disableFilter(TenantIdBinder.FILTER_NAME);
assertNotNull( session.createSelectionQuery("where id=?1", Account.class)
.setParameter(1, acc.id)
.getSingleResultOrNull() );
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;
scope.inTransaction( session -> {
session.persist(client);
session.persist(acc);
} );
assertEquals( mine, acc.tenantId );
assertEquals( mine, client.tenantId );
}
}

View File

@ -9,7 +9,6 @@ package org.hibernate.processor.util.xml;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.tools.FileObject;