HHH-16133 allow before-execution generators for embeddable properties
and by side-effect allow @TenantId for embeddable properties
This commit is contained in:
parent
48682d4104
commit
b3b293578e
|
@ -37,7 +37,9 @@ public class TenantIdGeneration implements BeforeExecutionGenerator {
|
||||||
private final Class<?> propertyType;
|
private final Class<?> propertyType;
|
||||||
|
|
||||||
public TenantIdGeneration(TenantId annotation, Member member, GeneratorCreationContext context) {
|
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();
|
propertyName = context.getProperty().getName();
|
||||||
propertyType = getPropertyType( member );
|
propertyType = getPropertyType( member );
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,12 +7,16 @@
|
||||||
package org.hibernate.tuple.entity;
|
package org.hibernate.tuple.entity;
|
||||||
|
|
||||||
import org.hibernate.dialect.Dialect;
|
import org.hibernate.dialect.Dialect;
|
||||||
|
import org.hibernate.engine.spi.SharedSessionContractImplementor;
|
||||||
import org.hibernate.generator.BeforeExecutionGenerator;
|
import org.hibernate.generator.BeforeExecutionGenerator;
|
||||||
import org.hibernate.generator.EventType;
|
import org.hibernate.generator.EventType;
|
||||||
import org.hibernate.generator.Generator;
|
import org.hibernate.generator.Generator;
|
||||||
import org.hibernate.generator.OnExecutionGenerator;
|
import org.hibernate.generator.OnExecutionGenerator;
|
||||||
import org.hibernate.mapping.Component;
|
import org.hibernate.mapping.Component;
|
||||||
import org.hibernate.mapping.Property;
|
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.ArrayList;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
|
@ -25,72 +29,63 @@ import static org.hibernate.generator.EventTypeSets.NONE;
|
||||||
* Handles value generation for composite properties.
|
* Handles value generation for composite properties.
|
||||||
*/
|
*/
|
||||||
class CompositeGeneratorBuilder {
|
class CompositeGeneratorBuilder {
|
||||||
|
private final String entityName;
|
||||||
private final Property mappingProperty;
|
private final Property mappingProperty;
|
||||||
private final Dialect dialect;
|
private final Dialect dialect;
|
||||||
|
|
||||||
private boolean hadBeforeExecutionGeneration;
|
private boolean hadBeforeExecutionGeneration;
|
||||||
private boolean hadOnExecutionGeneration;
|
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.mappingProperty = mappingProperty;
|
||||||
this.dialect = dialect;
|
this.dialect = dialect;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void add(Generator generator) {
|
public void add(Generator generator) {
|
||||||
if ( generator != null ) {
|
generators.add( generator );
|
||||||
|
|
||||||
|
if ( generator != null && generator.generatesSometimes() ) {
|
||||||
if ( generator.generatedOnExecution() ) {
|
if ( generator.generatedOnExecution() ) {
|
||||||
if ( generator instanceof OnExecutionGenerator ) {
|
hadOnExecutionGeneration = true;
|
||||||
add( (OnExecutionGenerator) generator );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if ( generator instanceof BeforeExecutionGenerator ) {
|
|
||||||
add( (BeforeExecutionGenerator) generator );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void add(BeforeExecutionGenerator beforeExecutionGenerator) {
|
|
||||||
if ( beforeExecutionGenerator.generatesSometimes() ) {
|
|
||||||
hadBeforeExecutionGeneration = true;
|
hadBeforeExecutionGeneration = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void add(OnExecutionGenerator onExecutionGenerator) {
|
|
||||||
if ( onExecutionGenerators == null ) {
|
|
||||||
onExecutionGenerators = new ArrayList<>();
|
|
||||||
}
|
|
||||||
onExecutionGenerators.add( onExecutionGenerator );
|
|
||||||
|
|
||||||
if ( onExecutionGenerator.generatesSometimes() ) {
|
|
||||||
hadOnExecutionGeneration = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Generator build() {
|
public Generator build() {
|
||||||
if ( hadBeforeExecutionGeneration && hadOnExecutionGeneration) {
|
if ( hadBeforeExecutionGeneration && hadOnExecutionGeneration ) {
|
||||||
throw new CompositeValueGenerationException(
|
throw new CompositeValueGenerationException(
|
||||||
"Composite attribute [" + mappingProperty.getName() + "] contained both in-memory"
|
"Composite attribute contained both on-execution and before-execution generators: "
|
||||||
+ " and in-database value generation"
|
+ mappingProperty.getName()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
else if ( hadBeforeExecutionGeneration ) {
|
else if ( hadBeforeExecutionGeneration ) {
|
||||||
throw new UnsupportedOperationException("Composite in-memory value generation not supported");
|
return createCompositeBeforeExecutionGenerator();
|
||||||
|
|
||||||
}
|
}
|
||||||
else if ( hadOnExecutionGeneration ) {
|
else if ( hadOnExecutionGeneration ) {
|
||||||
final Component composite = (Component) mappingProperty.getValue();
|
return createCompositeOnExecutionGenerator();
|
||||||
|
|
||||||
// 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()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
return new Generator() {
|
||||||
|
@Override
|
||||||
|
public EnumSet<EventType> getEventTypes() {
|
||||||
|
return NONE;
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean generatedOnExecution() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private OnExecutionGenerator createCompositeOnExecutionGenerator() {
|
||||||
|
final Component composite = (Component) mappingProperty.getValue();
|
||||||
|
|
||||||
// the base-line values for the aggregated OnExecutionGenerator we will build here.
|
// the base-line values for the aggregated OnExecutionGenerator we will build here.
|
||||||
final EnumSet<EventType> eventTypes = EnumSet.noneOf(EventType.class);
|
final EnumSet<EventType> eventTypes = EnumSet.noneOf(EventType.class);
|
||||||
|
@ -98,11 +93,17 @@ class CompositeGeneratorBuilder {
|
||||||
final String[] columnValues = new String[composite.getColumnSpan()];
|
final String[] columnValues = new String[composite.getColumnSpan()];
|
||||||
|
|
||||||
// start building the aggregate values
|
// start building the aggregate values
|
||||||
int propertyIndex = -1;
|
|
||||||
int columnIndex = 0;
|
int columnIndex = 0;
|
||||||
for ( Property property : composite.getProperties() ) {
|
final List<Property> properties = composite.getProperties();
|
||||||
propertyIndex++;
|
for ( int i = 0; i < properties.size(); i++ ) {
|
||||||
final OnExecutionGenerator generator = onExecutionGenerators.get( propertyIndex );
|
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() );
|
eventTypes.addAll( generator.getEventTypes() );
|
||||||
if ( generator.referenceColumnsInSql( dialect ) ) {
|
if ( generator.referenceColumnsInSql( dialect ) ) {
|
||||||
// override base-line value
|
// override base-line value
|
||||||
|
@ -117,6 +118,7 @@ class CompositeGeneratorBuilder {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
arraycopy( referencedColumnValues, 0, columnValues, columnIndex, span );
|
arraycopy( referencedColumnValues, 0, columnValues, columnIndex, span );
|
||||||
|
columnIndex += span;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -145,17 +147,57 @@ class CompositeGeneratorBuilder {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
else {
|
||||||
return new Generator() {
|
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
|
@Override
|
||||||
public EnumSet<EventType> getEventTypes() {
|
public EnumSet<EventType> getEventTypes() {
|
||||||
return NONE;
|
return eventTypes;
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public boolean generatedOnExecution() {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -309,7 +309,7 @@ public class EntityMetamodel implements Serializable {
|
||||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
// generated value strategies ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
// generated value strategies ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
final Generator generator = buildGenerator( property, creationContext );
|
final Generator generator = buildGenerator( name, property, creationContext );
|
||||||
if ( generator != null ) {
|
if ( generator != null ) {
|
||||||
if ( i == tempVersionProperty && !generator.generatedOnExecution() ) {
|
if ( i == tempVersionProperty && !generator.generatedOnExecution() ) {
|
||||||
// when we have an in-memory generator for the version, we
|
// when we have an in-memory generator for the version, we
|
||||||
|
@ -469,6 +469,7 @@ public class EntityMetamodel implements Serializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Generator buildGenerator(
|
private static Generator buildGenerator(
|
||||||
|
final String entityName,
|
||||||
final Property mappingProperty,
|
final Property mappingProperty,
|
||||||
final RuntimeModelCreationContext context) {
|
final RuntimeModelCreationContext context) {
|
||||||
final GeneratorCreator generatorCreator = mappingProperty.getValueGeneratorCreator();
|
final GeneratorCreator generatorCreator = mappingProperty.getValueGeneratorCreator();
|
||||||
|
@ -480,7 +481,7 @@ public class EntityMetamodel implements Serializable {
|
||||||
}
|
}
|
||||||
if ( mappingProperty.getValue() instanceof Component ) {
|
if ( mappingProperty.getValue() instanceof Component ) {
|
||||||
final Dialect dialect = context.getDialect();
|
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();
|
final Component component = (Component) mappingProperty.getValue();
|
||||||
for ( Property property : component.getProperties() ) {
|
for ( Property property : component.getProperties() ) {
|
||||||
builder.add( property.createGenerator( context ) );
|
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.SessionFactoryBuilder;
|
||||||
import org.hibernate.boot.spi.MetadataImplementor;
|
import org.hibernate.boot.spi.MetadataImplementor;
|
||||||
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
|
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
|
||||||
|
import org.hibernate.dialect.SybaseASEDialect;
|
||||||
import org.hibernate.engine.spi.SessionFactoryImplementor;
|
import org.hibernate.engine.spi.SessionFactoryImplementor;
|
||||||
import org.hibernate.testing.orm.junit.DomainModel;
|
import org.hibernate.testing.orm.junit.DomainModel;
|
||||||
import org.hibernate.testing.orm.junit.ServiceRegistry;
|
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.SessionFactoryScope;
|
||||||
import org.hibernate.testing.orm.junit.Setting;
|
import org.hibernate.testing.orm.junit.Setting;
|
||||||
import org.hibernate.binder.internal.TenantIdBinder;
|
import org.hibernate.binder.internal.TenantIdBinder;
|
||||||
|
import org.hibernate.testing.orm.junit.SkipForDialect;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.Test;
|
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;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
|
||||||
@SessionFactory
|
@SessionFactory
|
||||||
@DomainModel(annotatedClasses = { Account.class, Client.class })
|
@DomainModel(annotatedClasses = { Account.class, Client.class, Record.class })
|
||||||
@ServiceRegistry(
|
@ServiceRegistry(
|
||||||
settings = {
|
settings = {
|
||||||
@Setting(name = HBM2DDL_DATABASE_ACTION, value = "create-drop")
|
@Setting(name = HBM2DDL_DATABASE_ACTION, value = "create-drop")
|
||||||
|
@ -124,4 +126,28 @@ public class TenantIdTest implements SessionFactoryProducer {
|
||||||
assertEquals( "mine", acc.client.tenantId );
|
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