From 25ac9113921e5bd0ed5c373fcd0ac586392d049c Mon Sep 17 00:00:00 2001 From: Vlad Mihalcea Date: Sun, 10 Apr 2022 20:37:28 +0300 Subject: [PATCH] Add support for Oracle 21c JSON columns #422 Add support for customizing the JsonType underlying Oracle column type #424 --- .../hibernate/type/json/JsonBlobType.java | 15 +- .../hibernate/type/json/JsonType.java | 47 ++++- .../internal/JsonBlobSqlTypeDescriptor.java | 31 +++ .../internal/JsonBytesSqlTypeDescriptor.java | 29 ++- .../json/internal/JsonSqlTypeDescriptor.java | 74 ++++++-- .../type/util/ParameterTypeUtils.java | 105 +++++++++++ .../hibernate/util/StringUtils.java | 23 +++ .../GenericMySQLJsonTypeTest.java} | 4 +- .../GenericOffsetDateTimeJsonTest.java} | 5 +- .../hibernate/type/json/JsonBlobType.java | 15 +- .../hibernate/type/json/JsonType.java | 47 ++++- .../internal/JsonBlobSqlTypeDescriptor.java | 31 +++ .../internal/JsonBytesSqlTypeDescriptor.java | 29 ++- .../json/internal/JsonSqlTypeDescriptor.java | 74 ++++++-- .../type/util/ParameterTypeUtils.java | 105 +++++++++++ .../hibernate/util/StringUtils.java | 23 +++ .../json/OracleJsonStringPropertyTest.java | 1 + .../GenericMySQLJsonTypeTest.java} | 4 +- .../GenericOffsetDateTimeJsonTest.java} | 5 +- ...nTypeBlobPropertyParameterSettingTest.java | 165 ++++++++++++++++ ...GenericOracleJsonTypeBlobPropertyTest.java | 164 ++++++++++++++++ .../GenericOracleJsonTypePropertyTest.java | 164 ++++++++++++++++ ...ericOracleJsonTypeVarcharPropertyTest.java | 168 +++++++++++++++++ .../type/MutableDynamicParameterizedType.java | 5 + .../hibernate/type/json/JsonBlobType.java | 18 +- .../hibernate/type/json/JsonType.java | 49 ++++- .../AbstractJsonJdbcTypeDescriptor.java | 2 + .../internal/JsonBlobJdbcTypeDescriptor.java | 31 +++ .../internal/JsonBytesJdbcTypeDescriptor.java | 28 ++- .../json/internal/JsonJdbcTypeDescriptor.java | 82 ++++++-- .../type/util/ParameterTypeUtils.java | 105 +++++++++++ .../hibernate/util/StringUtils.java | 23 +++ .../GenericMySQLJsonTypeTest.java} | 5 +- .../GenericOffsetDateTimeJsonTest.java} | 5 +- .../generic/GenericOracleJsonMapTest.java | 9 +- ...GenericOracleJsonTypeBlobPropertyTest.java | 175 +++++++++++++++++ .../GenericOracleJsonTypePropertyTest.java | 175 +++++++++++++++++ ...ericOracleJsonTypeVarcharPropertyTest.java | 177 ++++++++++++++++++ pom.xml | 2 +- 39 files changed, 2095 insertions(+), 124 deletions(-) create mode 100644 hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBlobSqlTypeDescriptor.java create mode 100644 hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/util/ParameterTypeUtils.java rename hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/json/{MySQLGenericJsonTypeTest.java => generic/GenericMySQLJsonTypeTest.java} (96%) rename hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/json/{OffsetDateTimeJsonTest.java => generic/GenericOffsetDateTimeJsonTest.java} (93%) create mode 100644 hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBlobSqlTypeDescriptor.java create mode 100644 hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/util/ParameterTypeUtils.java rename hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/{MySQLJsonTypeTest.java => generic/GenericMySQLJsonTypeTest.java} (97%) rename hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/{OffsetDateTimeJsonTest.java => generic/GenericOffsetDateTimeJsonTest.java} (93%) create mode 100644 hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonTypeBlobPropertyParameterSettingTest.java create mode 100644 hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonTypeBlobPropertyTest.java create mode 100644 hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonTypePropertyTest.java create mode 100644 hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonTypeVarcharPropertyTest.java create mode 100644 hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBlobJdbcTypeDescriptor.java create mode 100644 hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/ParameterTypeUtils.java rename hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/{MySQLJsonTypeTest.java => generic/GenericMySQLJsonTypeTest.java} (95%) rename hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/{OffsetDateTimeJsonTest.java => generic/GenericOffsetDateTimeJsonTest.java} (93%) create mode 100644 hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonTypeBlobPropertyTest.java create mode 100644 hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonTypePropertyTest.java create mode 100644 hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonTypeVarcharPropertyTest.java diff --git a/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/json/JsonBlobType.java b/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/json/JsonBlobType.java index ed6931acb..a7b2495da 100644 --- a/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/json/JsonBlobType.java +++ b/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/json/JsonBlobType.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.vladmihalcea.hibernate.type.AbstractHibernateType; +import com.vladmihalcea.hibernate.type.json.internal.JsonBlobSqlTypeDescriptor; import com.vladmihalcea.hibernate.type.json.internal.JsonTypeDescriptor; import com.vladmihalcea.hibernate.type.util.Configuration; import com.vladmihalcea.hibernate.type.util.ObjectMapperWrapper; @@ -33,21 +34,21 @@ public class JsonBlobType extends AbstractHibernateType implements Dynam public JsonBlobType() { super( - org.hibernate.type.descriptor.sql.BlobTypeDescriptor.DEFAULT, + JsonBlobSqlTypeDescriptor.INSTANCE, new JsonTypeDescriptor(Configuration.INSTANCE.getObjectMapperWrapper()) ); } public JsonBlobType(Type javaType) { super( - org.hibernate.type.descriptor.sql.BlobTypeDescriptor.DEFAULT, + JsonBlobSqlTypeDescriptor.INSTANCE, new JsonTypeDescriptor(Configuration.INSTANCE.getObjectMapperWrapper(), javaType) ); } public JsonBlobType(Configuration configuration) { super( - org.hibernate.type.descriptor.sql.BlobTypeDescriptor.DEFAULT, + JsonBlobSqlTypeDescriptor.INSTANCE, new JsonTypeDescriptor(configuration.getObjectMapperWrapper()), configuration ); @@ -55,28 +56,28 @@ public JsonBlobType(Configuration configuration) { public JsonBlobType(ObjectMapper objectMapper) { super( - org.hibernate.type.descriptor.sql.BlobTypeDescriptor.DEFAULT, + JsonBlobSqlTypeDescriptor.INSTANCE, new JsonTypeDescriptor(new ObjectMapperWrapper(objectMapper)) ); } public JsonBlobType(ObjectMapperWrapper objectMapperWrapper) { super( - org.hibernate.type.descriptor.sql.BlobTypeDescriptor.DEFAULT, + JsonBlobSqlTypeDescriptor.INSTANCE, new JsonTypeDescriptor(objectMapperWrapper) ); } public JsonBlobType(ObjectMapper objectMapper, Type javaType) { super( - org.hibernate.type.descriptor.sql.BlobTypeDescriptor.DEFAULT, + JsonBlobSqlTypeDescriptor.INSTANCE, new JsonTypeDescriptor(new ObjectMapperWrapper(objectMapper), javaType) ); } public JsonBlobType(ObjectMapperWrapper objectMapperWrapper, Type javaType) { super( - org.hibernate.type.descriptor.sql.BlobTypeDescriptor.DEFAULT, + JsonBlobSqlTypeDescriptor.INSTANCE, new JsonTypeDescriptor(objectMapperWrapper, javaType) ); } diff --git a/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/json/JsonType.java b/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/json/JsonType.java index 861b6b334..45526adcc 100644 --- a/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/json/JsonType.java +++ b/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/json/JsonType.java @@ -6,8 +6,11 @@ import com.vladmihalcea.hibernate.type.json.internal.JsonTypeDescriptor; import com.vladmihalcea.hibernate.type.util.Configuration; import com.vladmihalcea.hibernate.type.util.ObjectMapperWrapper; +import org.hibernate.type.descriptor.sql.SqlTypeDescriptor; import org.hibernate.usertype.DynamicParameterizedType; +import org.hibernate.usertype.ParameterizedType; +import javax.persistence.Column; import java.lang.reflect.Type; import java.util.Properties; @@ -16,20 +19,40 @@ * {@link JsonType} allows you to map any given JSON object (e.g., POJO, Map<String, Object>, List<T>, JsonNode) on any of the following database systems: *

* - * + *

+ * If you switch to Oracle 21c from an older version, then you should also migrate your {@code JSON} columns to the native JSON type since this binary type performs better than + * {@code VARCHAR2} or {@code BLOB} column types. + *

+ *

+ * However, if you don't want to migrate to the new {@code JSON} data type, + * then you just have to provide the column type via the JPA {@link Column#columnDefinition()} attribute, + * like in the following example: + *

+ *
+ * {@code @Type(}type = "com.vladmihalcea.hibernate.type.json.JsonType")
+ * {@code @Column(}columnDefinition = "VARCHAR2")
+ * 
*

* For more details about how to use the {@link JsonType}, check out this article on vladmihalcea.com. *

*

- * If you are using Oracle and want to store JSON objects in a BLOB column types, then you should use the {@link JsonBlobType} instead. For more details, check out this article on vladmihalcea.com. + * If you are using Oracle and want to store JSON objects in a BLOB column type, then you can use the {@link JsonBlobType} instead. For more details, check out this article on vladmihalcea.com. *

- * + *

+ * Or, you can use the {@link JsonType}, but you'll have to specify the underlying column type + * using the JPA {@link Column#columnDefinition()} attribute, like this: + *

+ *
+ * {@code @Type(}type = "com.vladmihalcea.hibernate.type.json.JsonType")
+ * {@code @Column(}columnDefinition = "BLOB")
+ * private String properties;
+ * 
* @author Vlad Mihalcea */ public class JsonType @@ -53,7 +76,7 @@ public JsonType(Type javaType) { public JsonType(Configuration configuration) { super( - new JsonSqlTypeDescriptor(), + new JsonSqlTypeDescriptor(configuration.getProperties()), new JsonTypeDescriptor(configuration.getObjectMapperWrapper()), configuration ); @@ -94,6 +117,10 @@ public String getName() { @Override public void setParameterValues(Properties parameters) { ((JsonTypeDescriptor) getJavaTypeDescriptor()).setParameterValues(parameters); + SqlTypeDescriptor sqlTypeDescriptor = getSqlTypeDescriptor(); + if(sqlTypeDescriptor instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) sqlTypeDescriptor; + parameterizedType.setParameterValues(parameters); + } } - } \ No newline at end of file diff --git a/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBlobSqlTypeDescriptor.java b/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBlobSqlTypeDescriptor.java new file mode 100644 index 000000000..6e4fb2220 --- /dev/null +++ b/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBlobSqlTypeDescriptor.java @@ -0,0 +1,31 @@ +package com.vladmihalcea.hibernate.type.json.internal; + +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.java.JavaTypeDescriptor; +import org.hibernate.type.descriptor.sql.BlobTypeDescriptor; + +/** + * @author Vlad Mihalcea + */ +public class JsonBlobSqlTypeDescriptor extends AbstractJsonSqlTypeDescriptor { + + public static final JsonBlobSqlTypeDescriptor INSTANCE = new JsonBlobSqlTypeDescriptor(); + + private BlobTypeDescriptor blobTypeDescriptor = BlobTypeDescriptor.DEFAULT; + + @Override + public ValueBinder getBinder(JavaTypeDescriptor javaTypeDescriptor) { + return blobTypeDescriptor.getBinder(javaTypeDescriptor); + } + + @Override + public int getSqlType() { + return blobTypeDescriptor.getSqlType(); + } + + @Override + public ValueExtractor getExtractor(JavaTypeDescriptor javaTypeDescriptor) { + return blobTypeDescriptor.getExtractor(javaTypeDescriptor); + } +} diff --git a/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBytesSqlTypeDescriptor.java b/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBytesSqlTypeDescriptor.java index c004eaeca..da61767eb 100644 --- a/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBytesSqlTypeDescriptor.java +++ b/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBytesSqlTypeDescriptor.java @@ -1,13 +1,15 @@ package com.vladmihalcea.hibernate.type.json.internal; +import org.hibernate.dialect.Database; import org.hibernate.type.descriptor.ValueBinder; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.java.JavaTypeDescriptor; import org.hibernate.type.descriptor.sql.BasicBinder; import java.io.UnsupportedEncodingException; -import java.nio.charset.Charset; import java.sql.*; +import java.util.HashMap; +import java.util.Map; /** * @author Vlad Mihalcea @@ -16,11 +18,32 @@ public class JsonBytesSqlTypeDescriptor extends AbstractJsonSqlTypeDescriptor { public static final JsonBytesSqlTypeDescriptor INSTANCE = new JsonBytesSqlTypeDescriptor(); + private static final Map INSTANCE_MAP = new HashMap<>(); + + static { + INSTANCE_MAP.put(Database.H2, INSTANCE); + INSTANCE_MAP.put(Database.ORACLE, new JsonBytesSqlTypeDescriptor(2016)); + } + + public static JsonBytesSqlTypeDescriptor of(Database database) { + return INSTANCE_MAP.get(database); + } + public static final String CHARSET = "UTF8"; + private final int jdbcType; + + public JsonBytesSqlTypeDescriptor() { + this.jdbcType = Types.BINARY; + } + + public JsonBytesSqlTypeDescriptor(int jdbcType) { + this.jdbcType = jdbcType; + } + @Override public int getSqlType() { - return Types.BINARY; + return jdbcType; } @Override @@ -33,7 +56,7 @@ protected void doBind(PreparedStatement st, X value, int index, WrapperOptions o @Override protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) - throws SQLException { + throws SQLException { st.setBytes(name, toJsonBytes(javaTypeDescriptor.unwrap(value, String.class, options))); } }; diff --git a/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonSqlTypeDescriptor.java b/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonSqlTypeDescriptor.java index c89b47586..37ecb24c7 100644 --- a/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonSqlTypeDescriptor.java +++ b/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonSqlTypeDescriptor.java @@ -1,28 +1,37 @@ package com.vladmihalcea.hibernate.type.json.internal; -import org.hibernate.dialect.Dialect; -import org.hibernate.dialect.H2Dialect; -import org.hibernate.dialect.PostgreSQL81Dialect; +import com.vladmihalcea.hibernate.type.util.ParameterTypeUtils; +import com.vladmihalcea.hibernate.util.StringUtils; +import org.hibernate.dialect.*; import org.hibernate.engine.jdbc.dialect.internal.StandardDialectResolver; import org.hibernate.engine.jdbc.dialect.spi.DatabaseMetaDataDialectResolutionInfoAdapter; import org.hibernate.type.descriptor.ValueBinder; -import org.hibernate.type.descriptor.ValueExtractor; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.java.JavaTypeDescriptor; import org.hibernate.type.descriptor.sql.BasicBinder; -import org.hibernate.type.descriptor.sql.BasicExtractor; -import org.hibernate.type.descriptor.sql.SqlTypeDescriptor; +import org.hibernate.usertype.DynamicParameterizedType; +import org.hibernate.usertype.ParameterizedType; import java.sql.*; +import java.util.Properties; /** * @author Vlad Mihalcea */ -public class JsonSqlTypeDescriptor extends AbstractJsonSqlTypeDescriptor { +public class JsonSqlTypeDescriptor extends AbstractJsonSqlTypeDescriptor implements ParameterizedType { private volatile Dialect dialect; private volatile AbstractJsonSqlTypeDescriptor sqlTypeDescriptor; + private volatile Properties properties; + + public JsonSqlTypeDescriptor() { + } + + public JsonSqlTypeDescriptor(Properties properties) { + this.properties = properties; + } + @Override public ValueBinder getBinder(final JavaTypeDescriptor javaTypeDescriptor) { return new BasicBinder(javaTypeDescriptor, this) { @@ -35,7 +44,7 @@ protected void doBind(PreparedStatement st, X value, int index, WrapperOptions o @Override protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) - throws SQLException { + throws SQLException { sqlTypeDescriptor(st.getConnection()).getBinder(javaTypeDescriptor).bind( st, value, name, options ); @@ -59,7 +68,7 @@ protected Object extractJson(CallableStatement statement, String name) throws SQ } private AbstractJsonSqlTypeDescriptor sqlTypeDescriptor(Connection connection) { - if(sqlTypeDescriptor == null) { + if (sqlTypeDescriptor == null) { sqlTypeDescriptor = resolveSqlTypeDescriptor(connection); } return sqlTypeDescriptor; @@ -68,20 +77,53 @@ private AbstractJsonSqlTypeDescriptor sqlTypeDescriptor(Connection connection) { private AbstractJsonSqlTypeDescriptor resolveSqlTypeDescriptor(Connection connection) { try { StandardDialectResolver dialectResolver = new StandardDialectResolver(); - dialect = dialectResolver.resolveDialect( - new DatabaseMetaDataDialectResolutionInfoAdapter(connection.getMetaData()) - ); - if(PostgreSQL81Dialect.class.isInstance(dialect)) { + DatabaseMetaDataDialectResolutionInfoAdapter metaDataInfo = new DatabaseMetaDataDialectResolutionInfoAdapter(connection.getMetaData()); + dialect = dialectResolver.resolveDialect(metaDataInfo); + if (dialect instanceof PostgreSQL81Dialect) { return JsonBinarySqlTypeDescriptor.INSTANCE; - } else if(H2Dialect.class.isInstance(dialect)) { + } else if (dialect instanceof H2Dialect) { return JsonBytesSqlTypeDescriptor.INSTANCE; - } else { - return JsonStringSqlTypeDescriptor.INSTANCE; + } else if (dialect instanceof Oracle12cDialect) { + if (properties != null) { + DynamicParameterizedType.ParameterType parameterType = ParameterTypeUtils.resolve(properties); + if (parameterType != null) { + String columnType = ParameterTypeUtils.getColumnType(parameterType); + if (!StringUtils.isBlank(columnType)) { + switch (columnType) { + case "json": + return JsonBytesSqlTypeDescriptor.of(Database.ORACLE); + case "blob": + case "clob": + return JsonBlobSqlTypeDescriptor.INSTANCE; + case "varchar2": + return JsonStringSqlTypeDescriptor.INSTANCE; + } + } + } + } + if (metaDataInfo.getDatabaseMajorVersion() >= 21) { + return JsonBytesSqlTypeDescriptor.of(Database.ORACLE); + } } + return JsonStringSqlTypeDescriptor.INSTANCE; } catch (SQLException e) { throw new IllegalStateException(e); } } + @Override + public int getSqlType() { + return sqlTypeDescriptor != null ? + sqlTypeDescriptor.getSqlType() : + super.getSqlType(); + } + @Override + public void setParameterValues(Properties parameters) { + if (properties == null) { + properties = parameters; + } else { + properties.putAll(parameters); + } + } } diff --git a/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/util/ParameterTypeUtils.java b/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/util/ParameterTypeUtils.java new file mode 100644 index 000000000..cf5b9bfce --- /dev/null +++ b/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/util/ParameterTypeUtils.java @@ -0,0 +1,105 @@ +package com.vladmihalcea.hibernate.type.util; + +import com.vladmihalcea.hibernate.util.StringUtils; +import org.hibernate.usertype.DynamicParameterizedType; + +import javax.persistence.Column; +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * ParameterizedTypeUtils - {@link DynamicParameterizedType.ParameterType} utilities holder. + * + * @author Vlad Mihalcea + * @since 2.16.0 + */ +public class ParameterTypeUtils { + + private static final Pattern COLUMN_TYPE_PATTERN = Pattern.compile("([a-zA-Z0-9]+).*?"); + + private ParameterTypeUtils() { + throw new UnsupportedOperationException("StringUtils is not instantiable!"); + } + + /** + * Resolve the {@link DynamicParameterizedType.ParameterType} instance + * from the provided {@link Properties} object. + * + * @param properties configuration properties + * @return {@link DynamicParameterizedType.ParameterType} instance + */ + public static DynamicParameterizedType.ParameterType resolve(Properties properties) { + Object parameterTypeObject = properties.get(DynamicParameterizedType.PARAMETER_TYPE); + if (parameterTypeObject instanceof DynamicParameterizedType.ParameterType) { + return (DynamicParameterizedType.ParameterType) parameterTypeObject; + } + return null; + } + + /** + * Get the required annotation from the {@link DynamicParameterizedType.ParameterType} instance. + * + * @param parameterType {@link DynamicParameterizedType.ParameterType} instance + * @param annotationClass annotation class + * @return annotation + */ + @SuppressWarnings("unchecked") + public static A getAnnotationOrNull(DynamicParameterizedType.ParameterType parameterType, Class annotationClass) { + List annotations = getAnnotations(parameterType, annotationClass); + if(annotations.size() > 1) { + throw new IllegalArgumentException( + String.format( + "The provided ParameterType associated with the [%s] property contains more than one annotation of the [%s] type!", + parameterType.getReturnedClass(), + annotationClass.getName() + ) + ); + } + return (A) Arrays.stream(parameterType.getAnnotationsMethod()) + .filter(a -> annotationClass.isAssignableFrom(a.annotationType())) + .findAny() + .orElse(null); + } + + /** + * Get the required annotations from the {@link DynamicParameterizedType.ParameterType} instance. + * + * @param parameterType {@link DynamicParameterizedType.ParameterType} instance + * @param annotationClass annotation class + * @return annotations + */ + @SuppressWarnings("unchecked") + public static List getAnnotations(DynamicParameterizedType.ParameterType parameterType, Class annotationClass) { + return Arrays.stream(parameterType.getAnnotationsMethod()) + .filter(a -> annotationClass.isAssignableFrom(a.annotationType())) + .map(a -> (A) a) + .collect(Collectors.toList()); + } + + /** + * Get the column type association from the {@link DynamicParameterizedType.ParameterType} instance. + * + * @param parameterType {@link DynamicParameterizedType.ParameterType} instance + * @return column type + */ + public static String getColumnType(DynamicParameterizedType.ParameterType parameterType) { + if (parameterType != null) { + Column columnAnnotation = ParameterTypeUtils.getAnnotationOrNull(parameterType, Column.class); + if(columnAnnotation != null) { + String columnDefinition = columnAnnotation.columnDefinition(); + if(!StringUtils.isBlank(columnDefinition)) { + Matcher matcher = COLUMN_TYPE_PATTERN.matcher(columnDefinition); + if (matcher.matches()) { + return StringUtils.toLowercase(matcher.group(1)); + } + } + } + } + return null; + } +} diff --git a/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/util/StringUtils.java b/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/util/StringUtils.java index 473aff942..6a5a95a47 100644 --- a/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/util/StringUtils.java +++ b/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/util/StringUtils.java @@ -1,5 +1,7 @@ package com.vladmihalcea.hibernate.util; +import java.util.Locale; + /** * StringUtils - String utilities holder. * @@ -38,4 +40,25 @@ public static String join(CharSequence delimiter, CharSequence... elements) { return builder.toString(); } + /** + * Check if the String value is null, empty or contains only whitespace characters. + * @param value String value + * @return if the string is blank + */ + public static boolean isBlank(String value) { + return value == null || value.isEmpty() || value.trim().isEmpty(); + } + + /** + * Transform string to lowercase. + * + * @param value String value + * @return String value in lowercase + */ + public static String toLowercase(String value) { + if(isBlank(value)) { + return value; + } + return value.toLowerCase(Locale.ROOT); + } } diff --git a/hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/json/MySQLGenericJsonTypeTest.java b/hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericMySQLJsonTypeTest.java similarity index 96% rename from hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/json/MySQLGenericJsonTypeTest.java rename to hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericMySQLJsonTypeTest.java index b94e8890d..74cf0b213 100644 --- a/hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/json/MySQLGenericJsonTypeTest.java +++ b/hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericMySQLJsonTypeTest.java @@ -1,4 +1,4 @@ -package com.vladmihalcea.hibernate.type.json; +package com.vladmihalcea.hibernate.type.json.generic; import com.vladmihalcea.hibernate.type.model.BaseEntity; import com.vladmihalcea.hibernate.type.model.Location; @@ -21,7 +21,7 @@ /** * @author Vlad Mihalcea */ -public class MySQLGenericJsonTypeTest extends AbstractMySQLIntegrationTest { +public class GenericMySQLJsonTypeTest extends AbstractMySQLIntegrationTest { @Override protected Class[] entities() { diff --git a/hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/json/OffsetDateTimeJsonTest.java b/hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOffsetDateTimeJsonTest.java similarity index 93% rename from hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/json/OffsetDateTimeJsonTest.java rename to hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOffsetDateTimeJsonTest.java index ae7403949..4f548d5ea 100644 --- a/hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/json/OffsetDateTimeJsonTest.java +++ b/hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOffsetDateTimeJsonTest.java @@ -1,5 +1,6 @@ -package com.vladmihalcea.hibernate.type.json; +package com.vladmihalcea.hibernate.type.json.generic; +import com.vladmihalcea.hibernate.type.json.JsonType; import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; import org.hibernate.annotations.TypeDef; import org.junit.Test; @@ -18,7 +19,7 @@ /** * @author Vlad Mihalcea */ -public class OffsetDateTimeJsonTest extends AbstractPostgreSQLIntegrationTest { +public class GenericOffsetDateTimeJsonTest extends AbstractPostgreSQLIntegrationTest { @Override protected Class[] entities() { diff --git a/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/json/JsonBlobType.java b/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/json/JsonBlobType.java index 25d37824c..3d4575b5d 100644 --- a/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/json/JsonBlobType.java +++ b/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/json/JsonBlobType.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.vladmihalcea.hibernate.type.AbstractHibernateType; +import com.vladmihalcea.hibernate.type.json.internal.JsonBlobSqlTypeDescriptor; import com.vladmihalcea.hibernate.type.json.internal.JsonTypeDescriptor; import com.vladmihalcea.hibernate.type.util.Configuration; import com.vladmihalcea.hibernate.type.util.ObjectMapperWrapper; @@ -33,21 +34,21 @@ public class JsonBlobType extends AbstractHibernateType implements Dynam public JsonBlobType() { super( - org.hibernate.type.descriptor.sql.BlobTypeDescriptor.DEFAULT, + JsonBlobSqlTypeDescriptor.INSTANCE, new JsonTypeDescriptor(Configuration.INSTANCE.getObjectMapperWrapper()) ); } public JsonBlobType(Type javaType) { super( - org.hibernate.type.descriptor.sql.BlobTypeDescriptor.DEFAULT, + JsonBlobSqlTypeDescriptor.INSTANCE, new JsonTypeDescriptor(Configuration.INSTANCE.getObjectMapperWrapper(), javaType) ); } public JsonBlobType(Configuration configuration) { super( - org.hibernate.type.descriptor.sql.BlobTypeDescriptor.DEFAULT, + JsonBlobSqlTypeDescriptor.INSTANCE, new JsonTypeDescriptor(configuration.getObjectMapperWrapper()), configuration ); @@ -59,28 +60,28 @@ public JsonBlobType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapCon public JsonBlobType(ObjectMapper objectMapper) { super( - org.hibernate.type.descriptor.sql.BlobTypeDescriptor.DEFAULT, + JsonBlobSqlTypeDescriptor.INSTANCE, new JsonTypeDescriptor(new ObjectMapperWrapper(objectMapper)) ); } public JsonBlobType(ObjectMapperWrapper objectMapperWrapper) { super( - org.hibernate.type.descriptor.sql.BlobTypeDescriptor.DEFAULT, + JsonBlobSqlTypeDescriptor.INSTANCE, new JsonTypeDescriptor(objectMapperWrapper) ); } public JsonBlobType(ObjectMapper objectMapper, Type javaType) { super( - org.hibernate.type.descriptor.sql.BlobTypeDescriptor.DEFAULT, + JsonBlobSqlTypeDescriptor.INSTANCE, new JsonTypeDescriptor(new ObjectMapperWrapper(objectMapper), javaType) ); } public JsonBlobType(ObjectMapperWrapper objectMapperWrapper, Type javaType) { super( - org.hibernate.type.descriptor.sql.BlobTypeDescriptor.DEFAULT, + JsonBlobSqlTypeDescriptor.INSTANCE, new JsonTypeDescriptor(objectMapperWrapper, javaType) ); } diff --git a/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/json/JsonType.java b/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/json/JsonType.java index 0ca84ef3b..f63211f83 100644 --- a/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/json/JsonType.java +++ b/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/json/JsonType.java @@ -6,8 +6,11 @@ import com.vladmihalcea.hibernate.type.json.internal.JsonTypeDescriptor; import com.vladmihalcea.hibernate.type.util.Configuration; import com.vladmihalcea.hibernate.type.util.ObjectMapperWrapper; +import org.hibernate.type.descriptor.sql.SqlTypeDescriptor; import org.hibernate.usertype.DynamicParameterizedType; +import org.hibernate.usertype.ParameterizedType; +import javax.persistence.Column; import java.lang.reflect.Type; import java.util.Properties; @@ -16,20 +19,40 @@ * {@link JsonType} allows you to map any given JSON object (e.g., POJO, Map<String, Object>, List<T>, JsonNode) on any of the following database systems: *

*
    - *
  • PostgreSQL - for both jsonb and json column types
  • - *
  • MySQL - for the json column type
  • - *
  • SQL Server - for the NVARCHAR column type storing JSON
  • - *
  • Oracle - for the VARCHAR column type storing JSON
  • - *
  • H2 - for the json column type
  • + *
  • PostgreSQL - for both jsonb and json column types
  • + *
  • MySQL - for the json column type
  • + *
  • SQL Server - for the NVARCHAR column type storing JSON
  • + *
  • Oracle - for the JSON column type if you're using Oracle 21c or the VARCHAR column type storing JSON if you're using an older Oracle version
  • + *
  • H2 - for the json column type
  • *
- * + *

+ * If you switch to Oracle 21c from an older version, then you should also migrate your {@code JSON} columns to the native JSON type since this binary type performs better than + * {@code VARCHAR2} or {@code BLOB} column types. + *

+ *

+ * However, if you don't want to migrate to the new {@code JSON} data type, + * then you just have to provide the column type via the JPA {@link Column#columnDefinition()} attribute, + * like in the following example: + *

+ *
+ * {@code @Type(}type = "com.vladmihalcea.hibernate.type.json.JsonType")
+ * {@code @Column(}columnDefinition = "VARCHAR2")
+ * 
*

* For more details about how to use the {@link JsonType}, check out this article on vladmihalcea.com. *

*

- * If you are using Oracle and want to store JSON objects in a BLOB column types, then you should use the {@link JsonBlobType} instead. For more details, check out this article on vladmihalcea.com. + * If you are using Oracle and want to store JSON objects in a BLOB column type, then you can use the {@link JsonBlobType} instead. For more details, check out this article on vladmihalcea.com. *

- * + *

+ * Or, you can use the {@link JsonType}, but you'll have to specify the underlying column type + * using the JPA {@link Column#columnDefinition()} attribute, like this: + *

+ *
+ * {@code @Type(}type = "com.vladmihalcea.hibernate.type.json.JsonType")
+ * {@code @Column(}columnDefinition = "BLOB")
+ * private String properties;
+ * 
* @author Vlad Mihalcea */ public class JsonType @@ -53,7 +76,7 @@ public JsonType(Type javaType) { public JsonType(Configuration configuration) { super( - new JsonSqlTypeDescriptor(), + new JsonSqlTypeDescriptor(configuration.getProperties()), new JsonTypeDescriptor(configuration.getObjectMapperWrapper()), configuration ); @@ -98,6 +121,10 @@ public String getName() { @Override public void setParameterValues(Properties parameters) { ((JsonTypeDescriptor) getJavaTypeDescriptor()).setParameterValues(parameters); + SqlTypeDescriptor sqlTypeDescriptor = getSqlTypeDescriptor(); + if(sqlTypeDescriptor instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) sqlTypeDescriptor; + parameterizedType.setParameterValues(parameters); + } } - } \ No newline at end of file diff --git a/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBlobSqlTypeDescriptor.java b/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBlobSqlTypeDescriptor.java new file mode 100644 index 000000000..6e4fb2220 --- /dev/null +++ b/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBlobSqlTypeDescriptor.java @@ -0,0 +1,31 @@ +package com.vladmihalcea.hibernate.type.json.internal; + +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.java.JavaTypeDescriptor; +import org.hibernate.type.descriptor.sql.BlobTypeDescriptor; + +/** + * @author Vlad Mihalcea + */ +public class JsonBlobSqlTypeDescriptor extends AbstractJsonSqlTypeDescriptor { + + public static final JsonBlobSqlTypeDescriptor INSTANCE = new JsonBlobSqlTypeDescriptor(); + + private BlobTypeDescriptor blobTypeDescriptor = BlobTypeDescriptor.DEFAULT; + + @Override + public ValueBinder getBinder(JavaTypeDescriptor javaTypeDescriptor) { + return blobTypeDescriptor.getBinder(javaTypeDescriptor); + } + + @Override + public int getSqlType() { + return blobTypeDescriptor.getSqlType(); + } + + @Override + public ValueExtractor getExtractor(JavaTypeDescriptor javaTypeDescriptor) { + return blobTypeDescriptor.getExtractor(javaTypeDescriptor); + } +} diff --git a/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBytesSqlTypeDescriptor.java b/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBytesSqlTypeDescriptor.java index c004eaeca..da61767eb 100644 --- a/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBytesSqlTypeDescriptor.java +++ b/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBytesSqlTypeDescriptor.java @@ -1,13 +1,15 @@ package com.vladmihalcea.hibernate.type.json.internal; +import org.hibernate.dialect.Database; import org.hibernate.type.descriptor.ValueBinder; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.java.JavaTypeDescriptor; import org.hibernate.type.descriptor.sql.BasicBinder; import java.io.UnsupportedEncodingException; -import java.nio.charset.Charset; import java.sql.*; +import java.util.HashMap; +import java.util.Map; /** * @author Vlad Mihalcea @@ -16,11 +18,32 @@ public class JsonBytesSqlTypeDescriptor extends AbstractJsonSqlTypeDescriptor { public static final JsonBytesSqlTypeDescriptor INSTANCE = new JsonBytesSqlTypeDescriptor(); + private static final Map INSTANCE_MAP = new HashMap<>(); + + static { + INSTANCE_MAP.put(Database.H2, INSTANCE); + INSTANCE_MAP.put(Database.ORACLE, new JsonBytesSqlTypeDescriptor(2016)); + } + + public static JsonBytesSqlTypeDescriptor of(Database database) { + return INSTANCE_MAP.get(database); + } + public static final String CHARSET = "UTF8"; + private final int jdbcType; + + public JsonBytesSqlTypeDescriptor() { + this.jdbcType = Types.BINARY; + } + + public JsonBytesSqlTypeDescriptor(int jdbcType) { + this.jdbcType = jdbcType; + } + @Override public int getSqlType() { - return Types.BINARY; + return jdbcType; } @Override @@ -33,7 +56,7 @@ protected void doBind(PreparedStatement st, X value, int index, WrapperOptions o @Override protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) - throws SQLException { + throws SQLException { st.setBytes(name, toJsonBytes(javaTypeDescriptor.unwrap(value, String.class, options))); } }; diff --git a/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonSqlTypeDescriptor.java b/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonSqlTypeDescriptor.java index c89b47586..37ecb24c7 100644 --- a/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonSqlTypeDescriptor.java +++ b/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonSqlTypeDescriptor.java @@ -1,28 +1,37 @@ package com.vladmihalcea.hibernate.type.json.internal; -import org.hibernate.dialect.Dialect; -import org.hibernate.dialect.H2Dialect; -import org.hibernate.dialect.PostgreSQL81Dialect; +import com.vladmihalcea.hibernate.type.util.ParameterTypeUtils; +import com.vladmihalcea.hibernate.util.StringUtils; +import org.hibernate.dialect.*; import org.hibernate.engine.jdbc.dialect.internal.StandardDialectResolver; import org.hibernate.engine.jdbc.dialect.spi.DatabaseMetaDataDialectResolutionInfoAdapter; import org.hibernate.type.descriptor.ValueBinder; -import org.hibernate.type.descriptor.ValueExtractor; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.java.JavaTypeDescriptor; import org.hibernate.type.descriptor.sql.BasicBinder; -import org.hibernate.type.descriptor.sql.BasicExtractor; -import org.hibernate.type.descriptor.sql.SqlTypeDescriptor; +import org.hibernate.usertype.DynamicParameterizedType; +import org.hibernate.usertype.ParameterizedType; import java.sql.*; +import java.util.Properties; /** * @author Vlad Mihalcea */ -public class JsonSqlTypeDescriptor extends AbstractJsonSqlTypeDescriptor { +public class JsonSqlTypeDescriptor extends AbstractJsonSqlTypeDescriptor implements ParameterizedType { private volatile Dialect dialect; private volatile AbstractJsonSqlTypeDescriptor sqlTypeDescriptor; + private volatile Properties properties; + + public JsonSqlTypeDescriptor() { + } + + public JsonSqlTypeDescriptor(Properties properties) { + this.properties = properties; + } + @Override public ValueBinder getBinder(final JavaTypeDescriptor javaTypeDescriptor) { return new BasicBinder(javaTypeDescriptor, this) { @@ -35,7 +44,7 @@ protected void doBind(PreparedStatement st, X value, int index, WrapperOptions o @Override protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) - throws SQLException { + throws SQLException { sqlTypeDescriptor(st.getConnection()).getBinder(javaTypeDescriptor).bind( st, value, name, options ); @@ -59,7 +68,7 @@ protected Object extractJson(CallableStatement statement, String name) throws SQ } private AbstractJsonSqlTypeDescriptor sqlTypeDescriptor(Connection connection) { - if(sqlTypeDescriptor == null) { + if (sqlTypeDescriptor == null) { sqlTypeDescriptor = resolveSqlTypeDescriptor(connection); } return sqlTypeDescriptor; @@ -68,20 +77,53 @@ private AbstractJsonSqlTypeDescriptor sqlTypeDescriptor(Connection connection) { private AbstractJsonSqlTypeDescriptor resolveSqlTypeDescriptor(Connection connection) { try { StandardDialectResolver dialectResolver = new StandardDialectResolver(); - dialect = dialectResolver.resolveDialect( - new DatabaseMetaDataDialectResolutionInfoAdapter(connection.getMetaData()) - ); - if(PostgreSQL81Dialect.class.isInstance(dialect)) { + DatabaseMetaDataDialectResolutionInfoAdapter metaDataInfo = new DatabaseMetaDataDialectResolutionInfoAdapter(connection.getMetaData()); + dialect = dialectResolver.resolveDialect(metaDataInfo); + if (dialect instanceof PostgreSQL81Dialect) { return JsonBinarySqlTypeDescriptor.INSTANCE; - } else if(H2Dialect.class.isInstance(dialect)) { + } else if (dialect instanceof H2Dialect) { return JsonBytesSqlTypeDescriptor.INSTANCE; - } else { - return JsonStringSqlTypeDescriptor.INSTANCE; + } else if (dialect instanceof Oracle12cDialect) { + if (properties != null) { + DynamicParameterizedType.ParameterType parameterType = ParameterTypeUtils.resolve(properties); + if (parameterType != null) { + String columnType = ParameterTypeUtils.getColumnType(parameterType); + if (!StringUtils.isBlank(columnType)) { + switch (columnType) { + case "json": + return JsonBytesSqlTypeDescriptor.of(Database.ORACLE); + case "blob": + case "clob": + return JsonBlobSqlTypeDescriptor.INSTANCE; + case "varchar2": + return JsonStringSqlTypeDescriptor.INSTANCE; + } + } + } + } + if (metaDataInfo.getDatabaseMajorVersion() >= 21) { + return JsonBytesSqlTypeDescriptor.of(Database.ORACLE); + } } + return JsonStringSqlTypeDescriptor.INSTANCE; } catch (SQLException e) { throw new IllegalStateException(e); } } + @Override + public int getSqlType() { + return sqlTypeDescriptor != null ? + sqlTypeDescriptor.getSqlType() : + super.getSqlType(); + } + @Override + public void setParameterValues(Properties parameters) { + if (properties == null) { + properties = parameters; + } else { + properties.putAll(parameters); + } + } } diff --git a/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/util/ParameterTypeUtils.java b/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/util/ParameterTypeUtils.java new file mode 100644 index 000000000..807bf07c5 --- /dev/null +++ b/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/util/ParameterTypeUtils.java @@ -0,0 +1,105 @@ +package com.vladmihalcea.hibernate.type.util; + +import com.vladmihalcea.hibernate.util.StringUtils; +import org.hibernate.usertype.DynamicParameterizedType; + +import javax.persistence.Column; +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * ParameterizedTypeUtils - {@link org.hibernate.usertype.DynamicParameterizedType.ParameterType} utilities holder. + * + * @author Vlad Mihalcea + * @since 2.16.0 + */ +public class ParameterTypeUtils { + + private static final Pattern COLUMN_TYPE_PATTERN = Pattern.compile("([a-zA-Z0-9]+).*?"); + + private ParameterTypeUtils() { + throw new UnsupportedOperationException("StringUtils is not instantiable!"); + } + + /** + * Resolve the {@link org.hibernate.usertype.DynamicParameterizedType.ParameterType} instance + * from the provided {@link Properties} object. + * + * @param properties configuration properties + * @return {@link org.hibernate.usertype.DynamicParameterizedType.ParameterType} instance + */ + public static DynamicParameterizedType.ParameterType resolve(Properties properties) { + Object parameterTypeObject = properties.get(DynamicParameterizedType.PARAMETER_TYPE); + if (parameterTypeObject instanceof DynamicParameterizedType.ParameterType) { + return (DynamicParameterizedType.ParameterType) parameterTypeObject; + } + return null; + } + + /** + * Get the required annotation from the {@link org.hibernate.usertype.DynamicParameterizedType.ParameterType} instance. + * + * @param parameterType {@link org.hibernate.usertype.DynamicParameterizedType.ParameterType} instance + * @param annotationClass annotation class + * @return annotation + */ + @SuppressWarnings("unchecked") + public static A getAnnotationOrNull(DynamicParameterizedType.ParameterType parameterType, Class annotationClass) { + List annotations = getAnnotations(parameterType, annotationClass); + if(annotations.size() > 1) { + throw new IllegalArgumentException( + String.format( + "The provided ParameterType associated with the [%s] property contains more than one annotation of the [%s] type!", + parameterType.getReturnedClass(), + annotationClass.getName() + ) + ); + } + return (A) Arrays.stream(parameterType.getAnnotationsMethod()) + .filter(a -> annotationClass.isAssignableFrom(a.annotationType())) + .findAny() + .orElse(null); + } + + /** + * Get the required annotations from the {@link org.hibernate.usertype.DynamicParameterizedType.ParameterType} instance. + * + * @param parameterType {@link org.hibernate.usertype.DynamicParameterizedType.ParameterType} instance + * @param annotationClass annotation class + * @return annotations + */ + @SuppressWarnings("unchecked") + public static List getAnnotations(DynamicParameterizedType.ParameterType parameterType, Class annotationClass) { + return Arrays.stream(parameterType.getAnnotationsMethod()) + .filter(a -> annotationClass.isAssignableFrom(a.annotationType())) + .map(a -> (A) a) + .collect(Collectors.toList()); + } + + /** + * Get the column type association from the {@link org.hibernate.usertype.DynamicParameterizedType.ParameterType} instance. + * + * @param parameterType {@link org.hibernate.usertype.DynamicParameterizedType.ParameterType} instance + * @return column type + */ + public static String getColumnType(DynamicParameterizedType.ParameterType parameterType) { + if (parameterType != null) { + Column columnAnnotation = ParameterTypeUtils.getAnnotationOrNull(parameterType, Column.class); + if(columnAnnotation != null) { + String columnDefinition = columnAnnotation.columnDefinition(); + if(!StringUtils.isBlank(columnDefinition)) { + Matcher matcher = COLUMN_TYPE_PATTERN.matcher(columnDefinition); + if (matcher.matches()) { + return StringUtils.toLowercase(matcher.group(1)); + } + } + } + } + return null; + } +} diff --git a/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/util/StringUtils.java b/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/util/StringUtils.java index 473aff942..6a5a95a47 100644 --- a/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/util/StringUtils.java +++ b/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/util/StringUtils.java @@ -1,5 +1,7 @@ package com.vladmihalcea.hibernate.util; +import java.util.Locale; + /** * StringUtils - String utilities holder. * @@ -38,4 +40,25 @@ public static String join(CharSequence delimiter, CharSequence... elements) { return builder.toString(); } + /** + * Check if the String value is null, empty or contains only whitespace characters. + * @param value String value + * @return if the string is blank + */ + public static boolean isBlank(String value) { + return value == null || value.isEmpty() || value.trim().isEmpty(); + } + + /** + * Transform string to lowercase. + * + * @param value String value + * @return String value in lowercase + */ + public static String toLowercase(String value) { + if(isBlank(value)) { + return value; + } + return value.toLowerCase(Locale.ROOT); + } } diff --git a/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/OracleJsonStringPropertyTest.java b/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/OracleJsonStringPropertyTest.java index 1ac7c1478..66dfe3b73 100644 --- a/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/OracleJsonStringPropertyTest.java +++ b/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/OracleJsonStringPropertyTest.java @@ -1,6 +1,7 @@ package com.vladmihalcea.hibernate.type.json; import com.fasterxml.jackson.databind.JsonNode; +import com.vladmihalcea.hibernate.type.json.JsonStringType; import com.vladmihalcea.hibernate.util.AbstractOracleIntegrationTest; import net.ttddyy.dsproxy.QueryCount; import net.ttddyy.dsproxy.QueryCountHolder; diff --git a/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/MySQLJsonTypeTest.java b/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericMySQLJsonTypeTest.java similarity index 97% rename from hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/MySQLJsonTypeTest.java rename to hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericMySQLJsonTypeTest.java index d1e2b6a7c..fbb7aa66f 100644 --- a/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/MySQLJsonTypeTest.java +++ b/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericMySQLJsonTypeTest.java @@ -1,4 +1,4 @@ -package com.vladmihalcea.hibernate.type.json; +package com.vladmihalcea.hibernate.type.json.generic; import com.vladmihalcea.hibernate.type.model.BaseEntity; import com.vladmihalcea.hibernate.type.model.Location; @@ -20,7 +20,7 @@ /** * @author Vlad Mihalcea */ -public class MySQLJsonTypeTest extends AbstractMySQLIntegrationTest { +public class GenericMySQLJsonTypeTest extends AbstractMySQLIntegrationTest { @Override protected Class[] entities() { diff --git a/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/OffsetDateTimeJsonTest.java b/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOffsetDateTimeJsonTest.java similarity index 93% rename from hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/OffsetDateTimeJsonTest.java rename to hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOffsetDateTimeJsonTest.java index ae7403949..4f548d5ea 100644 --- a/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/OffsetDateTimeJsonTest.java +++ b/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOffsetDateTimeJsonTest.java @@ -1,5 +1,6 @@ -package com.vladmihalcea.hibernate.type.json; +package com.vladmihalcea.hibernate.type.json.generic; +import com.vladmihalcea.hibernate.type.json.JsonType; import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; import org.hibernate.annotations.TypeDef; import org.junit.Test; @@ -18,7 +19,7 @@ /** * @author Vlad Mihalcea */ -public class OffsetDateTimeJsonTest extends AbstractPostgreSQLIntegrationTest { +public class GenericOffsetDateTimeJsonTest extends AbstractPostgreSQLIntegrationTest { @Override protected Class[] entities() { diff --git a/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonTypeBlobPropertyParameterSettingTest.java b/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonTypeBlobPropertyParameterSettingTest.java new file mode 100644 index 000000000..59a2d7431 --- /dev/null +++ b/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonTypeBlobPropertyParameterSettingTest.java @@ -0,0 +1,165 @@ +package com.vladmihalcea.hibernate.type.json.generic; + +import com.fasterxml.jackson.databind.JsonNode; +import com.vladmihalcea.hibernate.type.json.JsonBlobType; +import com.vladmihalcea.hibernate.type.util.Configuration; +import com.vladmihalcea.hibernate.util.AbstractOracleIntegrationTest; +import com.vladmihalcea.hibernate.util.transaction.JPATransactionFunction; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.hibernate.query.NativeQuery; +import org.junit.Test; + +import javax.persistence.*; +import java.util.Properties; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class GenericOracleJsonTypeBlobPropertyParameterSettingTest extends AbstractOracleIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Test + public void test() { + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .setProperties( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99" + + "}" + ) + ); + + return null; + } + }); + + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + Book book = entityManager + .createQuery( + "select b " + + "from Book b " + + "where b.isbn = :isbn", Book.class) + .setParameter("isbn", "978-9730228236") + .getSingleResult(); + + LOGGER.info("Book details: {}", book.getProperties()); + + assertTrue(book.getProperties().replaceAll(" ", "").contains("\"price\":44.99")); + + book.setProperties( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99," + + " \"url\": \"https://www.amazon.com/High-Performance-Java-Persistence-Vlad-Mihalcea/dp/973022823X/\"" + + "}" + ); + + return null; + } + }); + + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + JsonNode properties = (JsonNode) entityManager + .createNativeQuery( + "SELECT " + + " properties AS properties " + + "FROM book " + + "WHERE " + + " isbn = :isbn") + .setParameter("isbn", "978-9730228236") + .unwrap(NativeQuery.class) + .addScalar("properties", new JsonBlobType(JsonNode.class)) + .uniqueResult(); + + assertEquals("High-Performance Java Persistence", properties.get("title").asText()); + + return null; + } + }); + + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + Book book = (Book) entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + book.setProperties(null); + + return null; + } + }); + + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertNull(book.getProperties()); + + return null; + } + }); + } + + @Entity(name = "Book") + @Table(name = "book") + + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + @Type(type = "com.vladmihalcea.hibernate.type.json.JsonType") + @Column(columnDefinition = "BLOB") + private String properties; + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public String getProperties() { + return properties; + } + + public Book setProperties(String properties) { + this.properties = properties; + return this; + } + } +} diff --git a/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonTypeBlobPropertyTest.java b/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonTypeBlobPropertyTest.java new file mode 100644 index 000000000..4c9eb954c --- /dev/null +++ b/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonTypeBlobPropertyTest.java @@ -0,0 +1,164 @@ +package com.vladmihalcea.hibernate.type.json.generic; + +import com.fasterxml.jackson.databind.JsonNode; +import com.vladmihalcea.hibernate.type.json.JsonBlobType; +import com.vladmihalcea.hibernate.type.json.JsonType; +import com.vladmihalcea.hibernate.util.AbstractOracleIntegrationTest; +import com.vladmihalcea.hibernate.util.transaction.JPATransactionFunction; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.hibernate.query.NativeQuery; +import org.junit.Test; + +import javax.persistence.*; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class GenericOracleJsonTypeBlobPropertyTest extends AbstractOracleIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Test + public void test() { + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .setProperties( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99" + + "}" + ) + ); + + return null; + } + }); + + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + Book book = entityManager + .createQuery( + "select b " + + "from Book b " + + "where b.isbn = :isbn", Book.class) + .setParameter("isbn", "978-9730228236") + .getSingleResult(); + + LOGGER.info("Book details: {}", book.getProperties()); + + assertTrue(book.getProperties().replaceAll(" ", "").contains("\"price\":44.99")); + + book.setProperties( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99," + + " \"url\": \"https://www.amazon.com/High-Performance-Java-Persistence-Vlad-Mihalcea/dp/973022823X/\"" + + "}" + ); + + return null; + } + }); + + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + JsonNode properties = (JsonNode) entityManager + .createNativeQuery( + "SELECT " + + " properties AS properties " + + "FROM book " + + "WHERE " + + " isbn = :isbn") + .setParameter("isbn", "978-9730228236") + .unwrap(NativeQuery.class) + .addScalar("properties", new JsonBlobType(JsonNode.class)) + .uniqueResult(); + + assertEquals("High-Performance Java Persistence", properties.get("title").asText()); + + return null; + } + }); + + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + Book book = (Book) entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + book.setProperties(null); + + return null; + } + }); + + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertNull(book.getProperties()); + + return null; + } + }); + } + + @Entity(name = "Book") + @Table(name = "book") + @TypeDef(name = "json", typeClass = JsonType.class) + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + @Type(type = "json") + @Column(columnDefinition = "BLOB") + private String properties; + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public String getProperties() { + return properties; + } + + public Book setProperties(String properties) { + this.properties = properties; + return this; + } + } +} diff --git a/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonTypePropertyTest.java b/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonTypePropertyTest.java new file mode 100644 index 000000000..c2527f833 --- /dev/null +++ b/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonTypePropertyTest.java @@ -0,0 +1,164 @@ +package com.vladmihalcea.hibernate.type.json.generic; + +import com.fasterxml.jackson.databind.JsonNode; +import com.vladmihalcea.hibernate.type.json.JsonStringType; +import com.vladmihalcea.hibernate.type.json.JsonType; +import com.vladmihalcea.hibernate.util.AbstractOracleIntegrationTest; +import com.vladmihalcea.hibernate.util.transaction.JPATransactionFunction; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.hibernate.query.NativeQuery; +import org.junit.Test; + +import javax.persistence.*; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class GenericOracleJsonTypePropertyTest extends AbstractOracleIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Test + public void test() { + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .setProperties( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99" + + "}" + ) + ); + + return null; + } + }); + + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + Book book = entityManager + .createQuery( + "select b " + + "from Book b " + + "where b.isbn = :isbn", Book.class) + .setParameter("isbn", "978-9730228236") + .getSingleResult(); + + LOGGER.info("Book details: {}", book.getProperties()); + + assertTrue(book.getProperties().contains("\"price\":44.99")); + + book.setProperties( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99," + + " \"url\": \"https://www.amazon.com/High-Performance-Java-Persistence-Vlad-Mihalcea/dp/973022823X/\"" + + "}" + ); + + return null; + } + }); + + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + JsonNode properties = (JsonNode) entityManager + .createNativeQuery( + "SELECT " + + " properties AS properties " + + "FROM book " + + "WHERE " + + " isbn = :isbn") + .setParameter("isbn", "978-9730228236") + .unwrap(NativeQuery.class) + .addScalar("properties", new JsonStringType(JsonNode.class)) + .uniqueResult(); + + assertEquals("High-Performance Java Persistence", properties.get("title").asText()); + + return null; + } + }); + + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + Book book = (Book) entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + book.setProperties(null); + + return null; + } + }); + + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertNull(book.getProperties()); + + return null; + } + }); + } + + @Entity(name = "Book") + @Table(name = "book") + @TypeDef(name = "json", typeClass = JsonType.class) + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + @Type(type = "json") + @Column(columnDefinition = "json") + private String properties; + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public String getProperties() { + return properties; + } + + public Book setProperties(String properties) { + this.properties = properties; + return this; + } + } +} diff --git a/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonTypeVarcharPropertyTest.java b/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonTypeVarcharPropertyTest.java new file mode 100644 index 000000000..54501f367 --- /dev/null +++ b/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonTypeVarcharPropertyTest.java @@ -0,0 +1,168 @@ +package com.vladmihalcea.hibernate.type.json.generic; + +import com.fasterxml.jackson.databind.JsonNode; +import com.vladmihalcea.hibernate.type.json.JsonStringType; +import com.vladmihalcea.hibernate.type.json.JsonType; +import com.vladmihalcea.hibernate.type.util.Configuration; +import com.vladmihalcea.hibernate.util.AbstractOracleIntegrationTest; +import com.vladmihalcea.hibernate.util.transaction.JPATransactionFunction; +import org.hibernate.Session; +import org.hibernate.annotations.Check; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.hibernate.query.NativeQuery; +import org.junit.Test; + +import javax.persistence.*; +import java.util.Properties; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class GenericOracleJsonTypeVarcharPropertyTest extends AbstractOracleIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Test + public void test() { + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .setProperties( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99" + + "}" + ) + ); + + return null; + } + }); + + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + Book book = entityManager + .createQuery( + "select b " + + "from Book b " + + "where b.isbn = :isbn", Book.class) + .setParameter("isbn", "978-9730228236") + .getSingleResult(); + + LOGGER.info("Book details: {}", book.getProperties()); + + assertTrue(book.getProperties().replaceAll(" ", "").contains("\"price\":44.99")); + + book.setProperties( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99," + + " \"url\": \"https://www.amazon.com/High-Performance-Java-Persistence-Vlad-Mihalcea/dp/973022823X/\"" + + "}" + ); + + return null; + } + }); + + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + JsonNode properties = (JsonNode) entityManager + .createNativeQuery( + "SELECT " + + " properties AS properties " + + "FROM book " + + "WHERE " + + " isbn = :isbn") + .setParameter("isbn", "978-9730228236") + .unwrap(NativeQuery.class) + .addScalar("properties", new JsonStringType(JsonNode.class)) + .uniqueResult(); + + assertEquals("High-Performance Java Persistence", properties.get("title").asText()); + + return null; + } + }); + + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + Book book = (Book) entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + book.setProperties(null); + + return null; + } + }); + + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertNull(book.getProperties()); + + return null; + } + }); + } + + @Entity(name = "Book") + @Table(name = "book") + @TypeDef(name = "json", typeClass = JsonType.class) + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + @Type(type = "json") + @Column(columnDefinition = "VARCHAR2(1000)") + @Check(constraints = "properties IS JSON") + private String properties; + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public String getProperties() { + return properties; + } + + public Book setProperties(String properties) { + this.properties = properties; + return this; + } + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/MutableDynamicParameterizedType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/MutableDynamicParameterizedType.java index 745c55112..c6e6d4bb3 100644 --- a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/MutableDynamicParameterizedType.java +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/MutableDynamicParameterizedType.java @@ -32,5 +32,10 @@ public void setParameterValues(Properties parameters) { ParameterizedType parameterizedType = (ParameterizedType) javaTypeDescriptor; parameterizedType.setParameterValues(parameters); } + JDBC jdbcTypeDescriptor = getJdbcTypeDescriptor(); + if(jdbcTypeDescriptor instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) jdbcTypeDescriptor; + parameterizedType.setParameterValues(parameters); + } } } \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/JsonBlobType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/JsonBlobType.java index 45ca7b965..55bd9e251 100644 --- a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/JsonBlobType.java +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/JsonBlobType.java @@ -2,10 +2,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.vladmihalcea.hibernate.type.MutableDynamicParameterizedType; +import com.vladmihalcea.hibernate.type.json.internal.JsonBlobJdbcTypeDescriptor; import com.vladmihalcea.hibernate.type.json.internal.JsonJavaTypeDescriptor; import com.vladmihalcea.hibernate.type.util.Configuration; import com.vladmihalcea.hibernate.type.util.ObjectMapperWrapper; -import org.hibernate.type.descriptor.jdbc.BlobJdbcType; import java.lang.reflect.Type; import java.sql.Blob; @@ -26,14 +26,14 @@ * * @author Vlad Mihalcea */ -public class JsonBlobType extends MutableDynamicParameterizedType { +public class JsonBlobType extends MutableDynamicParameterizedType { public static final JsonBlobType INSTANCE = new JsonBlobType(); public JsonBlobType() { super( Object.class, - org.hibernate.type.descriptor.jdbc.BlobJdbcType.DEFAULT, + JsonBlobJdbcTypeDescriptor.INSTANCE, new JsonJavaTypeDescriptor(Configuration.INSTANCE.getObjectMapperWrapper()) ); } @@ -41,7 +41,7 @@ public JsonBlobType() { public JsonBlobType(Type javaType) { super( Object.class, - org.hibernate.type.descriptor.jdbc.BlobJdbcType.DEFAULT, + JsonBlobJdbcTypeDescriptor.INSTANCE, new JsonJavaTypeDescriptor(Configuration.INSTANCE.getObjectMapperWrapper(), javaType) ); } @@ -49,7 +49,7 @@ public JsonBlobType(Type javaType) { public JsonBlobType(Configuration configuration) { super( Object.class, - org.hibernate.type.descriptor.jdbc.BlobJdbcType.DEFAULT, + JsonBlobJdbcTypeDescriptor.INSTANCE, new JsonJavaTypeDescriptor(configuration.getObjectMapperWrapper()) ); } @@ -61,7 +61,7 @@ public JsonBlobType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapCon public JsonBlobType(ObjectMapper objectMapper) { super( Object.class, - org.hibernate.type.descriptor.jdbc.BlobJdbcType.DEFAULT, + JsonBlobJdbcTypeDescriptor.INSTANCE, new JsonJavaTypeDescriptor(new ObjectMapperWrapper(objectMapper)) ); } @@ -69,7 +69,7 @@ public JsonBlobType(ObjectMapper objectMapper) { public JsonBlobType(ObjectMapperWrapper objectMapperWrapper) { super( Object.class, - org.hibernate.type.descriptor.jdbc.BlobJdbcType.DEFAULT, + JsonBlobJdbcTypeDescriptor.INSTANCE, new JsonJavaTypeDescriptor(objectMapperWrapper) ); } @@ -77,7 +77,7 @@ public JsonBlobType(ObjectMapperWrapper objectMapperWrapper) { public JsonBlobType(ObjectMapper objectMapper, Type javaType) { super( Object.class, - org.hibernate.type.descriptor.jdbc.BlobJdbcType.DEFAULT, + JsonBlobJdbcTypeDescriptor.INSTANCE, new JsonJavaTypeDescriptor(new ObjectMapperWrapper(objectMapper), javaType) ); } @@ -85,7 +85,7 @@ public JsonBlobType(ObjectMapper objectMapper, Type javaType) { public JsonBlobType(ObjectMapperWrapper objectMapperWrapper, Type javaType) { super( Object.class, - org.hibernate.type.descriptor.jdbc.BlobJdbcType.DEFAULT, + JsonBlobJdbcTypeDescriptor.INSTANCE, new JsonJavaTypeDescriptor(objectMapperWrapper, javaType) ); } diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/JsonType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/JsonType.java index 209ecb7bf..76cdc6382 100644 --- a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/JsonType.java +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/JsonType.java @@ -14,20 +14,48 @@ * {@link JsonType} allows you to map any given JSON object (e.g., POJO, Map<String, Object>, List<T>, JsonNode) on any of the following database systems: *

*
    - *
  • PostgreSQL - for both jsonb and json column types
  • - *
  • MySQL - for the json column type
  • - *
  • SQL Server - for the NVARCHAR column type storing JSON
  • - *
  • Oracle - for the VARCHAR column type storing JSON
  • - *
  • H2 - for the json column type
  • + *
  • PostgreSQL - for both jsonb and json column types
  • + *
  • MySQL - for the json column type
  • + *
  • SQL Server - for the NVARCHAR column type storing JSON
  • + *
  • Oracle - for the JSON column type if you're using Oracle 21c or the VARCHAR column type storing JSON if you're using an older Oracle version
  • + *
  • H2 - for the json column type
  • *
- * + *

+ *

+ * If you switch to Oracle 21c from an older version, then you should also migrate your {@code JSON} columns to the native JSON type since this binary type performs better than + * {@code VARCHAR2} or {@code BLOB} column types. + *

+ *

+ * However, if you don't want to migrate to the new {@code JSON} data type, then you will have to change your {@link JsonType} mappings to use the {@code varchar} type on + * Oracle 21c or newer versions, like in the following example: + *

+ *
+ * {@code @Type(}
+ *     value = JsonType.class,
+ *     parameters = {@code @Parameter(}
+ *         name = "hibernate.types.default.json.type",
+ *         value = "varchar"
+ *     )
+ * )
+ * 
*

* For more details about how to use the {@link JsonType}, check out this article on vladmihalcea.com. *

*

- * If you are using Oracle and want to store JSON objects in a BLOB column types, then you should use the {@link JsonBlobType} instead. For more details, check out this article on vladmihalcea.com. + * If you are using Oracle and want to store JSON objects in a BLOB column type, then you can use the {@link JsonBlobType} instead. For more details, check out this article on vladmihalcea.com. + *

+ *

+ * Or, you can customize the {@link JsonType} as follows: *

- * + *
+ * {@code @Type(}
+ *     value = JsonType.class,
+ *     parameters = {@code @Parameter(}
+ *         name = "hibernate.types.default.json.type",
+ *         value = "blob"
+ *     )
+ * )
+ * 
* @author Vlad Mihalcea */ public class JsonType extends MutableDynamicParameterizedType { @@ -53,8 +81,9 @@ public JsonType(Type javaType) { public JsonType(Configuration configuration) { super( Object.class, - new JsonJdbcTypeDescriptor(), - new JsonJavaTypeDescriptor(configuration.getObjectMapperWrapper()) + new JsonJdbcTypeDescriptor(configuration.getProperties()), + new JsonJavaTypeDescriptor(configuration.getObjectMapperWrapper()), + configuration ); } diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/AbstractJsonJdbcTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/AbstractJsonJdbcTypeDescriptor.java index 80e54e2f9..5f087b10f 100644 --- a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/AbstractJsonJdbcTypeDescriptor.java +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/AbstractJsonJdbcTypeDescriptor.java @@ -5,11 +5,13 @@ import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.jdbc.BasicExtractor; import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.usertype.ParameterizedType; import java.sql.CallableStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; +import java.util.Properties; /** * @author Vlad Mihalcea diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBlobJdbcTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBlobJdbcTypeDescriptor.java new file mode 100644 index 000000000..220167b9b --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBlobJdbcTypeDescriptor.java @@ -0,0 +1,31 @@ +package com.vladmihalcea.hibernate.type.json.internal; + +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.BlobJdbcType; + +/** + * @author Vlad Mihalcea + */ +public class JsonBlobJdbcTypeDescriptor extends AbstractJsonJdbcTypeDescriptor { + + public static final JsonBlobJdbcTypeDescriptor INSTANCE = new JsonBlobJdbcTypeDescriptor(); + + private BlobJdbcType blobTypeDescriptor = BlobJdbcType.DEFAULT; + + @Override + public int getJdbcTypeCode() { + return blobTypeDescriptor.getJdbcTypeCode(); + } + + @Override + public ValueBinder getBinder(JavaType javaType) { + return blobTypeDescriptor.getBinder(javaType); + } + + @Override + public ValueExtractor getExtractor(JavaType javaType) { + return blobTypeDescriptor.getExtractor(javaType); + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBytesJdbcTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBytesJdbcTypeDescriptor.java index 51eecc9a6..bbd24480f 100644 --- a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBytesJdbcTypeDescriptor.java +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBytesJdbcTypeDescriptor.java @@ -1,5 +1,6 @@ package com.vladmihalcea.hibernate.type.json.internal; +import org.hibernate.dialect.Database; import org.hibernate.type.descriptor.ValueBinder; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.java.JavaType; @@ -7,6 +8,8 @@ import java.io.UnsupportedEncodingException; import java.sql.*; +import java.util.HashMap; +import java.util.Map; /** * @author Vlad Mihalcea @@ -15,11 +18,32 @@ public class JsonBytesJdbcTypeDescriptor extends AbstractJsonJdbcTypeDescriptor public static final JsonBytesJdbcTypeDescriptor INSTANCE = new JsonBytesJdbcTypeDescriptor(); + private static final Map INSTANCE_MAP = new HashMap<>(); + + static { + INSTANCE_MAP.put(Database.H2, INSTANCE); + INSTANCE_MAP.put(Database.ORACLE, new JsonBytesJdbcTypeDescriptor(2016)); + } + + public static JsonBytesJdbcTypeDescriptor of(Database database) { + return INSTANCE_MAP.get(database); + } + public static final String CHARSET = "UTF8"; + private final int jdbcType; + + public JsonBytesJdbcTypeDescriptor() { + this.jdbcType = Types.BINARY; + } + + public JsonBytesJdbcTypeDescriptor(int jdbcType) { + this.jdbcType = jdbcType; + } + @Override public int getJdbcTypeCode() { - return Types.BINARY; + return jdbcType; } @Override @@ -32,7 +56,7 @@ protected void doBind(PreparedStatement st, X value, int index, WrapperOptions o @Override protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) - throws SQLException { + throws SQLException { st.setBytes(name, toJsonBytes(JavaType.unwrap(value, String.class, options))); } }; diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonJdbcTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonJdbcTypeDescriptor.java index cb9e4ec5e..6d958ef9b 100644 --- a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonJdbcTypeDescriptor.java +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonJdbcTypeDescriptor.java @@ -1,24 +1,36 @@ package com.vladmihalcea.hibernate.type.json.internal; -import org.hibernate.dialect.Dialect; -import org.hibernate.dialect.H2Dialect; -import org.hibernate.dialect.PostgreSQLDialect; +import com.vladmihalcea.hibernate.type.util.ParameterTypeUtils; +import com.vladmihalcea.hibernate.util.StringUtils; +import org.hibernate.dialect.*; import org.hibernate.engine.jdbc.dialect.internal.StandardDialectResolver; import org.hibernate.engine.jdbc.dialect.spi.DatabaseMetaDataDialectResolutionInfoAdapter; 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.BasicBinder; +import org.hibernate.usertype.DynamicParameterizedType; +import org.hibernate.usertype.ParameterizedType; import java.sql.*; +import java.util.Properties; /** * @author Vlad Mihalcea */ -public class JsonJdbcTypeDescriptor extends AbstractJsonJdbcTypeDescriptor { +public class JsonJdbcTypeDescriptor extends AbstractJsonJdbcTypeDescriptor implements ParameterizedType { private volatile Dialect dialect; - private volatile AbstractJsonJdbcTypeDescriptor sqlTypeDescriptor; + private volatile AbstractJsonJdbcTypeDescriptor jdbcTypeDescriptor; + + private volatile Properties properties; + + public JsonJdbcTypeDescriptor() { + } + + public JsonJdbcTypeDescriptor(Properties properties) { + this.properties = properties; + } @Override public ValueBinder getBinder(final JavaType javaType) { @@ -32,7 +44,7 @@ protected void doBind(PreparedStatement st, X value, int index, WrapperOptions o @Override protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) - throws SQLException { + throws SQLException { sqlTypeDescriptor(st.getConnection()).getBinder(javaType).bind( st, value, name, options ); @@ -56,29 +68,65 @@ protected Object extractJson(CallableStatement statement, String name) throws SQ } private AbstractJsonJdbcTypeDescriptor sqlTypeDescriptor(Connection connection) { - if(sqlTypeDescriptor == null) { - sqlTypeDescriptor = resolveSqlTypeDescriptor(connection); + if (jdbcTypeDescriptor == null) { + jdbcTypeDescriptor = resolveJdbcTypeDescriptor(connection); } - return sqlTypeDescriptor; + return jdbcTypeDescriptor; } - private AbstractJsonJdbcTypeDescriptor resolveSqlTypeDescriptor(Connection connection) { + private AbstractJsonJdbcTypeDescriptor resolveJdbcTypeDescriptor(Connection connection) { try { StandardDialectResolver dialectResolver = new StandardDialectResolver(); - dialect = dialectResolver.resolveDialect( - new DatabaseMetaDataDialectResolutionInfoAdapter(connection.getMetaData()) - ); - if(PostgreSQLDialect.class.isInstance(dialect)) { + DatabaseMetaDataDialectResolutionInfoAdapter metaDataInfo = new DatabaseMetaDataDialectResolutionInfoAdapter(connection.getMetaData()); + dialect = dialectResolver.resolveDialect(metaDataInfo); + if (dialect instanceof PostgreSQLDialect) { return JsonBinaryJdbcTypeDescriptor.INSTANCE; - } else if(H2Dialect.class.isInstance(dialect)) { + } else if (dialect instanceof H2Dialect) { return JsonBytesJdbcTypeDescriptor.INSTANCE; - } else { - return JsonStringJdbcTypeDescriptor.INSTANCE; + } else if (dialect instanceof OracleDialect) { + if (properties != null) { + DynamicParameterizedType.ParameterType parameterType = ParameterTypeUtils.resolve(properties); + if (parameterType != null) { + String columnType = ParameterTypeUtils.getColumnType(parameterType); + if (!StringUtils.isBlank(columnType)) { + switch (columnType) { + case "json": + return JsonBytesJdbcTypeDescriptor.of(Database.ORACLE); + case "blob": + case "clob": + return JsonBlobJdbcTypeDescriptor.INSTANCE; + case "varchar": + case "varchar2": + case "string": + case "text": + return JsonStringJdbcTypeDescriptor.INSTANCE; + } + } + } + } + if (metaDataInfo.getDatabaseMajorVersion() >= 21) { + return JsonBytesJdbcTypeDescriptor.of(Database.ORACLE); + } } + return JsonStringJdbcTypeDescriptor.INSTANCE; } catch (SQLException e) { throw new IllegalStateException(e); } } + @Override + public int getJdbcTypeCode() { + return jdbcTypeDescriptor != null ? + jdbcTypeDescriptor.getJdbcTypeCode() : + super.getJdbcTypeCode(); + } + @Override + public void setParameterValues(Properties parameters) { + if (properties == null) { + properties = parameters; + } else { + properties.putAll(parameters); + } + } } diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/ParameterTypeUtils.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/ParameterTypeUtils.java new file mode 100644 index 000000000..1f789af33 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/ParameterTypeUtils.java @@ -0,0 +1,105 @@ +package com.vladmihalcea.hibernate.type.util; + +import com.vladmihalcea.hibernate.util.StringUtils; +import org.hibernate.usertype.DynamicParameterizedType; + +import jakarta.persistence.Column; +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * ParameterizedTypeUtils - {@link DynamicParameterizedType.ParameterType} utilities holder. + * + * @author Vlad Mihalcea + * @since 2.16.0 + */ +public class ParameterTypeUtils { + + private static final Pattern COLUMN_TYPE_PATTERN = Pattern.compile("([a-zA-Z0-9]+).*?"); + + private ParameterTypeUtils() { + throw new UnsupportedOperationException("StringUtils is not instantiable!"); + } + + /** + * Resolve the {@link DynamicParameterizedType.ParameterType} instance + * from the provided {@link Properties} object. + * + * @param properties configuration properties + * @return {@link DynamicParameterizedType.ParameterType} instance + */ + public static DynamicParameterizedType.ParameterType resolve(Properties properties) { + Object parameterTypeObject = properties.get(DynamicParameterizedType.PARAMETER_TYPE); + if (parameterTypeObject instanceof DynamicParameterizedType.ParameterType) { + return (DynamicParameterizedType.ParameterType) parameterTypeObject; + } + return null; + } + + /** + * Get the required annotation from the {@link DynamicParameterizedType.ParameterType} instance. + * + * @param parameterType {@link DynamicParameterizedType.ParameterType} instance + * @param annotationClass annotation class + * @return annotation + */ + @SuppressWarnings("unchecked") + public static A getAnnotationOrNull(DynamicParameterizedType.ParameterType parameterType, Class annotationClass) { + List annotations = getAnnotations(parameterType, annotationClass); + if(annotations.size() > 1) { + throw new IllegalArgumentException( + String.format( + "The provided ParameterType associated with the [%s] property contains more than one annotation of the [%s] type!", + parameterType.getReturnedClass(), + annotationClass.getName() + ) + ); + } + return (A) Arrays.stream(parameterType.getAnnotationsMethod()) + .filter(a -> annotationClass.isAssignableFrom(a.annotationType())) + .findAny() + .orElse(null); + } + + /** + * Get the required annotations from the {@link DynamicParameterizedType.ParameterType} instance. + * + * @param parameterType {@link DynamicParameterizedType.ParameterType} instance + * @param annotationClass annotation class + * @return annotations + */ + @SuppressWarnings("unchecked") + public static List getAnnotations(DynamicParameterizedType.ParameterType parameterType, Class annotationClass) { + return Arrays.stream(parameterType.getAnnotationsMethod()) + .filter(a -> annotationClass.isAssignableFrom(a.annotationType())) + .map(a -> (A) a) + .collect(Collectors.toList()); + } + + /** + * Get the column type association from the {@link DynamicParameterizedType.ParameterType} instance. + * + * @param parameterType {@link DynamicParameterizedType.ParameterType} instance + * @return column type + */ + public static String getColumnType(DynamicParameterizedType.ParameterType parameterType) { + if (parameterType != null) { + Column columnAnnotation = ParameterTypeUtils.getAnnotationOrNull(parameterType, Column.class); + if(columnAnnotation != null) { + String columnDefinition = columnAnnotation.columnDefinition(); + if(!StringUtils.isBlank(columnDefinition)) { + Matcher matcher = COLUMN_TYPE_PATTERN.matcher(columnDefinition); + if (matcher.matches()) { + return StringUtils.toLowercase(matcher.group(1)); + } + } + } + } + return null; + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/util/StringUtils.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/util/StringUtils.java index 473aff942..6a5a95a47 100644 --- a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/util/StringUtils.java +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/util/StringUtils.java @@ -1,5 +1,7 @@ package com.vladmihalcea.hibernate.util; +import java.util.Locale; + /** * StringUtils - String utilities holder. * @@ -38,4 +40,25 @@ public static String join(CharSequence delimiter, CharSequence... elements) { return builder.toString(); } + /** + * Check if the String value is null, empty or contains only whitespace characters. + * @param value String value + * @return if the string is blank + */ + public static boolean isBlank(String value) { + return value == null || value.isEmpty() || value.trim().isEmpty(); + } + + /** + * Transform string to lowercase. + * + * @param value String value + * @return String value in lowercase + */ + public static String toLowercase(String value) { + if(isBlank(value)) { + return value; + } + return value.toLowerCase(Locale.ROOT); + } } diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/MySQLJsonTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericMySQLJsonTypeTest.java similarity index 95% rename from hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/MySQLJsonTypeTest.java rename to hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericMySQLJsonTypeTest.java index 13df792b1..5c2b8db22 100644 --- a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/MySQLJsonTypeTest.java +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericMySQLJsonTypeTest.java @@ -1,5 +1,6 @@ -package com.vladmihalcea.hibernate.type.json; +package com.vladmihalcea.hibernate.type.json.generic; +import com.vladmihalcea.hibernate.type.json.JsonType; import com.vladmihalcea.hibernate.type.model.BaseEntity; import com.vladmihalcea.hibernate.type.model.Location; import com.vladmihalcea.hibernate.type.model.Ticket; @@ -20,7 +21,7 @@ /** * @author Vlad Mihalcea */ -public class MySQLJsonTypeTest extends AbstractMySQLIntegrationTest { +public class GenericMySQLJsonTypeTest extends AbstractMySQLIntegrationTest { @Override protected Class[] entities() { diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/OffsetDateTimeJsonTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOffsetDateTimeJsonTest.java similarity index 93% rename from hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/OffsetDateTimeJsonTest.java rename to hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOffsetDateTimeJsonTest.java index 7e44b82e6..8c0e08aa4 100644 --- a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/OffsetDateTimeJsonTest.java +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOffsetDateTimeJsonTest.java @@ -1,5 +1,6 @@ -package com.vladmihalcea.hibernate.type.json; +package com.vladmihalcea.hibernate.type.json.generic; +import com.vladmihalcea.hibernate.type.json.JsonType; import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -18,7 +19,7 @@ /** * @author Vlad Mihalcea */ -public class OffsetDateTimeJsonTest extends AbstractPostgreSQLIntegrationTest { +public class GenericOffsetDateTimeJsonTest extends AbstractPostgreSQLIntegrationTest { @Override protected Class[] entities() { diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonMapTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonMapTest.java index 668dd4f23..b6fb816d4 100644 --- a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonMapTest.java +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonMapTest.java @@ -1,6 +1,7 @@ package com.vladmihalcea.hibernate.type.json.generic; import com.vladmihalcea.hibernate.type.json.JsonType; +import com.vladmihalcea.hibernate.type.util.Configuration; import com.vladmihalcea.hibernate.util.AbstractOracleIntegrationTest; import jakarta.persistence.*; import org.hibernate.Session; @@ -71,7 +72,13 @@ public static class Book { @Column(length = 15) private String isbn; - @Type(JsonType.class) + @Type( + value = JsonType.class, + parameters = @org.hibernate.annotations.Parameter( + name = "hibernate.types.default.json.type", + value = "varchar" + ) + ) @Column(columnDefinition = "VARCHAR2(1000)") @Check(constraints = "properties IS JSON") private Map properties = new HashMap<>(); diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonTypeBlobPropertyTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonTypeBlobPropertyTest.java new file mode 100644 index 000000000..05d619125 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonTypeBlobPropertyTest.java @@ -0,0 +1,175 @@ +package com.vladmihalcea.hibernate.type.json.generic; + +import com.fasterxml.jackson.databind.JsonNode; +import com.vladmihalcea.hibernate.type.json.JsonBlobType; +import com.vladmihalcea.hibernate.type.json.JsonType; +import com.vladmihalcea.hibernate.util.AbstractOracleIntegrationTest; +import com.vladmihalcea.hibernate.util.transaction.JPATransactionFunction; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.hibernate.jpa.boot.spi.TypeContributorList; +import org.hibernate.query.NativeQuery; +import org.junit.Test; + +import java.util.Collections; +import java.util.Properties; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class GenericOracleJsonTypeBlobPropertyTest extends AbstractOracleIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.type_contributors", + (TypeContributorList) () -> Collections.singletonList( + (typeContributions, serviceRegistry) -> { + typeContributions.contributeType(new JsonBlobType(JsonNode.class)); + } + )); + } + + @Test + public void test() { + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .setProperties( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99" + + "}" + ) + ); + + return null; + } + }); + + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + Book book = entityManager + .createQuery( + "select b " + + "from Book b " + + "where b.isbn = :isbn", Book.class) + .setParameter("isbn", "978-9730228236") + .getSingleResult(); + + LOGGER.info("Book details: {}", book.getProperties()); + + assertTrue(book.getProperties().replaceAll(" ", "").contains("\"price\":44.99")); + + book.setProperties( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99," + + " \"url\": \"https://www.amazon.com/High-Performance-Java-Persistence-Vlad-Mihalcea/dp/973022823X/\"" + + "}" + ); + + return null; + } + }); + + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + JsonNode properties = (JsonNode) entityManager + .createNativeQuery( + "SELECT " + + " properties AS properties " + + "FROM book " + + "WHERE " + + " isbn = :isbn") + .setParameter("isbn", "978-9730228236") + .unwrap(NativeQuery.class) + .addScalar("properties", JsonNode.class) + .uniqueResult(); + + assertEquals("High-Performance Java Persistence", properties.get("title").asText()); + + return null; + } + }); + + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + Book book = (Book) entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + book.setProperties(null); + + return null; + } + }); + + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertNull(book.getProperties()); + + return null; + } + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + @Type(JsonType.class) + @Column(columnDefinition = "BLOB") + private String properties; + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public String getProperties() { + return properties; + } + + public Book setProperties(String properties) { + this.properties = properties; + return this; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonTypePropertyTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonTypePropertyTest.java new file mode 100644 index 000000000..b098e919c --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonTypePropertyTest.java @@ -0,0 +1,175 @@ +package com.vladmihalcea.hibernate.type.json.generic; + +import com.fasterxml.jackson.databind.JsonNode; +import com.vladmihalcea.hibernate.type.json.JsonStringType; +import com.vladmihalcea.hibernate.type.json.JsonType; +import com.vladmihalcea.hibernate.util.AbstractOracleIntegrationTest; +import com.vladmihalcea.hibernate.util.transaction.JPATransactionFunction; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.hibernate.jpa.boot.spi.TypeContributorList; +import org.hibernate.query.NativeQuery; +import org.junit.Test; + +import java.util.Collections; +import java.util.Properties; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class GenericOracleJsonTypePropertyTest extends AbstractOracleIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.type_contributors", + (TypeContributorList) () -> Collections.singletonList( + (typeContributions, serviceRegistry) -> { + typeContributions.contributeType(new JsonStringType(JsonNode.class)); + } + )); + } + + @Test + public void test() { + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .setProperties( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99" + + "}" + ) + ); + + return null; + } + }); + + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + Book book = entityManager + .createQuery( + "select b " + + "from Book b " + + "where b.isbn = :isbn", Book.class) + .setParameter("isbn", "978-9730228236") + .getSingleResult(); + + LOGGER.info("Book details: {}", book.getProperties()); + + assertTrue(book.getProperties().contains("\"price\":44.99")); + + book.setProperties( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99," + + " \"url\": \"https://www.amazon.com/High-Performance-Java-Persistence-Vlad-Mihalcea/dp/973022823X/\"" + + "}" + ); + + return null; + } + }); + + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + JsonNode properties = (JsonNode) entityManager + .createNativeQuery( + "SELECT " + + " properties AS properties " + + "FROM book " + + "WHERE " + + " isbn = :isbn") + .setParameter("isbn", "978-9730228236") + .unwrap(NativeQuery.class) + .addScalar("properties", JsonNode.class) + .uniqueResult(); + + assertEquals("High-Performance Java Persistence", properties.get("title").asText()); + + return null; + } + }); + + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + Book book = (Book) entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + book.setProperties(null); + + return null; + } + }); + + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertNull(book.getProperties()); + + return null; + } + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + @Type(JsonType.class) + @Column(columnDefinition = "json") + private String properties; + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public String getProperties() { + return properties; + } + + public Book setProperties(String properties) { + this.properties = properties; + return this; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonTypeVarcharPropertyTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonTypeVarcharPropertyTest.java new file mode 100644 index 000000000..f7cf10447 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonTypeVarcharPropertyTest.java @@ -0,0 +1,177 @@ +package com.vladmihalcea.hibernate.type.json.generic; + +import com.fasterxml.jackson.databind.JsonNode; +import com.vladmihalcea.hibernate.type.json.JsonStringType; +import com.vladmihalcea.hibernate.type.json.JsonType; +import com.vladmihalcea.hibernate.util.AbstractOracleIntegrationTest; +import com.vladmihalcea.hibernate.util.transaction.JPATransactionFunction; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.Check; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.hibernate.jpa.boot.spi.TypeContributorList; +import org.hibernate.query.NativeQuery; +import org.junit.Test; + +import java.util.Collections; +import java.util.Properties; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class GenericOracleJsonTypeVarcharPropertyTest extends AbstractOracleIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.type_contributors", + (TypeContributorList) () -> Collections.singletonList( + (typeContributions, serviceRegistry) -> { + typeContributions.contributeType(new JsonStringType(JsonNode.class)); + } + )); + } + + @Test + public void test() { + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .setProperties( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99" + + "}" + ) + ); + + return null; + } + }); + + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + Book book = entityManager + .createQuery( + "select b " + + "from Book b " + + "where b.isbn = :isbn", Book.class) + .setParameter("isbn", "978-9730228236") + .getSingleResult(); + + LOGGER.info("Book details: {}", book.getProperties()); + + assertTrue(book.getProperties().replaceAll(" ", "").contains("\"price\":44.99")); + + book.setProperties( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99," + + " \"url\": \"https://www.amazon.com/High-Performance-Java-Persistence-Vlad-Mihalcea/dp/973022823X/\"" + + "}" + ); + + return null; + } + }); + + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + JsonNode properties = (JsonNode) entityManager + .createNativeQuery( + "SELECT " + + " properties AS properties " + + "FROM book " + + "WHERE " + + " isbn = :isbn") + .setParameter("isbn", "978-9730228236") + .unwrap(NativeQuery.class) + .addScalar("properties", JsonNode.class) + .uniqueResult(); + + assertEquals("High-Performance Java Persistence", properties.get("title").asText()); + + return null; + } + }); + + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + book.setProperties(null); + + return null; + } + }); + + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertNull(book.getProperties()); + + return null; + } + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + @Type(JsonType.class) + @Column(columnDefinition = "VARCHAR2(1000)") + @Check(constraints = "properties IS JSON") + private String properties; + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public String getProperties() { + return properties; + } + + public Book setProperties(String properties) { + this.properties = properties; + return this; + } + } +} diff --git a/pom.xml b/pom.xml index 8400ee061..0167579a8 100644 --- a/pom.xml +++ b/pom.xml @@ -359,7 +359,7 @@ 2.2.8 1.4.200 5.1.47 - 19.3.0.0 + 21.5.0.0 6.4.0.jre8