[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 0e54e84255
commit 633412097f
9 changed files with 348 additions and 47 deletions

View File

@ -47,6 +47,7 @@ import org.hibernate.resource.beans.spi.ManagedBeanRegistry;
import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation;
import org.hibernate.type.BasicType; import org.hibernate.type.BasicType;
import org.hibernate.type.CustomType; import org.hibernate.type.CustomType;
import org.hibernate.type.SqlTypes;
import org.hibernate.type.Type; import org.hibernate.type.Type;
import org.hibernate.type.WrapperArrayHandling; import org.hibernate.type.WrapperArrayHandling;
import org.hibernate.type.descriptor.converter.spi.BasicValueConverter; import org.hibernate.type.descriptor.converter.spi.BasicValueConverter;
@ -54,6 +55,10 @@ import org.hibernate.type.descriptor.java.BasicJavaType;
import org.hibernate.type.descriptor.java.BasicPluralJavaType; import org.hibernate.type.descriptor.java.BasicPluralJavaType;
import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.JavaType;
import org.hibernate.type.descriptor.java.MutabilityPlan; 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.JdbcType;
import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators;
import org.hibernate.type.internal.BasicTypeImpl; import org.hibernate.type.internal.BasicTypeImpl;
@ -525,15 +530,15 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol
private JavaType<?> determineJavaType(JavaType<?> explicitJavaType) { private JavaType<?> determineJavaType(JavaType<?> explicitJavaType) {
JavaType<?> javaType = explicitJavaType; JavaType<?> javaType = explicitJavaType;
//
if ( javaType == null ) { // if ( javaType == null ) {
if ( implicitJavaTypeAccess != null ) { // if ( implicitJavaTypeAccess != null ) {
final java.lang.reflect.Type implicitJtd = implicitJavaTypeAccess.apply( getTypeConfiguration() ); // final java.lang.reflect.Type implicitJtd = implicitJavaTypeAccess.apply( getTypeConfiguration() );
if ( implicitJtd != null ) { // if ( implicitJtd != null ) {
javaType = getTypeConfiguration().getJavaTypeRegistry().getDescriptor( implicitJtd ); // javaType = getTypeConfiguration().getJavaTypeRegistry().getDescriptor( implicitJtd );
} // }
} // }
} // }
if ( javaType == null ) { if ( javaType == null ) {
final JavaType<?> reflectedJtd = determineReflectedJavaType(); final JavaType<?> reflectedJtd = determineReflectedJavaType();
@ -548,11 +553,12 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol
private JavaType<?> determineReflectedJavaType() { private JavaType<?> determineReflectedJavaType() {
final java.lang.reflect.Type impliedJavaType; final java.lang.reflect.Type impliedJavaType;
final TypeConfiguration typeConfiguration = getTypeConfiguration();
if ( resolvedJavaType != null ) { if ( resolvedJavaType != null ) {
impliedJavaType = resolvedJavaType; impliedJavaType = resolvedJavaType;
} }
else if ( implicitJavaTypeAccess != null ) { else if ( implicitJavaTypeAccess != null ) {
impliedJavaType = implicitJavaTypeAccess.apply( getTypeConfiguration() ); impliedJavaType = implicitJavaTypeAccess.apply( typeConfiguration );
} }
else if ( ownerName != null && propertyName != null ) { else if ( ownerName != null && propertyName != null ) {
impliedJavaType = ReflectHelper.reflectedPropertyType( impliedJavaType = ReflectHelper.reflectedPropertyType(
@ -571,7 +577,40 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol
return null; 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( private static Resolution<?> interpretExplicitlyNamedType(

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.AbstractClassJavaType;
import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.JavaType;
import org.hibernate.type.descriptor.java.MutabilityPlan; 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.JdbcLiteralFormatter;
import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcType;
@ -242,7 +243,9 @@ public abstract class AbstractStandardBasicType<T>
} }
protected final boolean isDirty(Object old, Object current) { 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 @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.ParameterizedType;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
@ -110,6 +111,22 @@ public class JavaTypeRegistry implements JavaTypeBaseline.BaselineTarget, Serial
} }
public <J> JavaType<J> resolveDescriptor(Type javaType) { 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( return resolveDescriptor(
javaType, javaType,
() -> { () -> {
@ -131,21 +148,10 @@ public class JavaTypeRegistry implements JavaTypeBaseline.BaselineTarget, Serial
elementTypeDescriptor = null; elementTypeDescriptor = null;
} }
if ( elementTypeDescriptor == null ) { if ( elementTypeDescriptor == null ) {
//noinspection unchecked
elementTypeDescriptor = RegistryHelper.INSTANCE.createTypeDescriptor( elementTypeDescriptor = RegistryHelper.INSTANCE.createTypeDescriptor(
elementJavaType, elementJavaType,
() -> { () -> (MutabilityPlan<J>) mutabilityPlanCreator.apply( elementJavaType, typeConfiguration ),
final MutabilityPlan<J> determinedPlan = RegistryHelper.INSTANCE.determineMutabilityPlan(
elementJavaType,
typeConfiguration
);
if ( determinedPlan != null ) {
return determinedPlan;
}
//noinspection unchecked
return (MutabilityPlan<J>) MutableMutabilityPlan.INSTANCE;
},
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

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