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 3112e11a32..9a7ce014f5 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 @@ -37,7 +37,9 @@ public class TenantIdGeneration implements BeforeExecutionGenerator { private final Class propertyType; public TenantIdGeneration(TenantId annotation, Member member, GeneratorCreationContext context) { - entityName = context.getPersistentClass().getEntityName(); + entityName = context.getPersistentClass() == null + ? member.getDeclaringClass().getName() //it's an attribute of an embeddable + : context.getPersistentClass().getEntityName(); propertyName = context.getProperty().getName(); propertyType = getPropertyType( member ); } diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/entity/CompositeGeneratorBuilder.java b/hibernate-core/src/main/java/org/hibernate/tuple/entity/CompositeGeneratorBuilder.java index e5e9fd3a88..aa507b68e8 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/entity/CompositeGeneratorBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/entity/CompositeGeneratorBuilder.java @@ -7,12 +7,16 @@ package org.hibernate.tuple.entity; import org.hibernate.dialect.Dialect; +import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.generator.BeforeExecutionGenerator; import org.hibernate.generator.EventType; import org.hibernate.generator.Generator; import org.hibernate.generator.OnExecutionGenerator; import org.hibernate.mapping.Component; import org.hibernate.mapping.Property; +import org.hibernate.metamodel.mapping.AttributeMapping; +import org.hibernate.metamodel.mapping.EmbeddableMappingType; +import org.hibernate.persister.entity.EntityPersister; import java.util.ArrayList; import java.util.EnumSet; @@ -25,125 +29,46 @@ import static org.hibernate.generator.EventTypeSets.NONE; * Handles value generation for composite properties. */ class CompositeGeneratorBuilder { + private final String entityName; private final Property mappingProperty; private final Dialect dialect; private boolean hadBeforeExecutionGeneration; private boolean hadOnExecutionGeneration; - private List onExecutionGenerators; + private final List generators = new ArrayList<>(); - public CompositeGeneratorBuilder(Property mappingProperty, Dialect dialect) { + public CompositeGeneratorBuilder(String entityName, Property mappingProperty, Dialect dialect) { + this.entityName = entityName; this.mappingProperty = mappingProperty; this.dialect = dialect; } public void add(Generator generator) { - if ( generator != null ) { + generators.add( generator ); + + if ( generator != null && generator.generatesSometimes() ) { if ( generator.generatedOnExecution() ) { - if ( generator instanceof OnExecutionGenerator ) { - add( (OnExecutionGenerator) generator ); - } + hadOnExecutionGeneration = true; } else { - if ( generator instanceof BeforeExecutionGenerator ) { - add( (BeforeExecutionGenerator) generator ); - } + hadBeforeExecutionGeneration = true; } } } - private void add(BeforeExecutionGenerator beforeExecutionGenerator) { - if ( beforeExecutionGenerator.generatesSometimes() ) { - hadBeforeExecutionGeneration = true; - } - } - - private void add(OnExecutionGenerator onExecutionGenerator) { - if ( onExecutionGenerators == null ) { - onExecutionGenerators = new ArrayList<>(); - } - onExecutionGenerators.add( onExecutionGenerator ); - - if ( onExecutionGenerator.generatesSometimes() ) { - hadOnExecutionGeneration = true; - } - } - public Generator build() { - if ( hadBeforeExecutionGeneration && hadOnExecutionGeneration) { + if ( hadBeforeExecutionGeneration && hadOnExecutionGeneration ) { throw new CompositeValueGenerationException( - "Composite attribute [" + mappingProperty.getName() + "] contained both in-memory" - + " and in-database value generation" + "Composite attribute contained both on-execution and before-execution generators: " + + mappingProperty.getName() ); } else if ( hadBeforeExecutionGeneration ) { - throw new UnsupportedOperationException("Composite in-memory value generation not supported"); - + return createCompositeBeforeExecutionGenerator(); } else if ( hadOnExecutionGeneration ) { - final Component composite = (Component) mappingProperty.getValue(); - - // we need the numbers to match up so that we can properly handle 'referenced sql column values' - if ( onExecutionGenerators.size() != composite.getPropertySpan() ) { - throw new CompositeValueGenerationException( - "Internal error : mismatch between number of collected in-db generation strategies" + - " and number of attributes for composite attribute : " + mappingProperty.getName() - ); - } - - // the base-line values for the aggregated OnExecutionGenerator we will build here. - final EnumSet eventTypes = EnumSet.noneOf(EventType.class); - boolean referenceColumns = false; - final String[] columnValues = new String[composite.getColumnSpan()]; - - // start building the aggregate values - int propertyIndex = -1; - int columnIndex = 0; - for ( Property property : composite.getProperties() ) { - propertyIndex++; - final OnExecutionGenerator generator = onExecutionGenerators.get( propertyIndex ); - eventTypes.addAll( generator.getEventTypes() ); - if ( generator.referenceColumnsInSql( dialect ) ) { - // override base-line value - referenceColumns = true; - final String[] referencedColumnValues = generator.getReferencedColumnValues( dialect ); - if ( referencedColumnValues != null ) { - final int span = property.getColumnSpan(); - if ( referencedColumnValues.length != span ) { - throw new CompositeValueGenerationException( - "Mismatch between number of collected generated column values and number of columns for composite attribute: " - + mappingProperty.getName() + '.' + property.getName() - ); - } - arraycopy( referencedColumnValues, 0, columnValues, columnIndex, span ); - } - } - } - final boolean referenceColumnsInSql = referenceColumns; - - // then use the aggregated values to build an OnExecutionGenerator - return new OnExecutionGenerator() { - @Override - public EnumSet getEventTypes() { - return eventTypes; - } - - @Override - public boolean referenceColumnsInSql(Dialect dialect) { - return referenceColumnsInSql; - } - - @Override - public String[] getReferencedColumnValues(Dialect dialect) { - return columnValues; - } - - @Override - public boolean writePropertyValue() { - return false; - } - }; + return createCompositeOnExecutionGenerator(); } else { return new Generator() { @@ -158,4 +83,121 @@ class CompositeGeneratorBuilder { }; } } + + private OnExecutionGenerator createCompositeOnExecutionGenerator() { + final Component composite = (Component) mappingProperty.getValue(); + + // the base-line values for the aggregated OnExecutionGenerator we will build here. + final EnumSet eventTypes = EnumSet.noneOf(EventType.class); + boolean referenceColumns = false; + final String[] columnValues = new String[composite.getColumnSpan()]; + + // start building the aggregate values + int columnIndex = 0; + final List properties = composite.getProperties(); + for ( int i = 0; i < properties.size(); i++ ) { + final Property property = properties.get(i); + final OnExecutionGenerator generator = (OnExecutionGenerator) generators.get(i); + if ( generator == null ) { + throw new CompositeValueGenerationException( + "Property of on-execution generated embeddable is not generated: " + + mappingProperty.getName() + '.' + property.getName() + ); + } + eventTypes.addAll( generator.getEventTypes() ); + if ( generator.referenceColumnsInSql( dialect ) ) { + // override base-line value + referenceColumns = true; + final String[] referencedColumnValues = generator.getReferencedColumnValues( dialect ); + if ( referencedColumnValues != null ) { + final int span = property.getColumnSpan(); + if ( referencedColumnValues.length != span ) { + throw new CompositeValueGenerationException( + "Mismatch between number of collected generated column values and number of columns for composite attribute: " + + mappingProperty.getName() + '.' + property.getName() + ); + } + arraycopy( referencedColumnValues, 0, columnValues, columnIndex, span ); + columnIndex += span; + } + } + } + final boolean referenceColumnsInSql = referenceColumns; + + // then use the aggregated values to build an OnExecutionGenerator + return new OnExecutionGenerator() { + @Override + public EnumSet getEventTypes() { + return eventTypes; + } + + @Override + public boolean referenceColumnsInSql(Dialect dialect) { + return referenceColumnsInSql; + } + + @Override + public String[] getReferencedColumnValues(Dialect dialect) { + return columnValues; + } + + @Override + public boolean writePropertyValue() { + return false; + } + }; + } + + private BeforeExecutionGenerator createCompositeBeforeExecutionGenerator() { + final Component composite = (Component) mappingProperty.getValue(); + final EnumSet eventTypes = EnumSet.noneOf(EventType.class); + final List properties = composite.getProperties(); + for ( int i = 0; i < properties.size(); i++ ) { + final Generator generator = generators.get(i); + if ( generator != null ) { + eventTypes.addAll( generator.getEventTypes() ); + } + } + return new BeforeExecutionGenerator() { + @Override + public Object generate(SharedSessionContractImplementor session, Object owner, Object currentValue, EventType eventType) { + final EntityPersister persister = session.getEntityPersister( entityName, owner ); + final int index = persister.getPropertyIndex( mappingProperty.getName() ); + final EmbeddableMappingType descriptor = + persister.getAttributeMapping(index).asEmbeddedAttributeMapping() + .getEmbeddableTypeDescriptor(); + final int size = properties.size(); + if ( currentValue == null ) { + final Object[] generatedValues = new Object[size]; + for ( int i = 0; i < size; i++ ) { + final Generator generator = generators.get(i); + if ( generator != null ) { + generatedValues[i] = ((BeforeExecutionGenerator) generator) + .generate( session, owner, null, eventType ); + } + } + return descriptor.getRepresentationStrategy().getInstantiator() + .instantiate( () -> generatedValues, session.getFactory() ); + } + else { + for ( int i = 0; i < size; i++ ) { + final Generator generator = generators.get(i); + if ( generator != null ) { + final AttributeMapping attributeMapping = descriptor.getAttributeMapping(i); + final Object value = attributeMapping.getPropertyAccess().getGetter().get( currentValue ); + final Object generatedValue = ((BeforeExecutionGenerator) generator) + .generate( session, owner, value, eventType ); + attributeMapping.getPropertyAccess().getSetter().set( currentValue, generatedValue ); + } + } + return currentValue; + } + } + + @Override + public EnumSet getEventTypes() { + return eventTypes; + } + }; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java b/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java index 673c316b6a..ee0a998bae 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java @@ -309,7 +309,7 @@ public class EntityMetamodel implements Serializable { // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // generated value strategies ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - final Generator generator = buildGenerator( property, creationContext ); + final Generator generator = buildGenerator( name, property, creationContext ); if ( generator != null ) { if ( i == tempVersionProperty && !generator.generatedOnExecution() ) { // when we have an in-memory generator for the version, we @@ -469,6 +469,7 @@ public class EntityMetamodel implements Serializable { } private static Generator buildGenerator( + final String entityName, final Property mappingProperty, final RuntimeModelCreationContext context) { final GeneratorCreator generatorCreator = mappingProperty.getValueGeneratorCreator(); @@ -480,7 +481,7 @@ public class EntityMetamodel implements Serializable { } if ( mappingProperty.getValue() instanceof Component ) { final Dialect dialect = context.getDialect(); - final CompositeGeneratorBuilder builder = new CompositeGeneratorBuilder( mappingProperty, dialect ); + final CompositeGeneratorBuilder builder = new CompositeGeneratorBuilder( entityName, mappingProperty, dialect ); final Component component = (Component) mappingProperty.getValue(); for ( Property property : component.getProperties() ) { builder.add( property.createGenerator( context ) ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/tenantid/Record.java b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantid/Record.java new file mode 100644 index 0000000000..479ecbebcf --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantid/Record.java @@ -0,0 +1,12 @@ +package org.hibernate.orm.test.tenantid; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +@Entity +public class Record { + @Id @GeneratedValue + public Long id; + public State state = new State(); +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/tenantid/State.java b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantid/State.java new file mode 100644 index 0000000000..589dff6ac1 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantid/State.java @@ -0,0 +1,14 @@ +package org.hibernate.orm.test.tenantid; + +import jakarta.persistence.Embeddable; +import org.hibernate.annotations.TenantId; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.Instant; + +@Embeddable +public class State { + public boolean deleted; + public @TenantId String tenantId; + public @UpdateTimestamp Instant updated; +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/tenantid/TenantIdTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantid/TenantIdTest.java index e470a6d18c..a4e2ed40b9 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/tenantid/TenantIdTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantid/TenantIdTest.java @@ -10,6 +10,7 @@ import org.hibernate.PropertyValueException; import org.hibernate.boot.SessionFactoryBuilder; import org.hibernate.boot.spi.MetadataImplementor; import org.hibernate.context.spi.CurrentTenantIdentifierResolver; +import org.hibernate.dialect.SybaseASEDialect; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.ServiceRegistry; @@ -18,6 +19,7 @@ import org.hibernate.testing.orm.junit.SessionFactoryProducer; import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.hibernate.testing.orm.junit.Setting; import org.hibernate.binder.internal.TenantIdBinder; +import org.hibernate.testing.orm.junit.SkipForDialect; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -26,7 +28,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertNotNull; @SessionFactory -@DomainModel(annotatedClasses = { Account.class, Client.class }) +@DomainModel(annotatedClasses = { Account.class, Client.class, Record.class }) @ServiceRegistry( settings = { @Setting(name = HBM2DDL_DATABASE_ACTION, value = "create-drop") @@ -124,4 +126,28 @@ public class TenantIdTest implements SessionFactoryProducer { assertEquals( "mine", acc.client.tenantId ); } ); } + + @Test + @SkipForDialect(dialectClass = SybaseASEDialect.class, + reason = "low timestamp precision on Sybase") + public void testEmbeddedTenantId(SessionFactoryScope scope) { + currentTenant = "mine"; + Record record = new Record(); + scope.inTransaction( s -> s.persist( record ) ); + assertEquals( "mine", record.state.tenantId ); + assertNotNull( record.state.updated ); + scope.inTransaction( s -> { + Record r = s.find( Record.class, record.id ); + assertEquals( "mine", r.state.tenantId ); + assertEquals( record.state.updated, r.state.updated ); + assertEquals( false, r.state.deleted ); + r.state.deleted = true; + } ); + scope.inTransaction( s -> { + Record r = s.find( Record.class, record.id ); + assertEquals( "mine", r.state.tenantId ); + assertNotEquals( record.state.updated, r.state.updated ); + assertEquals( true, r.state.deleted ); + } ); + } }