From cb38afc773f38bc451a967fd6242bab964e6e655 Mon Sep 17 00:00:00 2001 From: Gavin King Date: Sat, 9 Nov 2024 10:52:48 +0100 Subject: [PATCH] move away from magical unsaved-value strings introduce NullValueSemantic Signed-off-by: Gavin King --- .../boot/model/internal/PropertyBinder.java | 2 +- .../source/internal/hbm/ModelBinder.java | 2 +- .../engine/internal/UnsavedValueFactory.java | 91 ++++++++++--------- .../java/org/hibernate/mapping/KeyValue.java | 8 ++ .../org/hibernate/mapping/SimpleValue.java | 67 +++++++++++++- 5 files changed, 120 insertions(+), 50 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyBinder.java index 8889cd07a3..a493dad7f1 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyBinder.java @@ -385,7 +385,7 @@ public class PropertyBinder { buildingContext ); rootClass.setIdentifier( identifier ); - identifier.setNullValue( "undefined" ); + identifier.setNullValueUndefined(); rootClass.setEmbeddedIdentifier( true ); rootClass.setIdentifierMapper( identifier ); return identifier; diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/ModelBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/ModelBinder.java index 6a90fe7293..c1f92c945d 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/ModelBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/ModelBinder.java @@ -923,7 +923,7 @@ public class ModelBinder { versionValue.setNullValue( versionAttributeSource.getUnsavedValue() ); } else { - versionValue.setNullValue( "undefined" ); + versionValue.setNullValueUndefined(); } if ( versionAttributeSource.getSource().equals("db") ) { property.setValueGeneratorCreator( diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/UnsavedValueFactory.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/UnsavedValueFactory.java index c6fb2a51f6..8672825817 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/UnsavedValueFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/UnsavedValueFactory.java @@ -6,10 +6,10 @@ package org.hibernate.engine.internal; import java.util.function.Supplier; -import org.hibernate.MappingException; import org.hibernate.engine.spi.IdentifierValue; import org.hibernate.engine.spi.VersionValue; import org.hibernate.mapping.KeyValue; +import org.hibernate.mapping.KeyValue.NullValueSemantic; import org.hibernate.property.access.spi.Getter; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.VersionJavaType; @@ -35,28 +35,31 @@ public class UnsavedValueFactory { JavaType idJavaType, Getter getter, Supplier templateInstanceAccess) { - final String unsavedValue = bootIdMapping.getNullValue(); - if ( unsavedValue == null ) { - if ( getter != null && templateInstanceAccess != null ) { - // use the id value of a newly instantiated instance as the unsaved-value - final Object defaultValue = getter.get( templateInstanceAccess.get() ); - return new IdentifierValue( defaultValue ); - } - else if ( idJavaType instanceof PrimitiveJavaType primitiveJavaType ) { - return new IdentifierValue( primitiveJavaType.getDefaultValue() ); - } - else { - return IdentifierValue.NULL; - } + final NullValueSemantic nullValueSemantic = bootIdMapping.getNullValueSemantic(); + return nullValueSemantic == null + ? inferUnsavedIdentifierValue( idJavaType, getter, templateInstanceAccess ) + : switch ( nullValueSemantic ) { + case UNDEFINED -> IdentifierValue.UNDEFINED; + case NULL -> IdentifierValue.NULL; + case ANY -> IdentifierValue.ANY; + case NONE -> IdentifierValue.NONE; + case VALUE -> new IdentifierValue( idJavaType.fromString( bootIdMapping.getNullValue() ) ); + default -> throw new IllegalArgumentException( "Illegal null-value semantic: " + nullValueSemantic ); + }; + } + + private static IdentifierValue inferUnsavedIdentifierValue( + JavaType idJavaType, Getter getter, Supplier templateInstanceAccess) { + if ( getter != null && templateInstanceAccess != null ) { + // use the id value of a newly instantiated instance as the unsaved-value + final Object defaultValue = getter.get( templateInstanceAccess.get() ); + return new IdentifierValue( defaultValue ); + } + else if ( idJavaType instanceof PrimitiveJavaType primitiveJavaType ) { + return new IdentifierValue( primitiveJavaType.getDefaultValue() ); } else { - return switch ( unsavedValue ) { - case "null" -> IdentifierValue.NULL; - case "undefined" -> IdentifierValue.UNDEFINED; - case "none" -> IdentifierValue.NONE; - case "any" -> IdentifierValue.ANY; - default -> new IdentifierValue( idJavaType.fromString( unsavedValue ) ); - }; + return IdentifierValue.NULL; } } @@ -72,31 +75,33 @@ public class UnsavedValueFactory { VersionJavaType versionJavaType, Getter getter, Supplier templateInstanceAccess) { - final String unsavedValue = bootVersionMapping.getNullValue(); - if ( unsavedValue == null ) { - if ( getter != null && templateInstanceAccess != null ) { - final Object defaultValue = getter.get( templateInstanceAccess.get() ); - // if the version of a newly instantiated object is null - // or a negative number, use that value as the unsaved-value, - // otherwise assume it's the initial version set by program - return isNullInitialVersion( defaultValue ) - ? new VersionValue( defaultValue ) - : VersionValue.UNDEFINED; - } - else { - return VersionValue.UNDEFINED; - } + final NullValueSemantic nullValueSemantic = bootVersionMapping.getNullValueSemantic(); + return nullValueSemantic == null + ? inferUnsavedVersionValue( versionJavaType, getter, templateInstanceAccess ) + : switch ( nullValueSemantic ) { + case UNDEFINED -> VersionValue.UNDEFINED; + case NULL -> VersionValue.NULL; + case NEGATIVE -> VersionValue.NEGATIVE; + // this should not happen since the DTD prevents it + case VALUE -> new VersionValue( versionJavaType.fromString( bootVersionMapping.getNullValue() ) ); + default -> throw new IllegalArgumentException( "Illegal null-value semantic: " + nullValueSemantic ); + }; + } + + private static VersionValue inferUnsavedVersionValue( + VersionJavaType versionJavaType, Getter getter, Supplier templateInstanceAccess) { + if ( getter != null && templateInstanceAccess != null ) { + final Object defaultValue = getter.get( templateInstanceAccess.get() ); + // if the version of a newly instantiated object is null + // or a negative number, use that value as the unsaved-value, + // otherwise assume it's the initial version set by program + return isNullInitialVersion( defaultValue ) + ? new VersionValue( defaultValue ) + : VersionValue.UNDEFINED; } else { - // this should not happen since the DTD prevents it - return switch ( unsavedValue ) { - case "undefined" -> VersionValue.UNDEFINED; - case "null" -> VersionValue.NULL; - case "negative" -> VersionValue.NEGATIVE; - default -> throw new MappingException( "Could not parse version unsaved-value: " + unsavedValue ); - }; + return VersionValue.UNDEFINED; } - } private UnsavedValueFactory() { diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/KeyValue.java b/hibernate-core/src/main/java/org/hibernate/mapping/KeyValue.java index 4118269d9e..e321cdb3c1 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/KeyValue.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/KeyValue.java @@ -22,10 +22,18 @@ public interface KeyValue extends Value { boolean isCascadeDeleteEnabled(); + enum NullValueSemantic { VALUE, NULL, NEGATIVE, UNDEFINED, NONE, ANY } + + NullValueSemantic getNullValueSemantic(); + String getNullValue(); boolean isUpdateable(); + /** + * @deprecated No longer called, except from tests. + * Use {@link #createGenerator(Dialect, RootClass, Property, GeneratorSettings)} + */ @Deprecated(since = "7.0", forRemoval = true) Generator createGenerator(Dialect dialect, RootClass rootClass); diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/SimpleValue.java b/hibernate-core/src/main/java/org/hibernate/mapping/SimpleValue.java index 623344ad5d..d4894c0136 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/SimpleValue.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/SimpleValue.java @@ -91,6 +91,7 @@ public abstract class SimpleValue implements KeyValue { private boolean isNationalized; private boolean isLob; + private NullValueSemantic nullValueSemantic; private String nullValue; private Table table; @@ -410,8 +411,8 @@ public abstract class SimpleValue implements KeyValue { final IdGeneratorCreationContext context = new IdGeneratorCreationContext( rootClass, property, defaults ); final Generator generator = customIdGeneratorCreator.createGenerator( context ); - if ( generator.allowAssignedIdentifiers() && getNullValue() == null ) { - setNullValue( "undefined" ); + if ( generator.allowAssignedIdentifiers() && nullValue == null ) { + setNullValueUndefined(); } return generator; } @@ -449,17 +450,73 @@ public abstract class SimpleValue implements KeyValue { return table; } + /** + * The property or field value which indicates that field + * or property has never been set. + * + * @see org.hibernate.engine.internal.UnsavedValueFactory + * @see org.hibernate.engine.spi.IdentifierValue + * @see org.hibernate.engine.spi.VersionValue + */ @Override public String getNullValue() { return nullValue; } /** - * Sets the nullValue. - * @param nullValue The nullValue to set + * Set the property or field value indicating that field + * or property has never been set. + * + * @see org.hibernate.engine.internal.UnsavedValueFactory + * @see org.hibernate.engine.spi.IdentifierValue + * @see org.hibernate.engine.spi.VersionValue */ public void setNullValue(String nullValue) { - this.nullValue = nullValue; + nullValueSemantic = switch (nullValue) { + // magical values (legacy of hbm.xml) + case "null" -> NullValueSemantic.NULL; + case "none" -> NullValueSemantic.NONE; + case "any" -> NullValueSemantic.ANY; + case "undefined" -> NullValueSemantic.UNDEFINED; + default -> NullValueSemantic.VALUE; + }; + if ( nullValueSemantic == NullValueSemantic.VALUE ) { + this.nullValue = nullValue; + } + } + + /** + * The rule for determining if the field or + * property has been set. + * + * @see org.hibernate.engine.internal.UnsavedValueFactory + */ + @Override + public NullValueSemantic getNullValueSemantic() { + return nullValueSemantic; + } + + /** + * Specifies the rule for determining if the field or + * property has been set. + * + * @see org.hibernate.engine.internal.UnsavedValueFactory + */ + public void setNullValueSemantic(NullValueSemantic nullValueSemantic) { + this.nullValueSemantic = nullValueSemantic; + } + + /** + * Specifies that there is no well-defined property or + * field value indicating that field or property has never + * been set. + * + * @see org.hibernate.engine.internal.UnsavedValueFactory + * @see org.hibernate.engine.spi.IdentifierValue#UNDEFINED + * @see org.hibernate.engine.spi.VersionValue#UNDEFINED + */ + public void setNullValueUndefined() { + nullValueSemantic = NullValueSemantic.UNDEFINED; } public String getForeignKeyName() {