HHH-16133 allow before-execution generators for embeddable properties
and by side-effect allow @TenantId for embeddable properties
This commit is contained in:
parent
19c559dfb9
commit
9d254f4f8e
|
@ -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 );
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 ) );
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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 );
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue