[HHH-17294] DeepCopy non-Embedded JSON or XML JdbcTypCode attribute using FormatMapper

This commit is contained in:
The-Huginn 2023-11-08 14:10:12 +01:00 committed by Christian Beikov
parent 2e0f0c5b58
commit 47e827200e
10 changed files with 366 additions and 47 deletions

View File

@ -152,6 +152,7 @@ public class BasicValueBinder implements JdbcTypeIndicators {
private TemporalType temporalPrecision;
private TimeZoneStorageType timeZoneStorageType;
private boolean partitionKey;
private Integer jdbcTypeCode;
private Table table;
private AnnotatedColumns columns;
@ -1072,6 +1073,12 @@ public class BasicValueBinder implements JdbcTypeIndicators {
return null;
};
final org.hibernate.annotations.JdbcTypeCode jdbcType =
findAnnotation( attributeXProperty, org.hibernate.annotations.JdbcTypeCode.class );
if ( jdbcType != null ) {
jdbcTypeCode = jdbcType.value();
}
normalJdbcTypeDetails( attributeXProperty);
normalMutabilityDetails( attributeXProperty );
@ -1223,6 +1230,10 @@ public class BasicValueBinder implements JdbcTypeIndicators {
basicValue.setTemporalPrecision( temporalPrecision );
}
if ( jdbcTypeCode != null ) {
basicValue.setExplicitJdbcTypeCode( jdbcTypeCode );
}
linkWithValue();
boolean isInSecondPass = buildingContext.getMetadataCollector().isInSecondPass();

View File

@ -12,6 +12,7 @@ import java.util.Properties;
import java.util.function.Consumer;
import java.util.function.Function;
import org.hibernate.Incubating;
import org.hibernate.Internal;
import org.hibernate.MappingException;
import org.hibernate.TimeZoneStorageStrategy;
@ -46,6 +47,7 @@ import org.hibernate.resource.beans.spi.ManagedBeanRegistry;
import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation;
import org.hibernate.type.BasicType;
import org.hibernate.type.CustomType;
import org.hibernate.type.SqlTypes;
import org.hibernate.type.Type;
import org.hibernate.type.WrapperArrayHandling;
import org.hibernate.type.descriptor.converter.spi.BasicValueConverter;
@ -53,6 +55,10 @@ import org.hibernate.type.descriptor.java.BasicJavaType;
import org.hibernate.type.descriptor.java.BasicPluralJavaType;
import org.hibernate.type.descriptor.java.JavaType;
import org.hibernate.type.descriptor.java.MutabilityPlan;
import org.hibernate.type.descriptor.java.spi.JavaTypeRegistry;
import org.hibernate.type.descriptor.java.spi.JsonJavaType;
import org.hibernate.type.descriptor.java.spi.RegistryHelper;
import org.hibernate.type.descriptor.java.spi.XmlJavaType;
import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators;
import org.hibernate.type.internal.BasicTypeImpl;
@ -97,6 +103,7 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Resolved state - available after `#resolve`
private Resolution<?> resolution;
private Integer jdbcTypeCode;
public BasicValue(MetadataBuildingContext buildingContext) {
@ -495,15 +502,15 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol
private JavaType<?> determineJavaType(JavaType<?> explicitJavaType) {
JavaType<?> javaType = explicitJavaType;
if ( javaType == null ) {
if ( implicitJavaTypeAccess != null ) {
final java.lang.reflect.Type implicitJtd = implicitJavaTypeAccess.apply( getTypeConfiguration() );
if ( implicitJtd != null ) {
javaType = getTypeConfiguration().getJavaTypeRegistry().getDescriptor( implicitJtd );
}
}
}
//
// if ( javaType == null ) {
// if ( implicitJavaTypeAccess != null ) {
// final java.lang.reflect.Type implicitJtd = implicitJavaTypeAccess.apply( getTypeConfiguration() );
// if ( implicitJtd != null ) {
// javaType = getTypeConfiguration().getJavaTypeRegistry().getDescriptor( implicitJtd );
// }
// }
// }
if ( javaType == null ) {
final JavaType<?> reflectedJtd = determineReflectedJavaType();
@ -518,11 +525,12 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol
private JavaType<?> determineReflectedJavaType() {
final java.lang.reflect.Type impliedJavaType;
final TypeConfiguration typeConfiguration = getTypeConfiguration();
if ( resolvedJavaType != null ) {
impliedJavaType = resolvedJavaType;
}
else if ( implicitJavaTypeAccess != null ) {
impliedJavaType = implicitJavaTypeAccess.apply( getTypeConfiguration() );
impliedJavaType = implicitJavaTypeAccess.apply( typeConfiguration );
}
else if ( ownerName != null && propertyName != null ) {
impliedJavaType = ReflectHelper.reflectedPropertyType(
@ -541,7 +549,40 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol
return null;
}
return getTypeConfiguration().getJavaTypeRegistry().resolveDescriptor( impliedJavaType );
final JavaTypeRegistry javaTypeRegistry = typeConfiguration.getJavaTypeRegistry();
final JavaType<Object> javaType = javaTypeRegistry.findDescriptor( impliedJavaType );
final MutabilityPlan<Object> explicitMutabilityPlan = explicitMutabilityPlanAccess != null
? explicitMutabilityPlanAccess.apply( typeConfiguration )
: null;
final MutabilityPlan<Object> determinedMutabilityPlan = explicitMutabilityPlan != null
? explicitMutabilityPlan
: RegistryHelper.INSTANCE.determineMutabilityPlan( impliedJavaType, typeConfiguration );
if ( javaType == null ) {
if ( jdbcTypeCode != null ) {
// Construct special JavaType instances for JSON/XML types which can report recommended JDBC types
// and implement toString/fromString as well as copying based on FormatMapper operations
switch ( jdbcTypeCode ) {
case SqlTypes.JSON:
final JavaType<Object> jsonJavaType = new JsonJavaType<>(
impliedJavaType,
determinedMutabilityPlan,
typeConfiguration
);
javaTypeRegistry.addDescriptor( jsonJavaType );
return jsonJavaType;
case SqlTypes.SQLXML:
final JavaType<Object> xmlJavaType = new XmlJavaType<>(
impliedJavaType,
determinedMutabilityPlan,
typeConfiguration
);
javaTypeRegistry.addDescriptor( xmlJavaType );
return xmlJavaType;
}
}
return javaTypeRegistry.resolveDescriptor( impliedJavaType );
}
return javaType;
}
private static Resolution<?> interpretExplicitlyNamedType(
@ -871,6 +912,11 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol
return javaTypeClass == Byte[].class || javaTypeClass == Character[].class;
}
@Incubating
public void setExplicitJdbcTypeCode(Integer jdbcTypeCode) {
this.jdbcTypeCode = jdbcTypeCode;
}
/**
* Resolved form of {@link BasicValue} as part of interpreting the
* boot-time model into the run-time model

View File

@ -30,6 +30,7 @@ import org.hibernate.type.descriptor.WrapperOptions;
import org.hibernate.type.descriptor.java.AbstractClassJavaType;
import org.hibernate.type.descriptor.java.JavaType;
import org.hibernate.type.descriptor.java.MutabilityPlan;
import org.hibernate.type.descriptor.java.MutableMutabilityPlan;
import org.hibernate.type.descriptor.jdbc.JdbcLiteralFormatter;
import org.hibernate.type.descriptor.jdbc.JdbcType;
@ -242,7 +243,9 @@ public abstract class AbstractStandardBasicType<T>
}
protected final boolean isDirty(Object old, Object current) {
return !isSame( old, current );
// MutableMutabilityPlan.INSTANCE is a special plan for which we always have to assume the value is dirty,
// because we can't actually copy a value, but have no knowledge about the mutability of the java type
return getMutabilityPlan() == MutableMutabilityPlan.INSTANCE || !isSame( old, current );
}
@Override

View File

@ -0,0 +1,122 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.type.descriptor.java.spi;
import java.io.Serializable;
import java.lang.reflect.Type;
import org.hibernate.Incubating;
import org.hibernate.SharedSessionContract;
import org.hibernate.type.descriptor.WrapperOptions;
import org.hibernate.type.descriptor.java.AbstractJavaType;
import org.hibernate.type.descriptor.java.MutabilityPlan;
import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators;
import org.hibernate.type.format.FormatMapper;
import org.hibernate.type.spi.TypeConfiguration;
/**
* Java type for {@link FormatMapper} based types i.e. {@link org.hibernate.type.SqlTypes#JSON}
* or {@link org.hibernate.type.SqlTypes#SQLXML} mapped types.
*
* @author Christian Beikov
*/
@Incubating
public abstract class FormatMapperBasedJavaType<T> extends AbstractJavaType<T> implements MutabilityPlan<T> {
private final TypeConfiguration typeConfiguration;
public FormatMapperBasedJavaType(
Type type,
MutabilityPlan<T> mutabilityPlan,
TypeConfiguration typeConfiguration) {
super( type, mutabilityPlan );
this.typeConfiguration = typeConfiguration;
}
protected abstract FormatMapper getFormatMapper(TypeConfiguration typeConfiguration);
@Override
public JdbcType getRecommendedJdbcType(JdbcTypeIndicators context) {
throw new JdbcTypeRecommendationException(
"Could not determine recommended JdbcType for Java type '" + getJavaType().getTypeName() + "'"
);
}
@Override
public String toString(T value) {
return getFormatMapper( typeConfiguration ).toString(
value,
this,
typeConfiguration.getSessionFactory().getWrapperOptions()
);
}
@Override
public T fromString(CharSequence string) {
return getFormatMapper( typeConfiguration ).fromString(
string,
this,
typeConfiguration.getSessionFactory().getWrapperOptions()
);
}
@Override
public <X> X unwrap(T value, Class<X> type, WrapperOptions options) {
if ( type.isAssignableFrom( getJavaTypeClass() ) ) {
//noinspection unchecked
return (X) value;
}
else if ( type == String.class ) {
//noinspection unchecked
return (X) getFormatMapper( typeConfiguration ).toString( value, this, options );
}
throw new UnsupportedOperationException(
"Unwrap strategy not known for this Java type : " + getJavaType().getTypeName()
);
}
@Override
public <X> T wrap(X value, WrapperOptions options) {
if ( getJavaTypeClass().isInstance( value ) ) {
//noinspection unchecked
return (T) value;
}
else if ( value instanceof String ) {
return getFormatMapper( typeConfiguration ).fromString( (String) value, this, options );
}
throw new UnsupportedOperationException(
"Wrap strategy not known for this Java type : " + getJavaType().getTypeName()
);
}
@Override
public MutabilityPlan<T> getMutabilityPlan() {
final MutabilityPlan<T> mutabilityPlan = super.getMutabilityPlan();
return mutabilityPlan == null ? this : mutabilityPlan;
}
@Override
public boolean isMutable() {
return true;
}
@Override
public T deepCopy(T value) {
return fromString( toString( value ) );
}
@Override
public Serializable disassemble(T value, SharedSessionContract session) {
return toString( value );
}
@Override
public T assemble(Serializable cached, SharedSessionContract session) {
return fromString( (CharSequence) cached );
}
}

View File

@ -10,6 +10,7 @@ import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Supplier;
@ -110,6 +111,22 @@ public class JavaTypeRegistry implements JavaTypeBaseline.BaselineTarget, Serial
}
public <J> JavaType<J> resolveDescriptor(Type javaType) {
return resolveDescriptor( javaType, (elementJavaType, typeConfiguration) -> {
final MutabilityPlan<J> determinedPlan = RegistryHelper.INSTANCE.determineMutabilityPlan(
elementJavaType,
typeConfiguration
);
if ( determinedPlan != null ) {
return determinedPlan;
}
return MutableMutabilityPlan.INSTANCE;
} );
}
public <J> JavaType<J> resolveDescriptor(
Type javaType,
BiFunction<Type, TypeConfiguration, MutabilityPlan<?>> mutabilityPlanCreator) {
return resolveDescriptor(
javaType,
() -> {
@ -131,21 +148,10 @@ public class JavaTypeRegistry implements JavaTypeBaseline.BaselineTarget, Serial
elementTypeDescriptor = null;
}
if ( elementTypeDescriptor == null ) {
//noinspection unchecked
elementTypeDescriptor = RegistryHelper.INSTANCE.createTypeDescriptor(
elementJavaType,
() -> {
final MutabilityPlan<J> determinedPlan = RegistryHelper.INSTANCE.determineMutabilityPlan(
elementJavaType,
typeConfiguration
);
if ( determinedPlan != null ) {
return determinedPlan;
}
//noinspection unchecked
return (MutabilityPlan<J>) MutableMutabilityPlan.INSTANCE;
},
() -> (MutabilityPlan<J>) mutabilityPlanCreator.apply( elementJavaType, typeConfiguration ),
typeConfiguration
);
}

View File

@ -0,0 +1,43 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html.
*/
package org.hibernate.type.descriptor.java.spi;
import java.lang.reflect.Type;
import org.hibernate.Incubating;
import org.hibernate.type.SqlTypes;
import org.hibernate.type.descriptor.java.MutabilityPlan;
import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators;
import org.hibernate.type.format.FormatMapper;
import org.hibernate.type.spi.TypeConfiguration;
@Incubating
public class JsonJavaType<T> extends FormatMapperBasedJavaType<T> {
public JsonJavaType(
Type type,
MutabilityPlan<T> mutabilityPlan,
TypeConfiguration typeConfiguration) {
super( type, mutabilityPlan, typeConfiguration );
}
@Override
protected FormatMapper getFormatMapper(TypeConfiguration typeConfiguration) {
return typeConfiguration.getSessionFactory().getFastSessionServices().getJsonFormatMapper();
}
@Override
public JdbcType getRecommendedJdbcType(JdbcTypeIndicators context) {
return context.getJdbcType( SqlTypes.JSON );
}
@Override
public String toString() {
return "JsonJavaType(" + getJavaType().getTypeName() + ")";
}
}

View File

@ -64,11 +64,7 @@ public class RegistryHelper {
return typeConfiguration.createMutabilityPlan( annotation.value() );
}
if ( javaTypeClass.isEnum() ) {
return ImmutableMutabilityPlan.instance();
}
if ( javaTypeClass.isPrimitive() ) {
if ( javaTypeClass.isEnum() || javaTypeClass.isPrimitive() || ReflectHelper.isRecord( javaTypeClass ) ) {
return ImmutableMutabilityPlan.instance();
}

View File

@ -0,0 +1,43 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html.
*/
package org.hibernate.type.descriptor.java.spi;
import java.lang.reflect.Type;
import org.hibernate.Incubating;
import org.hibernate.type.SqlTypes;
import org.hibernate.type.descriptor.java.MutabilityPlan;
import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators;
import org.hibernate.type.format.FormatMapper;
import org.hibernate.type.spi.TypeConfiguration;
@Incubating
public class XmlJavaType<T> extends FormatMapperBasedJavaType<T> {
public XmlJavaType(
Type type,
MutabilityPlan<T> mutabilityPlan,
TypeConfiguration typeConfiguration) {
super( type, mutabilityPlan, typeConfiguration );
}
@Override
protected FormatMapper getFormatMapper(TypeConfiguration typeConfiguration) {
return typeConfiguration.getSessionFactory().getFastSessionServices().getXmlFormatMapper();
}
@Override
public JdbcType getRecommendedJdbcType(JdbcTypeIndicators context) {
return context.getJdbcType( SqlTypes.SQLXML );
}
@Override
public String toString() {
return "XmlJavaType(" + getJavaType().getTypeName() + ")";
}
}

View File

@ -466,4 +466,11 @@ public class Aggregate {
}
return Objects.equals( mutableValue, that.mutableValue );
}
@Override
public int hashCode() {
int result = Objects.hash(theBoolean, theNumericBoolean, theStringBoolean, theString, theInteger, theInt, theDouble, theUrl, theClob, theDate, theTime, theTimestamp, theInstant, theUuid, gender, convertedGender, ordinalGender, theDuration, theLocalDateTime, theLocalDate, theLocalTime, theZonedDateTime, theOffsetDateTime, mutableValue);
result = 31 * result + Arrays.hashCode(theBinary);
return result;
}
}

View File

@ -12,23 +12,24 @@ import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.testing.orm.junit.BaseSessionFactoryFunctionalTest;
import org.hibernate.testing.orm.junit.DialectFeatureChecks;
import org.hibernate.testing.orm.junit.JiraKey;
import org.hibernate.testing.orm.junit.RequiresDialectFeature;
import org.hibernate.testing.orm.junit.RequiresDialectFeature;
import org.hibernate.type.SqlTypes;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
//@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonAggregate.class)
public class JsonAggregateTest extends BaseSessionFactoryFunctionalTest {
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonAggregate.class)
public class AggregateTest extends BaseSessionFactoryFunctionalTest {
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class<?>[] {
JsonHolder.class
JsonHolder.class, XmlHolder.class
};
}
@ -37,6 +38,7 @@ public class JsonAggregateTest extends BaseSessionFactoryFunctionalTest {
inTransaction(
session -> {
session.persist( new JsonHolder( 1L, Aggregate.createAggregate2() ) );
session.persist( new XmlHolder( 1L, Aggregate.createAggregate2() ) );
}
);
}
@ -46,27 +48,41 @@ public class JsonAggregateTest extends BaseSessionFactoryFunctionalTest {
inTransaction(
session -> {
session.createMutationQuery( "delete from JsonHolder h" ).executeUpdate();
session.createMutationQuery( "delete from XmlHolder h" ).executeUpdate();
}
);
}
@Test
@JiraKey("HHH-17294")
public void testDirtyChecking() {
public void testDirtyCheckingJsonAggregate() {
sessionFactoryScope().inTransaction(
entityManager -> {
JsonHolder jsonHolder = entityManager.find( JsonHolder.class, 1L );
assertEquals("String 'abc'", jsonHolder.getAggregate().getTheString());
jsonHolder.getAggregate().setTheString( "MyString" );
JsonHolder aggregateHolder = entityManager.find( JsonHolder.class, 1L );
Assertions.assertEquals("String 'abc'", aggregateHolder.getAggregate().getTheString());
aggregateHolder.getAggregate().setTheString( "MyString" );
entityManager.flush();
entityManager.clear();
// Fails, when it should pass
assertEquals( "String 'MyString'", entityManager.find( JsonHolder.class, 1L ).getAggregate().getTheString() );
Assertions.assertEquals( "MyString", entityManager.find( JsonHolder.class, 1L ).getAggregate().getTheString() );
}
);
}
@Test
@JiraKey("HHH-17294")
public void testDirtyCheckingXmlAggregate() {
sessionFactoryScope().inTransaction(
entityManager -> {
XmlHolder aggregateHolder = entityManager.find( XmlHolder.class, 1L );
Assertions.assertEquals("String 'abc'", aggregateHolder.getAggregate().getTheString());
aggregateHolder.getAggregate().setTheString( "MyString" );
entityManager.flush();
entityManager.clear();
Assertions.assertEquals( "MyString", entityManager.find( XmlHolder.class, 1L ).getAggregate().getTheString() );
}
);
}
//tag::json-type-mapping-example[]
@Entity(name = "JsonHolder")
public static class JsonHolder {
@ -75,9 +91,6 @@ public class JsonAggregateTest extends BaseSessionFactoryFunctionalTest {
@JdbcTypeCode(SqlTypes.JSON)
private Aggregate aggregate;
//end::json-type-mapping-example[]
//Getters and setters are omitted for brevity
public JsonHolder() {
}
@ -101,10 +114,39 @@ public class JsonAggregateTest extends BaseSessionFactoryFunctionalTest {
public void setAggregate(Aggregate aggregate) {
this.aggregate = aggregate;
}
//tag::json-type-mapping-example[]
}
//end::json-type-mapping-example[]
@Entity(name = "XmlHolder")
public static class XmlHolder {
@Id
private Long id;
@JdbcTypeCode(SqlTypes.SQLXML)
private Aggregate aggregate;
public XmlHolder() {
}
public XmlHolder(Long id, Aggregate aggregate) {
this.id = id;
this.aggregate = aggregate;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Aggregate getAggregate() {
return aggregate;
}
public void setAggregate(Aggregate aggregate) {
this.aggregate = aggregate;
}
}
}