HHH-18815 @Generated should not imply @Immutable

Signed-off-by: Gavin King <gavin@hibernate.org>
This commit is contained in:
Gavin King 2024-11-06 00:27:41 +01:00
parent 51254568df
commit 5fca1206b2
7 changed files with 223 additions and 103 deletions

View File

@ -34,6 +34,11 @@ public class Assigned implements Generator {
return true; return true;
} }
@Override
public boolean allowMutation() {
return true;
}
@Override @Override
public EnumSet<EventType> getEventTypes() { public EnumSet<EventType> getEventTypes() {
return EventTypeSets.NONE; return EventTypeSets.NONE;

View File

@ -70,16 +70,7 @@ class CompositeGeneratorBuilder {
return createCompositeOnExecutionGenerator(); return createCompositeOnExecutionGenerator();
} }
else { else {
return new Generator() { return DummyGenerator.INSTANCE;
@Override
public EnumSet<EventType> getEventTypes() {
return NONE;
}
@Override
public boolean generatedOnExecution() {
return false;
}
};
} }
} }
@ -89,6 +80,8 @@ class CompositeGeneratorBuilder {
// 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);
boolean referenceColumns = false; boolean referenceColumns = false;
boolean writable = false;
boolean mutable = false;
final String[] columnValues = new String[composite.getColumnSpan()]; final String[] columnValues = new String[composite.getColumnSpan()];
// start building the aggregate values // start building the aggregate values
@ -120,31 +113,16 @@ class CompositeGeneratorBuilder {
columnIndex += span; columnIndex += span;
} }
} }
if ( generator.writePropertyValue() ) {
writable = true;
}
if ( generator.allowMutation() ) {
mutable = true;
}
} }
final boolean referenceColumnsInSql = referenceColumns;
// then use the aggregated values to build an OnExecutionGenerator // then use the aggregated values to build an OnExecutionGenerator
return new OnExecutionGenerator() { return new CompositeOnExecutionGenerator( eventTypes, referenceColumns, columnValues, writable, mutable );
@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() { private BeforeExecutionGenerator createCompositeBeforeExecutionGenerator() {
@ -157,19 +135,54 @@ class CompositeGeneratorBuilder {
eventTypes.addAll( generator.getEventTypes() ); eventTypes.addAll( generator.getEventTypes() );
} }
} }
return new BeforeExecutionGenerator() { return new CompositeBeforeExecutionGenerator( entityName, generators, mappingProperty, properties, eventTypes );
}
private record CompositeOnExecutionGenerator(
EnumSet<EventType> eventTypes,
boolean referenceColumnsInSql,
String[] columnValues,
boolean writePropertyValue,
boolean allowMutation)
implements OnExecutionGenerator {
@Override
public boolean referenceColumnsInSql(Dialect dialect) {
return referenceColumnsInSql;
}
@Override
public String[] getReferencedColumnValues(Dialect dialect) {
return columnValues;
}
@Override
public EnumSet<EventType> getEventTypes() {
return eventTypes;
}
}
private record CompositeBeforeExecutionGenerator(
String entityName,
List<Generator> generators,
Property mappingProperty,
List<Property> properties,
EnumSet<EventType> eventTypes)
implements BeforeExecutionGenerator {
@Override
public EnumSet<EventType> getEventTypes() {
return eventTypes;
}
@Override @Override
public Object generate(SharedSessionContractImplementor session, Object owner, Object currentValue, EventType eventType) { public Object generate(SharedSessionContractImplementor session, Object owner, Object currentValue, EventType eventType) {
final EntityPersister persister = session.getEntityPersister( entityName, owner ); final EntityPersister persister = session.getEntityPersister( entityName, owner );
final int index = persister.getPropertyIndex( mappingProperty.getName() ); final int index = persister.getPropertyIndex( mappingProperty.getName() );
final EmbeddableMappingType descriptor = final EmbeddableMappingType descriptor =
persister.getAttributeMapping(index).asEmbeddedAttributeMapping() persister.getAttributeMapping( index ).asEmbeddedAttributeMapping()
.getEmbeddableTypeDescriptor(); .getEmbeddableTypeDescriptor();
final int size = properties.size(); final int size = properties.size();
if ( currentValue == null ) { if ( currentValue == null ) {
final Object[] generatedValues = new Object[size]; final Object[] generatedValues = new Object[size];
for ( int i = 0; i < size; i++ ) { for ( int i = 0; i < size; i++ ) {
final Generator generator = generators.get(i); final Generator generator = generators.get( i );
if ( generator != null ) { if ( generator != null ) {
generatedValues[i] = ((BeforeExecutionGenerator) generator) generatedValues[i] = ((BeforeExecutionGenerator) generator)
.generate( session, owner, null, eventType ); .generate( session, owner, null, eventType );
@ -180,7 +193,7 @@ class CompositeGeneratorBuilder {
} }
else { else {
for ( int i = 0; i < size; i++ ) { for ( int i = 0; i < size; i++ ) {
final Generator generator = generators.get(i); final Generator generator = generators.get( i );
if ( generator != null ) { if ( generator != null ) {
final Object value = descriptor.getValue( currentValue, i ); final Object value = descriptor.getValue( currentValue, i );
final Object generatedValue = ((BeforeExecutionGenerator) generator) final Object generatedValue = ((BeforeExecutionGenerator) generator)
@ -191,11 +204,29 @@ class CompositeGeneratorBuilder {
return currentValue; return currentValue;
} }
} }
}
private record DummyGenerator() implements Generator {
private static final Generator INSTANCE = new DummyGenerator();
@Override @Override
public EnumSet<EventType> getEventTypes() { public EnumSet<EventType> getEventTypes() {
return eventTypes; return NONE;
}
@Override
public boolean generatedOnExecution() {
return false;
}
@Override
public boolean allowMutation() {
return true;
}
@Override
public boolean allowAssignedIdentifiers() {
return true;
} }
};
} }
} }

View File

@ -235,9 +235,9 @@ public class EntityMetamodel implements Serializable {
boolean foundUpdateableNaturalIdProperty = false; boolean foundUpdateableNaturalIdProperty = false;
BeforeExecutionGenerator tempVersionGenerator = null; BeforeExecutionGenerator tempVersionGenerator = null;
List<Property> props = persistentClass.getPropertyClosure(); final List<Property> props = persistentClass.getPropertyClosure();
for ( int i=0; i<props.size(); i++ ) { for ( int i=0; i<props.size(); i++ ) {
Property property = props.get(i); final Property property = props.get(i);
final NonIdentifierAttribute attribute; final NonIdentifierAttribute attribute;
if ( property == persistentClass.getVersion() ) { if ( property == persistentClass.getVersion() ) {
tempVersionProperty = i; tempVersionProperty = i;
@ -310,9 +310,11 @@ public class EntityMetamodel implements Serializable {
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// generated value strategies ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // generated value strategies ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
final Generator generator = buildGenerator( name, property, creationContext ); final Generator generator = buildGenerator( name, property, creationContext );
if ( generator != null ) { if ( generator != null ) {
if ( i == tempVersionProperty && !generator.generatedOnExecution() ) { final boolean generatedOnExecution = generator.generatedOnExecution();
if ( i == tempVersionProperty && !generatedOnExecution ) {
// when we have an in-memory generator for the version, we // when we have an in-memory generator for the version, we
// want to plug it in to the older infrastructure specific // want to plug it in to the older infrastructure specific
// to version generation, instead of treating it like a // to version generation, instead of treating it like a
@ -321,33 +323,33 @@ public class EntityMetamodel implements Serializable {
} }
else { else {
generators[i] = generator; generators[i] = generator;
if ( !generator.allowMutation() ) { final boolean allowMutation = generator.allowMutation();
propertyInsertability[i] = false; if ( !allowMutation ) {
propertyUpdateability[i] = false; propertyCheckability[i] = false;
} }
if ( generator.generatesOnInsert() ) { if ( generator.generatesOnInsert() ) {
propertyInsertability[i] = !generatedWithNoParameter( generator ); if ( generatedOnExecution ) {
if ( generator.generatedOnExecution() ) { propertyInsertability[i] = writePropertyValue( (OnExecutionGenerator) generator );
foundPostInsertGeneratedValues = true;
if ( generator instanceof BeforeExecutionGenerator ) {
foundPreInsertGeneratedValues = true;
} }
foundPostInsertGeneratedValues = foundPostInsertGeneratedValues
|| generator instanceof OnExecutionGenerator;
foundPreInsertGeneratedValues = foundPreInsertGeneratedValues
|| generator instanceof BeforeExecutionGenerator;
} }
else { else if ( !allowMutation ) {
foundPreInsertGeneratedValues = true; propertyInsertability[i] = false;
}
} }
if ( generator.generatesOnUpdate() ) { if ( generator.generatesOnUpdate() ) {
propertyUpdateability[i] = !generatedWithNoParameter( generator ); if ( generatedOnExecution ) {
if ( generator.generatedOnExecution() ) { propertyUpdateability[i] = writePropertyValue( (OnExecutionGenerator) generator );
foundPostUpdateGeneratedValues = true;
if ( generator instanceof BeforeExecutionGenerator ) {
foundPreUpdateGeneratedValues = true;
} }
foundPostUpdateGeneratedValues = foundPostUpdateGeneratedValues
|| generator instanceof OnExecutionGenerator;
foundPreUpdateGeneratedValues = foundPreUpdateGeneratedValues
|| generator instanceof BeforeExecutionGenerator;
} }
else { else if ( !allowMutation ) {
foundPreUpdateGeneratedValues = true; propertyUpdateability[i] = false;
}
} }
} }
} }
@ -472,6 +474,15 @@ public class EntityMetamodel implements Serializable {
// entityNameByInheritanceClassMap = toSmallMap( entityNameByInheritanceClassMapLocal ); // entityNameByInheritanceClassMap = toSmallMap( entityNameByInheritanceClassMapLocal );
} }
private static boolean writePropertyValue(OnExecutionGenerator generator) {
final boolean writePropertyValue = generator.writePropertyValue();
// TODO: move this validation somewhere else!
// if ( !writePropertyValue && generator instanceof BeforeExecutionGenerator ) {
// throw new HibernateException( "BeforeExecutionGenerator returned false from OnExecutionGenerator.writePropertyValue()" );
// }
return writePropertyValue;
}
private Generator buildIdGenerator(PersistentClass persistentClass, RuntimeModelCreationContext creationContext) { private Generator buildIdGenerator(PersistentClass persistentClass, RuntimeModelCreationContext creationContext) {
final Generator existing = creationContext.getGenerators().get( rootName ); final Generator existing = creationContext.getGenerators().get( rootName );
if ( existing != null ) { if ( existing != null ) {
@ -513,11 +524,6 @@ public class EntityMetamodel implements Serializable {
return getName() + "." + property.getName(); return getName() + "." + property.getName();
} }
private static boolean generatedWithNoParameter(Generator generator) {
return generator.generatedOnExecution()
&& !((OnExecutionGenerator) generator).writePropertyValue();
}
private static Generator buildGenerator( private static Generator buildGenerator(
final String entityName, final String entityName,
final Property mappingProperty, final Property mappingProperty,

View File

@ -11,6 +11,7 @@ import jakarta.persistence.Id;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import org.hibernate.annotations.DialectOverride; import org.hibernate.annotations.DialectOverride;
import org.hibernate.annotations.Generated; import org.hibernate.annotations.Generated;
import org.hibernate.annotations.Immutable;
import org.hibernate.annotations.SQLInsert; import org.hibernate.annotations.SQLInsert;
import org.hibernate.annotations.SQLUpdate; import org.hibernate.annotations.SQLUpdate;
import org.hibernate.dialect.H2Dialect; import org.hibernate.dialect.H2Dialect;
@ -82,7 +83,7 @@ public class CustomSqlOverrideTest {
static class Custom { static class Custom {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
Long id; Long id;
@Generated @Generated @Immutable
String uid; String uid;
String whatever; String whatever;
} }

View File

@ -44,13 +44,13 @@ public class DefaultTest {
assertEquals( unitPrice, entity.unitPrice ); assertEquals( unitPrice, entity.unitPrice );
assertEquals( 5, entity.quantity ); assertEquals( 5, entity.quantity );
assertEquals( "new", entity.status ); assertEquals( "new", entity.status );
entity.status = "old"; //should be ignored when fetch=true entity.status = "old";
} ); } );
scope.inTransaction( session -> { scope.inTransaction( session -> {
OrderLine entity = session.createQuery("from WithDefault", OrderLine.class ).getSingleResult(); OrderLine entity = session.createQuery("from WithDefault", OrderLine.class ).getSingleResult();
assertEquals( unitPrice, entity.unitPrice ); assertEquals( unitPrice, entity.unitPrice );
assertEquals( 5, entity.quantity ); assertEquals( 5, entity.quantity );
assertEquals( "new", entity.status ); assertEquals( "old", entity.status );
} ); } );
} }

View File

@ -0,0 +1,77 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.orm.test.mapping.generated.sqldefault;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.Generated;
import org.hibernate.annotations.Immutable;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import static org.junit.Assert.assertEquals;
/**
* @author Gavin King
*/
@SuppressWarnings("JUnitMalformedDeclaration")
@DomainModel(annotatedClasses = ImmutableDefaultTest.OrderLine.class)
@SessionFactory
public class ImmutableDefaultTest {
@Test
public void test(SessionFactoryScope scope) {
BigDecimal unitPrice = new BigDecimal("12.99");
scope.inTransaction( session -> {
OrderLine entity = new OrderLine( unitPrice, 5 );
session.persist(entity);
session.flush();
assertEquals( "new", entity.status );
assertEquals( unitPrice, entity.unitPrice );
assertEquals( 5, entity.quantity );
} );
scope.inTransaction( session -> {
OrderLine entity = session.createQuery("from WithDefault", OrderLine.class ).getSingleResult();
assertEquals( unitPrice, entity.unitPrice );
assertEquals( 5, entity.quantity );
assertEquals( "new", entity.status );
entity.status = "old"; //should be ignored due to @Immutable
} );
scope.inTransaction( session -> {
OrderLine entity = session.createQuery("from WithDefault", OrderLine.class ).getSingleResult();
assertEquals( unitPrice, entity.unitPrice );
assertEquals( 5, entity.quantity );
assertEquals( "new", entity.status );
} );
}
@AfterEach
public void dropTestData(SessionFactoryScope scope) {
scope.inTransaction( session -> session.createQuery( "delete WithDefault" ).executeUpdate() );
}
@Entity(name="WithDefault")
public static class OrderLine {
@Id
private BigDecimal unitPrice;
@Id @ColumnDefault(value = "1")
private int quantity;
@Generated @Immutable
@ColumnDefault(value = "'new'")
private String status;
public OrderLine() {}
public OrderLine(BigDecimal unitPrice, int quantity) {
this.unitPrice = unitPrice;
this.quantity = quantity;
}
}
}

View File

@ -52,7 +52,7 @@ public class OverriddenDefaultTest {
OrderLine entity = session.createQuery("from WithDefault", OrderLine.class ).getSingleResult(); OrderLine entity = session.createQuery("from WithDefault", OrderLine.class ).getSingleResult();
assertEquals( unitPrice, entity.unitPrice ); assertEquals( unitPrice, entity.unitPrice );
assertEquals( 5, entity.quantity ); assertEquals( 5, entity.quantity );
assertEquals( getDefault(scope), entity.status ); assertEquals( "old", entity.status );
} ); } );
} }