diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/TenantId.java b/hibernate-core/src/main/java/org/hibernate/annotations/TenantId.java index e3892d056c4f..98b01dc003f7 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/TenantId.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/TenantId.java @@ -26,6 +26,7 @@ * @author Gavin King */ @ValueGenerationType(generatedBy = TenantIdGeneration.class) +@IdGeneratorType(TenantIdGeneration.class) @AttributeBinderType(binder = TenantIdBinder.class) @Target({METHOD, FIELD}) @Retention(RUNTIME) diff --git a/hibernate-core/src/main/java/org/hibernate/boot/internal/InFlightMetadataCollectorImpl.java b/hibernate-core/src/main/java/org/hibernate/boot/internal/InFlightMetadataCollectorImpl.java index 8295c558addf..75eb636bbfb5 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/internal/InFlightMetadataCollectorImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/internal/InFlightMetadataCollectorImpl.java @@ -2133,7 +2133,8 @@ private void processExportableProducers() { handleIdentifierValueBinding( entityBinding.getIdentifier(), dialect, - (RootClass) entityBinding + (RootClass) entityBinding, + entityBinding.getIdentifierProperty() ); } @@ -2146,15 +2147,14 @@ private void processExportableProducers() { 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 @@ -2163,7 +2163,8 @@ private void handleIdentifierValueBinding( final Generator generator = identifierValueBinding.createGenerator( bootstrapContext.getIdentifierGeneratorFactory(), dialect, - entityBinding + entityBinding, + identifierProperty ); if ( generator instanceof ExportableProducer ) { diff --git a/hibernate-core/src/main/java/org/hibernate/generator/internal/TenantIdGeneration.java b/hibernate-core/src/main/java/org/hibernate/generator/internal/TenantIdGeneration.java index 8b60131d9222..887e19ea69fc 100644 --- a/hibernate-core/src/main/java/org/hibernate/generator/internal/TenantIdGeneration.java +++ b/hibernate-core/src/main/java/org/hibernate/generator/internal/TenantIdGeneration.java @@ -18,6 +18,7 @@ import org.hibernate.generator.EventType; import org.hibernate.generator.EventTypeSets; import org.hibernate.generator.GeneratorCreationContext; +import org.hibernate.id.factory.spi.CustomIdGeneratorCreationContext; import org.hibernate.type.descriptor.java.JavaType; import static org.hibernate.generator.EventTypeSets.INSERT_ONLY; @@ -33,6 +34,10 @@ public class TenantIdGeneration implements BeforeExecutionGenerator { private final String entityName; private final String propertyName; + public TenantIdGeneration(TenantId annotation, Member member, CustomIdGeneratorCreationContext context) { + this(annotation, member, (GeneratorCreationContext) context); + } + public TenantIdGeneration(TenantId annotation, Member member, GeneratorCreationContext context) { entityName = context.getPersistentClass() == null ? member.getDeclaringClass().getName() //it's an attribute of an embeddable diff --git a/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java index 9ca7b6b571eb..1503a079d68b 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java @@ -467,7 +467,8 @@ private static Map createGenerators( final Generator generator = id.createGenerator( bootstrapContext.getIdentifierGeneratorFactory(), jdbcServices.getJdbcEnvironment().getDialect(), - (RootClass) model + (RootClass) model, + model == null ? null : model.getIdentifierProperty() ); if ( generator instanceof Configurable ) { final Configurable identifierGenerator = (Configurable) generator; diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Component.java b/hibernate-core/src/main/java/org/hibernate/mapping/Component.java index 8891b5219cac..634f293f4297 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Component.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Component.java @@ -671,12 +671,19 @@ public Generator createGenerator( IdentifierGeneratorFactory identifierGeneratorFactory, Dialect dialect, RootClass rootClass) throws MappingException { + return createGenerator( identifierGeneratorFactory, dialect, rootClass, rootClass == null ? null : rootClass.getIdentifierProperty() ); + } + + @Override + public Generator createGenerator( + IdentifierGeneratorFactory identifierGeneratorFactory, + Dialect dialect, + RootClass rootClass, + Property property) throws MappingException { if ( builtIdentifierGenerator == null ) { - builtIdentifierGenerator = buildIdentifierGenerator( - identifierGeneratorFactory, - dialect, - rootClass - ); + builtIdentifierGenerator = DEFAULT_ID_GEN_STRATEGY.equals( getIdentifierGeneratorStrategy() ) + ? buildIdentifierGenerator( identifierGeneratorFactory, dialect, rootClass ) + : super.createGenerator( identifierGeneratorFactory, dialect, rootClass, property ); } return builtIdentifierGenerator; } @@ -685,32 +692,10 @@ private Generator buildIdentifierGenerator( IdentifierGeneratorFactory identifierGeneratorFactory, Dialect dialect, RootClass rootClass) throws MappingException { - final boolean hasCustomGenerator = ! DEFAULT_ID_GEN_STRATEGY.equals( getIdentifierGeneratorStrategy() ); - if ( hasCustomGenerator ) { - return super.createGenerator( identifierGeneratorFactory, dialect, rootClass ); - } - - final Class entityClass = rootClass.getMappedClass(); - 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 - if ( rootClass.getIdentifierMapper() != null ) { - // we have the @IdClass / case - attributeDeclarer = resolveComponentClass(); - } - else if ( rootClass.getIdentifierProperty() != null ) { - // we have the "@EmbeddedId" / case - attributeDeclarer = resolveComponentClass(); - } - else { - // we have the "straight up" embedded (again the Hibernate term) component identifier - attributeDeclarer = entityClass; - } - - final CompositeNestedGeneratedValueGenerator.GenerationContextLocator locator = - new StandardGenerationContextLocator( rootClass.getEntityName() ); - final CompositeNestedGeneratedValueGenerator generator = - new CompositeNestedGeneratedValueGenerator( locator, getType() ); + final CompositeNestedGeneratedValueGenerator generator = new CompositeNestedGeneratedValueGenerator( + new StandardGenerationContextLocator( rootClass.getEntityName() ), + getType() + ); final List properties = getProperties(); for ( int i = 0; i < properties.size(); i++ ) { @@ -723,8 +708,8 @@ else if ( rootClass.getIdentifierProperty() != null ) { // skip any 'assigned' generators, they would have been handled by // the StandardGenerationContextLocator generator.addGeneratedValuePlan( new ValueGenerationPlan( - value.createGenerator( identifierGeneratorFactory, dialect, rootClass ), - getType().isMutable() ? injector( property, attributeDeclarer ) : null, + value.createGenerator( identifierGeneratorFactory, dialect, rootClass, property ), + getType().isMutable() ? injector( property, getAttributeDeclarer( rootClass ) ) : null, i ) ); } @@ -732,6 +717,28 @@ else if ( rootClass.getIdentifierProperty() != null ) { } return generator; } + /** + * 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 / case + return resolveComponentClass(); + } + else if ( rootClass.getIdentifierProperty() != null ) { + // we have the "@EmbeddedId" / case + return resolveComponentClass(); + } + else { + // we have the "straight up" embedded (again the Hibernate term) + // component identifier: the entity class itself is the id class + return rootClass.getMappedClass(); + } + } private Setter injector(Property property, Class attributeDeclarer) { return property.getPropertyAccessStrategy( attributeDeclarer ) diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/KeyValue.java b/hibernate-core/src/main/java/org/hibernate/mapping/KeyValue.java index 8904f6d8953f..4bd56eca913e 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/KeyValue.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/KeyValue.java @@ -34,6 +34,14 @@ Generator createGenerator( Dialect dialect, RootClass rootClass); + default Generator createGenerator( + IdentifierGeneratorFactory identifierGeneratorFactory, + Dialect dialect, + RootClass rootClass, + Property property) { + return createGenerator( identifierGeneratorFactory, dialect, rootClass ); + } + /** * @deprecated Use {@link #createGenerator(IdentifierGeneratorFactory, Dialect, RootClass)} instead. * No longer used except in legacy tests. diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/SimpleValue.java b/hibernate-core/src/main/java/org/hibernate/mapping/SimpleValue.java index e2ca14baf5de..106e0fc5df81 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/SimpleValue.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/SimpleValue.java @@ -392,10 +392,19 @@ public Generator createGenerator( IdentifierGeneratorFactory identifierGeneratorFactory, Dialect dialect, RootClass rootClass) throws MappingException { + return createGenerator( identifierGeneratorFactory, dialect, rootClass, rootClass == null ? null : rootClass.getIdentifierProperty() ); + } + + @Override + public Generator createGenerator( + IdentifierGeneratorFactory identifierGeneratorFactory, + Dialect dialect, + RootClass rootClass, + Property property) throws MappingException { if ( generator == null ) { if ( customIdGeneratorCreator != null ) { generator = customIdGeneratorCreator.createGenerator( - new IdGeneratorCreationContext( identifierGeneratorFactory, null, null, rootClass ) + new IdGeneratorCreationContext( identifierGeneratorFactory, null, null, rootClass, property) ); } else { @@ -1080,12 +1089,19 @@ private class IdGeneratorCreationContext implements CustomIdGeneratorCreationCon private final String defaultCatalog; private final String defaultSchema; private final RootClass rootClass; - - public IdGeneratorCreationContext(IdentifierGeneratorFactory identifierGeneratorFactory, String defaultCatalog, String defaultSchema, RootClass rootClass) { + private final Property property; + + public IdGeneratorCreationContext( + IdentifierGeneratorFactory identifierGeneratorFactory, + String defaultCatalog, + String defaultSchema, + RootClass rootClass, + Property property) { this.identifierGeneratorFactory = identifierGeneratorFactory; this.defaultCatalog = defaultCatalog; this.defaultSchema = defaultSchema; this.rootClass = rootClass; + this.property = property; } @Override @@ -1125,7 +1141,7 @@ public PersistentClass getPersistentClass() { @Override public Property getProperty() { - return rootClass.getIdentifierProperty(); + return property; } // we could add these if it helps integrate old infrastructure diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/tenantid/TenantIdGeneratedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantid/TenantIdGeneratedTest.java new file mode 100644 index 000000000000..5af90fc64e96 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantid/TenantIdGeneratedTest.java @@ -0,0 +1,120 @@ +/* + * 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.tenantid; + +import org.hibernate.annotations.TenantId; +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.Jira; +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 jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; + +import static org.hibernate.cfg.SchemaToolingSettings.JAKARTA_HBM2DDL_DATABASE_ACTION; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@SessionFactory +@DomainModel(annotatedClasses = { TenantIdGeneratedTest.DisplayIdBE.class }) +@ServiceRegistry( + settings = { + @Setting(name = JAKARTA_HBM2DDL_DATABASE_ACTION, value = "create-drop") + } +) +@Jira("https://hibernate.atlassian.net/browse/HHH-19004") +public class TenantIdGeneratedTest implements SessionFactoryProducer { + + @AfterEach + public void cleanup(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Override + public SessionFactoryImplementor produceSessionFactory(MetadataImplementor model) { + final SessionFactoryBuilder sessionFactoryBuilder = model.getSessionFactoryBuilder(); + sessionFactoryBuilder.applyCurrentTenantIdentifierResolver( new CurrentTenantIdentifierResolver() { + @Override + public Long resolveCurrentTenantIdentifier() { + return 0L; + } + @Override + public boolean validateExistingCurrentSessions() { + return false; + } + } ); + return (SessionFactoryImplementor) sessionFactoryBuilder.build(); + } + + @Test + public void test(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.persist(new DisplayIdBE( new DisplayIdKeyBE( null, DisplayIdType.TYPE1 ), 1L )); + } ); + scope.inTransaction( session -> { + assertNotNull( session.find( DisplayIdBE.class, new DisplayIdKeyBE( 0L, DisplayIdType.TYPE1 ) ) ); + assertEquals( 1, session.createQuery("from DisplayIdBE", DisplayIdBE.class).getResultList().size() ); + } ); + } + + @Entity(name = "DisplayIdBE") + public static class DisplayIdBE { + + @EmbeddedId + private DisplayIdKeyBE id; + + @Column(name = "display_id_value", nullable = false) + private long displayIdValue; + + protected DisplayIdBE() { + } + + public DisplayIdBE(DisplayIdKeyBE id, long displayIdValue) { + this.id = id; + this.displayIdValue = displayIdValue; + } + } + + public enum DisplayIdType { + TYPE1, + TYPE2 + } + + @Embeddable + public static class DisplayIdKeyBE { + + @TenantId + @Column(name = "tenant_id", nullable = false) + private Long tenantId; + + @Column(name = "type", nullable = false) + @Enumerated(EnumType.STRING) + private DisplayIdType type; + + protected DisplayIdKeyBE() {} + + public DisplayIdKeyBE(Long tenantId, DisplayIdType type) { + this.tenantId = tenantId; + this.type = type; + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/tenantidpk/Account.java b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantidpk/Account.java new file mode 100644 index 000000000000..f21c14779402 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantidpk/Account.java @@ -0,0 +1,32 @@ +/* + * 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.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() {} +} \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/tenantidpk/Client.java b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantidpk/Client.java new file mode 100644 index 000000000000..872da48e7582 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantidpk/Client.java @@ -0,0 +1,39 @@ +/* + * 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.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 accounts = new HashSet<>(); + + public Client(String name) { + this.name = name; + } + + Client() {} +} + diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/tenantidpk/TenantPkTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantidpk/TenantPkTest.java new file mode 100644 index 000000000000..8520f3bda738 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantidpk/TenantPkTest.java @@ -0,0 +1,113 @@ +/* + * 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.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; + +@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() { + @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 ); + } +} \ No newline at end of file