HHH-16133 allow before-execution generators for embeddable properties

and by side-effect allow @TenantId for embeddable properties
This commit is contained in:
Gavin King 2023-02-10 13:09:50 +01:00
parent 19c559dfb9
commit 9d254f4f8e
6 changed files with 194 additions and 97 deletions

View File

@ -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 );
}

View File

@ -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<OnExecutionGenerator> onExecutionGenerators;
private final List<Generator> 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<EventType> 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<EventType> 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<EventType> 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<Property> 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<EventType> 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<EventType> eventTypes = EnumSet.noneOf(EventType.class);
final List<Property> 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<EventType> getEventTypes() {
return eventTypes;
}
};
}
}

View File

@ -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 ) );

View File

@ -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();
}

View File

@ -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;
}

View File

@ -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 );
} );
}
}