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;
}
@Override
public boolean allowMutation() {
return true;
}
@Override
public EnumSet<EventType> getEventTypes() {
return EventTypeSets.NONE;

View File

@ -70,16 +70,7 @@ class CompositeGeneratorBuilder {
return createCompositeOnExecutionGenerator();
}
else {
return new Generator() {
@Override
public EnumSet<EventType> getEventTypes() {
return NONE;
}
@Override
public boolean generatedOnExecution() {
return false;
}
};
return DummyGenerator.INSTANCE;
}
}
@ -89,6 +80,8 @@ class CompositeGeneratorBuilder {
// the base-line values for the aggregated OnExecutionGenerator we will build here.
final EnumSet<EventType> eventTypes = EnumSet.noneOf(EventType.class);
boolean referenceColumns = false;
boolean writable = false;
boolean mutable = false;
final String[] columnValues = new String[composite.getColumnSpan()];
// start building the aggregate values
@ -120,31 +113,16 @@ class CompositeGeneratorBuilder {
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
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 new CompositeOnExecutionGenerator( eventTypes, referenceColumns, columnValues, writable, mutable );
}
private BeforeExecutionGenerator createCompositeBeforeExecutionGenerator() {
@ -157,45 +135,98 @@ class CompositeGeneratorBuilder {
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 Object value = descriptor.getValue( currentValue, i );
final Object generatedValue = ((BeforeExecutionGenerator) generator)
.generate( session, owner, value, eventType );
descriptor.setValue( currentValue, i, generatedValue );
}
}
return currentValue;
}
}
return new CompositeBeforeExecutionGenerator( entityName, generators, mappingProperty, properties, eventTypes );
}
@Override
public EnumSet<EventType> getEventTypes() {
return 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
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 Object value = descriptor.getValue( currentValue, i );
final Object generatedValue = ((BeforeExecutionGenerator) generator)
.generate( session, owner, value, eventType );
descriptor.setValue( currentValue, i, generatedValue );
}
}
return currentValue;
}
}
}
private record DummyGenerator() implements Generator {
private static final Generator INSTANCE = new DummyGenerator();
@Override
public EnumSet<EventType> getEventTypes() {
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;
BeforeExecutionGenerator tempVersionGenerator = null;
List<Property> props = persistentClass.getPropertyClosure();
final List<Property> props = persistentClass.getPropertyClosure();
for ( int i=0; i<props.size(); i++ ) {
Property property = props.get(i);
final Property property = props.get(i);
final NonIdentifierAttribute attribute;
if ( property == persistentClass.getVersion() ) {
tempVersionProperty = i;
@ -310,9 +310,11 @@ public class EntityMetamodel implements Serializable {
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// generated value strategies ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
final Generator generator = buildGenerator( name, property, creationContext );
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
// want to plug it in to the older infrastructure specific
// to version generation, instead of treating it like a
@ -321,33 +323,33 @@ public class EntityMetamodel implements Serializable {
}
else {
generators[i] = generator;
if ( !generator.allowMutation() ) {
propertyInsertability[i] = false;
propertyUpdateability[i] = false;
final boolean allowMutation = generator.allowMutation();
if ( !allowMutation ) {
propertyCheckability[i] = false;
}
if ( generator.generatesOnInsert() ) {
propertyInsertability[i] = !generatedWithNoParameter( generator );
if ( generator.generatedOnExecution() ) {
foundPostInsertGeneratedValues = true;
if ( generator instanceof BeforeExecutionGenerator ) {
foundPreInsertGeneratedValues = true;
}
}
else {
foundPreInsertGeneratedValues = true;
if ( generatedOnExecution ) {
propertyInsertability[i] = writePropertyValue( (OnExecutionGenerator) generator );
}
foundPostInsertGeneratedValues = foundPostInsertGeneratedValues
|| generator instanceof OnExecutionGenerator;
foundPreInsertGeneratedValues = foundPreInsertGeneratedValues
|| generator instanceof BeforeExecutionGenerator;
}
else if ( !allowMutation ) {
propertyInsertability[i] = false;
}
if ( generator.generatesOnUpdate() ) {
propertyUpdateability[i] = !generatedWithNoParameter( generator );
if ( generator.generatedOnExecution() ) {
foundPostUpdateGeneratedValues = true;
if ( generator instanceof BeforeExecutionGenerator ) {
foundPreUpdateGeneratedValues = true;
}
}
else {
foundPreUpdateGeneratedValues = true;
if ( generatedOnExecution ) {
propertyUpdateability[i] = writePropertyValue( (OnExecutionGenerator) generator );
}
foundPostUpdateGeneratedValues = foundPostUpdateGeneratedValues
|| generator instanceof OnExecutionGenerator;
foundPreUpdateGeneratedValues = foundPreUpdateGeneratedValues
|| generator instanceof BeforeExecutionGenerator;
}
else if ( !allowMutation ) {
propertyUpdateability[i] = false;
}
}
}
@ -472,6 +474,15 @@ public class EntityMetamodel implements Serializable {
// 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) {
final Generator existing = creationContext.getGenerators().get( rootName );
if ( existing != null ) {
@ -513,11 +524,6 @@ public class EntityMetamodel implements Serializable {
return getName() + "." + property.getName();
}
private static boolean generatedWithNoParameter(Generator generator) {
return generator.generatedOnExecution()
&& !((OnExecutionGenerator) generator).writePropertyValue();
}
private static Generator buildGenerator(
final String entityName,
final Property mappingProperty,

View File

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

View File

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