From c5d5bc192247d81f576f8e62d45d11c2f6b1f22d Mon Sep 17 00:00:00 2001 From: imunic Date: Tue, 12 Mar 2024 21:55:09 +0100 Subject: [PATCH] HHH-17840 Fix inconsistency of read/write null JsonNode/JsonValue --- .../community/dialect/H2LegacyDialect.java | 3 +- hibernate-core/hibernate-core.gradle | 4 +- .../java/org/hibernate/dialect/H2Dialect.java | 3 +- .../org/hibernate/dialect/H2JsonJdbcType.java | 67 +++++++++++++++++++ .../query/sqm/tree/expression/SqmLiteral.java | 17 +++-- .../java/org/hibernate/type/EnumType.java | 2 +- .../main/java/org/hibernate/type/Type.java | 46 +++++++------ .../type/descriptor/java/JavaType.java | 8 ++- .../type/descriptor/java/MutabilityPlan.java | 8 ++- .../descriptor/java/ObjectArrayJavaType.java | 14 +++- .../java/spi/BasicCollectionJavaType.java | 2 +- .../java/spi/FormatMapperBasedJavaType.java | 7 +- .../descriptor/jdbc/H2FormatJsonJdbcType.java | 2 + .../type/format/AbstractJsonFormatMapper.java | 41 ++++++++++++ .../jackson/JacksonJsonFormatMapper.java | 28 +++----- .../jakartajson/JsonBJsonFormatMapper.java | 27 +++----- .../test/mapping/basic/JsonMappingTests.java | 35 ++++++++++ 17 files changed, 235 insertions(+), 79 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/H2JsonJdbcType.java create mode 100644 hibernate-core/src/main/java/org/hibernate/type/format/AbstractJsonFormatMapper.java diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java index beae784bbc..133e12225a 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java @@ -70,7 +70,6 @@ import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorLe import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorNoOpImpl; import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; import org.hibernate.type.descriptor.jdbc.EnumJdbcType; -import org.hibernate.type.descriptor.jdbc.H2FormatJsonJdbcType; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.OrdinalEnumJdbcType; import org.hibernate.type.descriptor.jdbc.TimeAsTimestampWithTimeZoneJdbcType; @@ -294,7 +293,7 @@ public class H2LegacyDialect extends Dialect { jdbcTypeRegistry.addDescriptorIfAbsent( H2DurationIntervalSecondJdbcType.INSTANCE ); } if ( getVersion().isSameOrAfter( 1, 4, 200 ) ) { - jdbcTypeRegistry.addDescriptorIfAbsent( H2FormatJsonJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptorIfAbsent( H2JsonJdbcType.INSTANCE ); } jdbcTypeRegistry.addDescriptor( EnumJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptor( OrdinalEnumJdbcType.INSTANCE ); diff --git a/hibernate-core/hibernate-core.gradle b/hibernate-core/hibernate-core.gradle index 01a41dd760..689fddb9b7 100644 --- a/hibernate-core/hibernate-core.gradle +++ b/hibernate-core/hibernate-core.gradle @@ -66,8 +66,8 @@ dependencies { testRuntimeOnly libs.byteBuddy testRuntimeOnly testLibs.weld testRuntimeOnly testLibs.wildFlyTxnClient - testRuntimeOnly jakartaLibs.jsonb - testRuntimeOnly libs.jackson + testImplementation jakartaLibs.jsonb + testImplementation libs.jackson testRuntimeOnly libs.jacksonXml testRuntimeOnly libs.jacksonJsr310 diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java index ad38114ebd..622479b703 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -65,7 +65,6 @@ import org.hibernate.sql.model.internal.OptionalTableUpdate; import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorLegacyImpl; import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; import org.hibernate.type.descriptor.jdbc.EnumJdbcType; -import org.hibernate.type.descriptor.jdbc.H2FormatJsonJdbcType; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.OrdinalEnumJdbcType; import org.hibernate.type.descriptor.jdbc.TimeUtcAsOffsetTimeJdbcType; @@ -244,7 +243,7 @@ public class H2Dialect extends Dialect { jdbcTypeRegistry.addDescriptor( TimestampUtcAsInstantJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( UUIDJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( H2DurationIntervalSecondJdbcType.INSTANCE ); - jdbcTypeRegistry.addDescriptorIfAbsent( H2FormatJsonJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptorIfAbsent( H2JsonJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptor( EnumJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptor( OrdinalEnumJdbcType.INSTANCE ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2JsonJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2JsonJdbcType.java new file mode 100644 index 0000000000..b1816ee8cf --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2JsonJdbcType.java @@ -0,0 +1,67 @@ +/* + * 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.dialect; + +import java.nio.charset.StandardCharsets; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.hibernate.metamodel.mapping.EmbeddableMappingType; +import org.hibernate.metamodel.spi.RuntimeModelCreationContext; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.AggregateJdbcType; +import org.hibernate.type.descriptor.jdbc.BasicBinder; +import org.hibernate.type.descriptor.jdbc.JsonJdbcType; + +/** + * H2 requires binding JSON via {@code setBytes} methods. + */ +public class H2JsonJdbcType extends JsonJdbcType { + /** + * Singleton access + */ + public static final H2JsonJdbcType INSTANCE = new H2JsonJdbcType( null ); + + protected H2JsonJdbcType(EmbeddableMappingType embeddableMappingType) { + super( embeddableMappingType ); + } + + @Override + public String toString() { + return "FormatJsonJdbcType"; + } + + @Override + public AggregateJdbcType resolveAggregateJdbcType( + EmbeddableMappingType mappingType, + String sqlType, + RuntimeModelCreationContext creationContext) { + return new H2JsonJdbcType( mappingType ); + } + + @Override + public ValueBinder getBinder(JavaType javaType) { + return new BasicBinder<>( javaType, this ) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) + throws SQLException { + final String json = ( (H2JsonJdbcType) getJdbcType() ).toString( value, getJavaType(), options ); + st.setBytes( index, json.getBytes( StandardCharsets.UTF_8 ) ); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final String json = ( (H2JsonJdbcType) getJdbcType() ).toString( value, getJavaType(), options ); + st.setBytes( name, json.getBytes( StandardCharsets.UTF_8 ) ); + } + }; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmLiteral.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmLiteral.java index cc245c1aba..4b6cda0387 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmLiteral.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmLiteral.java @@ -13,6 +13,8 @@ import org.hibernate.query.sqm.SqmExpressible; import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.type.descriptor.java.JavaType; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * Represents a literal value in the sqm, e.g.
    *
  • 1
  • @@ -75,13 +77,18 @@ public class SqmLiteral extends AbstractSqmExpression { appendHqlString( sb, getJavaTypeDescriptor(), getLiteralValue() ); } - public static void appendHqlString(StringBuilder sb, JavaType javaType, T value) { - final String string = javaType.toString( value ); - if ( javaType.getJavaTypeClass() == String.class ) { - QueryLiteralHelper.appendStringLiteral( sb, string ); + public static void appendHqlString(StringBuilder sb, JavaType javaType, @Nullable T value) { + if ( value == null ) { + sb.append( "null" ); } else { - sb.append( string ); + final String string = javaType.toString( value ); + if ( javaType.getJavaTypeClass() == String.class ) { + QueryLiteralHelper.appendStringLiteral( sb, string ); + } + else { + sb.append( string ); + } } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/EnumType.java b/hibernate-core/src/main/java/org/hibernate/type/EnumType.java index 5367115069..cc0c1ff324 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/EnumType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/EnumType.java @@ -296,7 +296,7 @@ public class EnumType> @Override @SuppressWarnings("unchecked") public String toLoggableString(Object value, SessionFactoryImplementor factory) { verifyConfigured(); - return enumJavaType.toString( (T) value ); + return enumJavaType.extractLoggableRepresentation( (T) value ); } public boolean isOrdinal() { diff --git a/hibernate-core/src/main/java/org/hibernate/type/Type.java b/hibernate-core/src/main/java/org/hibernate/type/Type.java index 4d85ac95b2..c1bb37673c 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/Type.java +++ b/hibernate-core/src/main/java/org/hibernate/type/Type.java @@ -154,7 +154,7 @@ public interface Type extends Serializable { * * @throws HibernateException A problem occurred performing the comparison */ - boolean isSame(Object x, Object y) throws HibernateException; + boolean isSame(@Nullable Object x, @Nullable Object y) throws HibernateException; /** * Compare two instances of the class mapped by this type for persistence "equality", @@ -173,7 +173,7 @@ public interface Type extends Serializable { * * @throws HibernateException A problem occurred performing the comparison */ - boolean isEqual(Object x, Object y) throws HibernateException; + boolean isEqual(@Nullable Object x, @Nullable Object y) throws HibernateException; /** * Compare two instances of the class mapped by this type for persistence "equality", @@ -193,7 +193,7 @@ public interface Type extends Serializable { * * @throws HibernateException A problem occurred performing the comparison */ - boolean isEqual(Object x, Object y, SessionFactoryImplementor factory) throws HibernateException; + boolean isEqual(@Nullable Object x, @Nullable Object y, SessionFactoryImplementor factory) throws HibernateException; /** * Get a hash code, consistent with persistence "equality". For most types this could @@ -229,9 +229,9 @@ public interface Type extends Serializable { * * @see java.util.Comparator#compare(Object, Object) */ - int compare(Object x, Object y); + int compare(@Nullable Object x, @Nullable Object y); - int compare(Object x, Object y, SessionFactoryImplementor sessionFactory); + int compare(@Nullable Object x, @Nullable Object y, SessionFactoryImplementor sessionFactory); /** * Should the parent be considered dirty, given both the old and current value? @@ -244,7 +244,7 @@ public interface Type extends Serializable { * * @throws HibernateException A problem occurred performing the checking */ - boolean isDirty(Object old, Object current, SharedSessionContractImplementor session) throws HibernateException; + boolean isDirty(@Nullable Object old, @Nullable Object current, SharedSessionContractImplementor session) throws HibernateException; /** * Should the parent be considered dirty, given both the old and current value? @@ -258,7 +258,7 @@ public interface Type extends Serializable { * * @throws HibernateException A problem occurred performing the checking */ - boolean isDirty(Object oldState, Object currentState, boolean[] checkable, SharedSessionContractImplementor session) + boolean isDirty(@Nullable Object oldState, @Nullable Object currentState, boolean[] checkable, SharedSessionContractImplementor session) throws HibernateException; /** @@ -277,8 +277,8 @@ public interface Type extends Serializable { * @throws HibernateException A problem occurred performing the checking */ boolean isModified( - Object dbState, - Object currentState, + @Nullable Object dbState, + @Nullable Object currentState, boolean[] checkable, SharedSessionContractImplementor session) throws HibernateException; @@ -300,7 +300,7 @@ public interface Type extends Serializable { */ void nullSafeSet( PreparedStatement st, - Object value, + @Nullable Object value, int index, boolean[] settable, SharedSessionContractImplementor session) @@ -320,7 +320,7 @@ public interface Type extends Serializable { * @throws HibernateException An error from Hibernate * @throws SQLException An error from the JDBC driver */ - void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session) + void nullSafeSet(PreparedStatement st, @Nullable Object value, int index, SharedSessionContractImplementor session) throws HibernateException, SQLException; /** @@ -353,7 +353,7 @@ public interface Type extends Serializable { * * @throws HibernateException An error from Hibernate */ - Object deepCopy(Object value, SessionFactoryImplementor factory) + @Nullable Object deepCopy(@Nullable Object value, SessionFactoryImplementor factory) throws HibernateException; /** @@ -392,7 +392,7 @@ public interface Type extends Serializable { * * @throws HibernateException An error from Hibernate */ - default Serializable disassemble(Object value, SessionFactoryImplementor sessionFactory) throws HibernateException { + default @Nullable Serializable disassemble(@Nullable Object value, SessionFactoryImplementor sessionFactory) throws HibernateException { return disassemble( value, null, null ); } @@ -410,7 +410,7 @@ public interface Type extends Serializable { * * @throws HibernateException An error from Hibernate */ - Serializable disassemble(Object value, SharedSessionContractImplementor session, Object owner) throws HibernateException; + @Nullable Serializable disassemble(@Nullable Object value, @Nullable SharedSessionContractImplementor session, @Nullable Object owner) throws HibernateException; /** * Reconstruct the object from its disassembled state. This function is the inverse of @@ -424,7 +424,7 @@ public interface Type extends Serializable { * * @throws HibernateException An error from Hibernate */ - Object assemble(Serializable cached, SharedSessionContractImplementor session, Object owner) throws HibernateException; + @Nullable Object assemble(@Nullable Serializable cached, SharedSessionContractImplementor session, Object owner) throws HibernateException; /** * Called before assembling a query result set from the query cache, to allow batch @@ -432,7 +432,9 @@ public interface Type extends Serializable { * * @param cached The key * @param session The originating session + * @deprecated Is not called anymore */ + @Deprecated(forRemoval = true, since = "6.6") void beforeAssemble(Serializable cached, SharedSessionContractImplementor session); /** @@ -452,9 +454,9 @@ public interface Type extends Serializable { * * @throws HibernateException An error from Hibernate */ - Object replace( - Object original, - Object target, + @Nullable Object replace( + @Nullable Object original, + @Nullable Object target, SharedSessionContractImplementor session, Object owner, Map copyCache) throws HibernateException; @@ -477,9 +479,9 @@ public interface Type extends Serializable { * * @throws HibernateException An error from Hibernate */ - Object replace( - Object original, - Object target, + @Nullable Object replace( + @Nullable Object original, + @Nullable Object target, SharedSessionContractImplementor session, Object owner, Map copyCache, @@ -494,5 +496,5 @@ public interface Type extends Serializable { * * @return array indicating column nullness for a value instance */ - boolean[] toColumnNullness(Object value, Mapping mapping); + boolean[] toColumnNullness(@Nullable Object value, Mapping mapping); } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/JavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/JavaType.java index 526681e4c2..71488ce897 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/JavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/JavaType.java @@ -26,6 +26,8 @@ import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; import org.hibernate.type.spi.TypeConfiguration; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * Descriptor for the Java side of a value mapping. A {@code JavaType} is always * coupled with a {@link JdbcType} to describe the typing aspects of an attribute @@ -228,12 +230,12 @@ public interface JavaType extends Serializable { * * @return The loggable representation */ - default String extractLoggableRepresentation(T value) { - return toString( value ); + default String extractLoggableRepresentation(@Nullable T value) { + return value == null ? "null" : toString( value ); } default String toString(T value) { - return value == null ? "null" : value.toString(); + return value.toString(); } T fromString(CharSequence string); diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/MutabilityPlan.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/MutabilityPlan.java index 029a3e9791..34225483fc 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/MutabilityPlan.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/MutabilityPlan.java @@ -10,6 +10,8 @@ import java.io.Serializable; import org.hibernate.SharedSessionContract; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * Describes the mutability aspects of a given Java type. *

    @@ -65,7 +67,7 @@ public interface MutabilityPlan extends Serializable { * * @return The deep copy. */ - T deepCopy(T value); + @Nullable T deepCopy(@Nullable T value); /** * Return a disassembled representation of the value. @@ -76,7 +78,7 @@ public interface MutabilityPlan extends Serializable { * * @see #assemble */ - Serializable disassemble(T value, SharedSessionContract session); + @Nullable Serializable disassemble(@Nullable T value, SharedSessionContract session); /** * Assemble a previously {@linkplain #disassemble disassembled} value. @@ -87,5 +89,5 @@ public interface MutabilityPlan extends Serializable { * * @see #disassemble */ - T assemble(Serializable cached, SharedSessionContract session); + @Nullable T assemble(@Nullable Serializable cached, SharedSessionContract session); } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ObjectArrayJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ObjectArrayJavaType.java index 93b0633f7d..d21ba65b9f 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ObjectArrayJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ObjectArrayJavaType.java @@ -30,15 +30,25 @@ public class ObjectArrayJavaType extends AbstractClassJavaType { public String toString(Object[] value) { final StringBuilder sb = new StringBuilder(); sb.append( '(' ); - sb.append( components[0].toString( value[0] ) ); + append( sb, components, value, 0 ); for ( int i = 1; i < components.length; i++ ) { sb.append( ", " ); - sb.append( components[i].toString( value[i] ) ); + append( sb, components, value, i ); } sb.append( ')' ); return sb.toString(); } + private void append(StringBuilder sb, JavaType[] components, Object[] value, int i) { + final Object o = value[i]; + if (o == null ) { + sb.append( "null" ); + } + else { + sb.append( components[i].toString( o ) ); + } + } + @Override public boolean areEqual(Object[] one, Object[] another) { if ( one == another ) { diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/BasicCollectionJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/BasicCollectionJavaType.java index defc19dd8e..70a7d653d0 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/BasicCollectionJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/BasicCollectionJavaType.java @@ -160,7 +160,7 @@ public class BasicCollectionJavaType, E> extends Abstrac sb.append( '[' ); do { final E element = iterator.next(); - sb.append( componentJavaType.toString( element ) ); + sb.append( componentJavaType.extractLoggableRepresentation( element ) ); if ( !iterator.hasNext() ) { return sb.append( ']' ).toString(); } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/FormatMapperBasedJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/FormatMapperBasedJavaType.java index dcd6f4039d..1c0de795ce 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/FormatMapperBasedJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/FormatMapperBasedJavaType.java @@ -24,6 +24,7 @@ import org.hibernate.type.spi.TypeConfiguration; * or {@link org.hibernate.type.SqlTypes#SQLXML} mapped types. * * @author Christian Beikov + * @author Yanming Zhou */ @Incubating public abstract class FormatMapperBasedJavaType extends AbstractJavaType implements MutabilityPlan { @@ -107,16 +108,16 @@ public abstract class FormatMapperBasedJavaType extends AbstractJavaType i @Override public T deepCopy(T value) { - return fromString( toString( value ) ); + return value == null ? null : fromString( toString( value ) ); } @Override public Serializable disassemble(T value, SharedSessionContract session) { - return toString( value ); + return value == null ? null : toString( value ); } @Override public T assemble(Serializable cached, SharedSessionContract session) { - return fromString( (CharSequence) cached ); + return cached == null ? null : fromString( (CharSequence) cached ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/H2FormatJsonJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/H2FormatJsonJdbcType.java index f6b383caf8..6588ee5872 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/H2FormatJsonJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/H2FormatJsonJdbcType.java @@ -16,7 +16,9 @@ import org.hibernate.sql.ast.spi.SqlAppender; * '{@code ? format json}' write expression for H2. * * @author Marco Belladelli + * @deprecated Use {@link org.hibernate.dialect.H2JsonJdbcType} instead */ +@Deprecated(forRemoval = true, since = "6.6") public class H2FormatJsonJdbcType extends JsonJdbcType { /** * Singleton access diff --git a/hibernate-core/src/main/java/org/hibernate/type/format/AbstractJsonFormatMapper.java b/hibernate-core/src/main/java/org/hibernate/type/format/AbstractJsonFormatMapper.java new file mode 100644 index 0000000000..4a1a6b9060 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/format/AbstractJsonFormatMapper.java @@ -0,0 +1,41 @@ +/* + * 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 . + */ +package org.hibernate.type.format; + +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; + +import java.lang.reflect.Type; + +/** + * @author Yanming Zhou + */ +public abstract class AbstractJsonFormatMapper implements FormatMapper { + + @SuppressWarnings("unchecked") + @Override + public final T fromString(CharSequence charSequence, JavaType javaType, WrapperOptions wrapperOptions) { + final Type type = javaType.getJavaType(); + if ( type == String.class || type == Object.class ) { + return (T) charSequence.toString(); + } + return fromString( charSequence, type ); + } + + @Override + public final String toString(T value, JavaType javaType, WrapperOptions wrapperOptions) { + final Type type = javaType.getJavaType(); + if ( type == String.class || type == Object.class ) { + return (String) value; + } + return toString( value, type ); + } + + protected abstract T fromString(CharSequence charSequence, Type type); + + protected abstract String toString(T value, Type type); +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonJsonFormatMapper.java b/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonJsonFormatMapper.java index 76bbb7a41d..fe4b41b4f9 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonJsonFormatMapper.java +++ b/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonJsonFormatMapper.java @@ -6,17 +6,18 @@ */ package org.hibernate.type.format.jackson; -import org.hibernate.type.format.FormatMapper; -import org.hibernate.type.descriptor.WrapperOptions; -import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.format.AbstractJsonFormatMapper; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import java.lang.reflect.Type; + /** * @author Christian Beikov + * @author Yanming Zhou */ -public final class JacksonJsonFormatMapper implements FormatMapper { +public final class JacksonJsonFormatMapper extends AbstractJsonFormatMapper { public static final String SHORT_NAME = "jackson"; @@ -31,29 +32,22 @@ public final class JacksonJsonFormatMapper implements FormatMapper { } @Override - public T fromString(CharSequence charSequence, JavaType javaType, WrapperOptions wrapperOptions) { - if ( javaType.getJavaType() == String.class || javaType.getJavaType() == Object.class ) { - return (T) charSequence.toString(); - } + public T fromString(CharSequence charSequence, Type type) { try { - return objectMapper.readValue( charSequence.toString(), objectMapper.constructType( javaType.getJavaType() ) ); + return objectMapper.readValue( charSequence.toString(), objectMapper.constructType( type ) ); } catch (JsonProcessingException e) { - throw new IllegalArgumentException( "Could not deserialize string to java type: " + javaType, e ); + throw new IllegalArgumentException( "Could not deserialize string to java type: " + type, e ); } } @Override - public String toString(T value, JavaType javaType, WrapperOptions wrapperOptions) { - if ( javaType.getJavaType() == String.class || javaType.getJavaType() == Object.class ) { - return (String) value; - } + public String toString(T value, Type type) { try { - return objectMapper.writerFor( objectMapper.constructType( javaType.getJavaType() ) ) - .writeValueAsString( value ); + return objectMapper.writerFor( objectMapper.constructType( type ) ).writeValueAsString( value ); } catch (JsonProcessingException e) { - throw new IllegalArgumentException( "Could not serialize object of java type: " + javaType, e ); + throw new IllegalArgumentException( "Could not serialize object of java type: " + type, e ); } } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/format/jakartajson/JsonBJsonFormatMapper.java b/hibernate-core/src/main/java/org/hibernate/type/format/jakartajson/JsonBJsonFormatMapper.java index 409a586344..293174beca 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/format/jakartajson/JsonBJsonFormatMapper.java +++ b/hibernate-core/src/main/java/org/hibernate/type/format/jakartajson/JsonBJsonFormatMapper.java @@ -6,18 +6,19 @@ */ package org.hibernate.type.format.jakartajson; -import org.hibernate.type.format.FormatMapper; -import org.hibernate.type.descriptor.WrapperOptions; -import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.format.AbstractJsonFormatMapper; import jakarta.json.bind.Jsonb; import jakarta.json.bind.JsonbBuilder; import jakarta.json.bind.JsonbException; +import java.lang.reflect.Type; + /** * @author Christian Beikov + * @author Yanming Zhou */ -public final class JsonBJsonFormatMapper implements FormatMapper { +public final class JsonBJsonFormatMapper extends AbstractJsonFormatMapper { public static final String SHORT_NAME = "jsonb"; @@ -32,28 +33,22 @@ public final class JsonBJsonFormatMapper implements FormatMapper { } @Override - public T fromString(CharSequence charSequence, JavaType javaType, WrapperOptions wrapperOptions) { - if ( javaType.getJavaType() == String.class || javaType.getJavaType() == Object.class ) { - return (T) charSequence.toString(); - } + public T fromString(CharSequence charSequence, Type type) { try { - return jsonb.fromJson( charSequence.toString(), javaType.getJavaType() ); + return jsonb.fromJson( charSequence.toString(), type ); } catch (JsonbException e) { - throw new IllegalArgumentException( "Could not deserialize string to java type: " + javaType, e ); + throw new IllegalArgumentException( "Could not deserialize string to java type: " + type, e ); } } @Override - public String toString(T value, JavaType javaType, WrapperOptions wrapperOptions) { - if ( javaType.getJavaType() == String.class || javaType.getJavaType() == Object.class ) { - return (String) value; - } + public String toString(T value, Type type) { try { - return jsonb.toJson( value, javaType.getJavaType() ); + return jsonb.toJson( value, type ); } catch (JsonbException e) { - throw new IllegalArgumentException( "Could not serialize object of java type: " + javaType, e ); + throw new IllegalArgumentException( "Could not serialize object of java type: " + type, e ); } } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/JsonMappingTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/JsonMappingTests.java index 78f24a2912..f3187767f2 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/JsonMappingTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/JsonMappingTests.java @@ -12,16 +12,20 @@ import java.sql.Clob; import java.util.List; import java.util.Map; +import com.fasterxml.jackson.databind.JsonNode; +import jakarta.json.JsonValue; import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.cfg.AvailableSettings; import org.hibernate.community.dialect.AltibaseDialect; import org.hibernate.dialect.AbstractHANADialect; import org.hibernate.dialect.DerbyDialect; import org.hibernate.dialect.OracleDialect; +import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.dialect.SybaseDialect; import org.hibernate.metamodel.mapping.internal.BasicAttributeMapping; import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.testing.orm.junit.RequiresDialect; import org.hibernate.type.SqlTypes; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; @@ -47,9 +51,11 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.isOneOf; import static org.hamcrest.Matchers.isA; import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; /** * @author Christian Beikov + * @author Yanming Zhou */ @DomainModel(annotatedClasses = JsonMappingTests.EntityWithJson.class) @SessionFactory @@ -140,6 +146,29 @@ public abstract class JsonMappingTests { assertThat( entityWithJson.stringMap, is( stringMap ) ); assertThat( entityWithJson.objectMap, is( objectMap ) ); assertThat( entityWithJson.list, is( list ) ); + assertThat( entityWithJson.jsonNode, is( nullValue() )); + assertThat( entityWithJson.jsonValue, is( nullValue() )); + } + ); + } + + @Test + public void verifyMergeWorks(SessionFactoryScope scope) { + scope.inTransaction( + (session) -> { + session.merge( new EntityWithJson( 2, null, null, null, null ) ); + } + ); + + scope.inTransaction( + (session) -> { + EntityWithJson entityWithJson = session.find( EntityWithJson.class, 2 ); + assertThat( entityWithJson.stringMap, is( nullValue() ) ); + assertThat( entityWithJson.objectMap, is( nullValue() ) ); + assertThat( entityWithJson.list, is( nullValue() ) ); + assertThat( entityWithJson.jsonString, is( nullValue() ) ); + assertThat( entityWithJson.jsonNode, is( nullValue() )); + assertThat( entityWithJson.jsonValue, is( nullValue() )); } ); } @@ -234,6 +263,12 @@ public abstract class JsonMappingTests { @JdbcTypeCode( SqlTypes.JSON ) private String jsonString; + @JdbcTypeCode( SqlTypes.JSON ) + private JsonNode jsonNode; + + @JdbcTypeCode( SqlTypes.JSON ) + private JsonValue jsonValue; + public EntityWithJson() { }