HHH-15748 Use JSON DDL type on Oracle 21+ and BLOB on 12+

This commit is contained in:
Christian Beikov 2022-11-24 13:41:59 +01:00
parent 5b5721f64b
commit 276b7a6f95
17 changed files with 352 additions and 16 deletions

View File

@ -463,6 +463,8 @@ alter database drop logfile group 1;
alter database drop logfile group 2;
alter database drop logfile group 3;
alter system set open_cursors=1000 sid='*' scope=both;
create user hibernate_orm_test identified by hibernate_orm_test quota unlimited on users;
grant all privileges to hibernate_orm_test;
EOF\""
}
@ -502,6 +504,8 @@ alter system set open_cursors=1000 sid='*' scope=both;
alter system set processes=150 scope=spfile;
alter system set filesystemio_options=asynch scope=spfile;
alter system set disk_asynch_io=true scope=spfile;
create user hibernate_orm_test identified by hibernate_orm_test quota unlimited on users;
grant all privileges to hibernate_orm_test;
EOF\""
echo "Waiting for Oracle to restart after configuration..."
$CONTAINER_CLI stop oracle

View File

@ -6,6 +6,9 @@
*/
package org.hibernate.userguide.mapping.basic;
import java.nio.charset.StandardCharsets;
import java.sql.Blob;
import java.sql.Clob;
import java.util.List;
import java.util.Map;
@ -107,13 +110,33 @@ public abstract class JsonMappingTests {
assertThat( entityWithJson.objectMap, is( objectMap ) );
assertThat( entityWithJson.list, is( list ) );
assertThat( entityWithJson.jsonString, isOneOf( json, alternativeJson ) );
String nativeJson = session.createNativeQuery(
Object nativeJson = session.createNativeQuery(
"select jsonString from EntityWithJson",
String.class
Object.class
)
.getResultList()
.get( 0 );
assertThat( nativeJson, isOneOf( json, alternativeJson ) );
final String jsonText;
try {
if ( nativeJson instanceof Blob ) {
final Blob blob = (Blob) nativeJson;
jsonText = new String(
blob.getBytes( 1L, (int) blob.length() ),
StandardCharsets.UTF_8
);
}
else if ( nativeJson instanceof Clob ) {
final Clob jsonClob = (Clob) nativeJson;
jsonText = jsonClob.getSubString( 1L, (int) jsonClob.length() );
}
else {
jsonText = (String) nativeJson;
}
}
catch (Exception e) {
throw new RuntimeException( e );
}
assertThat( jsonText, isOneOf( json, alternativeJson ) );
}
);
}

View File

@ -131,8 +131,8 @@ ext {
oracle_ci : [
'db.dialect' : 'org.hibernate.dialect.OracleDialect',
'jdbc.driver': 'oracle.jdbc.OracleDriver',
'jdbc.user' : 'SYSTEM',
'jdbc.pass' : 'Oracle18',
'jdbc.user' : 'hibernate_orm_test',
'jdbc.pass' : 'hibernate_orm_test',
'jdbc.url' : 'jdbc:oracle:thin:@' + dbHost + ':1521:XE',
'connection.init_sql' : ''
],

View File

@ -62,6 +62,8 @@ dependencies {
implementation libs.logging
compileOnly libs.loggingAnnotations
// Used for compiling some Oracle specific JdbcTypes
compileOnly dbLibs.oracle
// JUnit dependencies made up of:
// * JUnit 5

View File

@ -23,6 +23,7 @@ import org.hibernate.dialect.BooleanDecoder;
import org.hibernate.dialect.DatabaseVersion;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.OracleArrayJdbcType;
import org.hibernate.dialect.OracleTypes;
import org.hibernate.dialect.OracleTypesHelper;
import org.hibernate.dialect.OracleXmlJdbcType;
import org.hibernate.dialect.Replacer;
@ -85,6 +86,7 @@ import org.hibernate.type.StandardBasicTypes;
import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType;
import org.hibernate.type.descriptor.jdbc.BlobJdbcType;
import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.hibernate.type.descriptor.jdbc.JsonBlobJdbcType;
import org.hibernate.type.descriptor.jdbc.NullJdbcType;
import org.hibernate.type.descriptor.jdbc.ObjectNullAsNullTypeJdbcType;
import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry;
@ -109,6 +111,7 @@ import static org.hibernate.type.SqlTypes.DATE;
import static org.hibernate.type.SqlTypes.DECIMAL;
import static org.hibernate.type.SqlTypes.GEOMETRY;
import static org.hibernate.type.SqlTypes.INTEGER;
import static org.hibernate.type.SqlTypes.JSON;
import static org.hibernate.type.SqlTypes.NUMERIC;
import static org.hibernate.type.SqlTypes.NVARCHAR;
import static org.hibernate.type.SqlTypes.REAL;
@ -632,6 +635,12 @@ public class OracleLegacyDialect extends Dialect {
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( SQLXML, "SYS.XMLTYPE", this ) );
if ( getVersion().isSameOrAfter( 10 ) ) {
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( GEOMETRY, "MDSYS.SDO_GEOMETRY", this ) );
if ( getVersion().isSameOrAfter( 21 ) ) {
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "json", this ) );
}
else if ( getVersion().isSameOrAfter( 12 ) ) {
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "blob", this ) );
}
}
}
@ -669,6 +678,8 @@ public class OracleLegacyDialect extends Dialect {
int scale,
JdbcTypeRegistry jdbcTypeRegistry) {
switch ( jdbcTypeCode ) {
case OracleTypes.JSON:
return jdbcTypeRegistry.getDescriptor( JSON );
case Types.NUMERIC:
if ( scale == -127 ) {
// For some reason, the Oracle JDBC driver reports FLOAT
@ -744,6 +755,13 @@ public class OracleLegacyDialect extends Dialect {
BlobJdbcType.DEFAULT;
typeContributions.contributeJdbcType( descriptor );
if ( getVersion().isSameOrAfter( 21 ) ) {
typeContributions.contributeJdbcType( OracleTypesHelper.INSTANCE.getJsonJdbcType() );
}
else {
typeContributions.contributeJdbcType( JsonBlobJdbcType.INSTANCE );
}
}
typeContributions.contributeJdbcType( OracleArrayJdbcType.INSTANCE );

View File

@ -74,6 +74,7 @@ import org.hibernate.type.StandardBasicTypes;
import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType;
import org.hibernate.type.descriptor.jdbc.BlobJdbcType;
import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.hibernate.type.descriptor.jdbc.JsonBlobJdbcType;
import org.hibernate.type.descriptor.jdbc.NullJdbcType;
import org.hibernate.type.descriptor.jdbc.ObjectNullAsNullTypeJdbcType;
import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry;
@ -608,6 +609,12 @@ public class OracleDialect extends Dialect {
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( SQLXML, "SYS.XMLTYPE", this ) );
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( GEOMETRY, "MDSYS.SDO_GEOMETRY", this ) );
if ( getVersion().isSameOrAfter( 21 ) ) {
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "json", this ) );
}
else if ( getVersion().isSameOrAfter( 12 ) ) {
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "blob", this ) );
}
}
@Override
@ -643,6 +650,8 @@ public class OracleDialect extends Dialect {
int scale,
JdbcTypeRegistry jdbcTypeRegistry) {
switch ( jdbcTypeCode ) {
case OracleTypes.JSON:
return jdbcTypeRegistry.getDescriptor( JSON );
case Types.NUMERIC:
if ( scale == -127 ) {
// For some reason, the Oracle JDBC driver reports FLOAT
@ -718,6 +727,13 @@ public class OracleDialect extends Dialect {
BlobJdbcType.DEFAULT;
typeContributions.contributeJdbcType( descriptor );
if ( getVersion().isSameOrAfter( 21 ) ) {
typeContributions.contributeJdbcType( OracleTypesHelper.INSTANCE.getJsonJdbcType() );
}
else {
typeContributions.contributeJdbcType( JsonBlobJdbcType.INSTANCE );
}
}
typeContributions.contributeJdbcType( OracleArrayJdbcType.INSTANCE );

View File

@ -0,0 +1,118 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.dialect;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.hibernate.type.SqlTypes;
import org.hibernate.type.descriptor.ValueBinder;
import org.hibernate.type.descriptor.ValueExtractor;
import org.hibernate.type.descriptor.WrapperOptions;
import org.hibernate.type.descriptor.java.JavaType;
import org.hibernate.type.descriptor.jdbc.BasicBinder;
import org.hibernate.type.descriptor.jdbc.BasicExtractor;
import org.hibernate.type.descriptor.jdbc.JdbcLiteralFormatter;
import org.hibernate.type.descriptor.jdbc.JdbcType;
import oracle.jdbc.OracleType;
/**
* Specialized type mapping for {@code JSON} that encodes as OSON.
* This class is used from {@link OracleTypesHelper} reflectively to avoid loading Oracle JDBC classes eagerly.
*
* @author Christian Beikov
*/
public class OracleJsonJdbcType implements JdbcType {
/**
* Singleton access
*/
public static final OracleJsonJdbcType INSTANCE = new OracleJsonJdbcType();
private static final int JSON_TYPE_CODE = OracleType.JSON.getVendorTypeNumber();
@Override
public int getJdbcTypeCode() {
return SqlTypes.BLOB;
}
@Override
public int getDefaultSqlTypeCode() {
return SqlTypes.JSON;
}
@Override
public String toString() {
return "OracleJsonJdbcType";
}
@Override
public <T> JdbcLiteralFormatter<T> getJdbcLiteralFormatter(JavaType<T> javaType) {
// No literal support for now
return null;
}
@Override
public <X> ValueBinder<X> getBinder(JavaType<X> javaType) {
return new BasicBinder<>( javaType, this ) {
@Override
protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options)
throws SQLException {
final String json = options.getSessionFactory().getFastSessionServices().getJsonFormatMapper().toString(
value,
getJavaType(),
options
);
st.setObject( index, json, JSON_TYPE_CODE );
}
@Override
protected void doBind(CallableStatement st, X value, String name, WrapperOptions options)
throws SQLException {
final String json = options.getSessionFactory().getFastSessionServices().getJsonFormatMapper().toString(
value,
getJavaType(),
options
);
st.setObject( name, json, JSON_TYPE_CODE );
}
};
}
@Override
public <X> ValueExtractor<X> getExtractor(JavaType<X> javaType) {
return new BasicExtractor<>( javaType, this ) {
@Override
protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException {
return getObject( rs.getString( paramIndex ), options );
}
@Override
protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException {
return getObject( statement.getString( index ), options );
}
@Override
protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException {
return getObject( statement.getString( name ), options );
}
private X getObject(String json, WrapperOptions options) throws SQLException {
if ( json == null ) {
return null;
}
return options.getSessionFactory().getFastSessionServices().getJsonFormatMapper().fromString(
json,
getJavaType(),
options
);
}
};
}
}

View File

@ -0,0 +1,14 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.dialect;
/**
* The Oracle specific JDBC type code.
*/
public class OracleTypes {
public static final int JSON = 2016;
}

View File

@ -9,6 +9,8 @@ package org.hibernate.dialect;
import org.hibernate.HibernateException;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.util.ReflectHelper;
import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.hibernate.type.descriptor.jdbc.JsonJdbcType;
import org.jboss.logging.Logger;
@ -27,8 +29,10 @@ public class OracleTypesHelper {
private static final String ORACLE_TYPES_CLASS_NAME = "oracle.jdbc.OracleTypes";
private static final String DEPRECATED_ORACLE_TYPES_CLASS_NAME = "oracle.jdbc.driver.OracleTypes";
private static final String ORACLE_JSON_JDBC_TYPE_CLASS_NAME = "org.hibernate.dialect.OracleJsonJdbcType";
private final int oracleCursorTypeSqlType;
private final JdbcType jsonJdbcType;
private OracleTypesHelper() {
int typeCode = -99;
@ -39,6 +43,17 @@ public class OracleTypesHelper {
log.warn( "Unable to resolve Oracle CURSOR JDBC type code: the class OracleTypesHelper was initialized but the Oracle JDBC driver could not be loaded." );
}
oracleCursorTypeSqlType = typeCode;
JdbcType jsonJdbcType = JsonJdbcType.INSTANCE;
try {
jsonJdbcType = (JdbcType) ReflectHelper.classForName( ORACLE_JSON_JDBC_TYPE_CLASS_NAME )
.getField( "INSTANCE" )
.get( null );
}
catch (Exception e) {
log.warn( "Unable to resolve OracleJsonJdbcType: the class OracleTypesHelper was initialized but the Oracle JDBC driver could not be loaded." );
}
this.jsonJdbcType = jsonJdbcType;
}
private int extractOracleCursorTypeValue() {
@ -75,6 +90,10 @@ public class OracleTypesHelper {
return oracleCursorTypeSqlType;
}
public JdbcType getJsonJdbcType() {
return jsonJdbcType;
}
// initial code as copied from Oracle8iDialect
//
// private int oracleCursorTypeSqlType = INIT_ORACLETYPES_CURSOR_VALUE;

View File

@ -820,7 +820,7 @@ public abstract class AbstractSharedSessionContract implements SharedSessionCont
else if ( getFactory().getMappingMetamodel().isEntityClass(resultClass) ) {
query.addEntity( "alias1", resultClass.getName(), LockMode.READ );
}
else {
else if ( resultClass != Object.class && resultClass != Object[].class ) {
query.addScalar( 1, resultClass );
}
return query;

View File

@ -191,6 +191,7 @@ public class JdbcTypeJavaClassMappings {
workMap.put( SqlTypes.ROWID, RowId.class );
workMap.put( SqlTypes.SQLXML, SQLXML.class );
workMap.put( SqlTypes.UUID, UUID.class );
workMap.put( SqlTypes.JSON, String.class );
workMap.put( SqlTypes.INET, InetAddress.class );
workMap.put( SqlTypes.TIMESTAMP_UTC, Instant.class );
workMap.put( SqlTypes.INTERVAL_SECOND, Duration.class );

View File

@ -0,0 +1,110 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.type.descriptor.jdbc;
import java.nio.charset.StandardCharsets;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.hibernate.type.SqlTypes;
import org.hibernate.type.descriptor.ValueBinder;
import org.hibernate.type.descriptor.ValueExtractor;
import org.hibernate.type.descriptor.WrapperOptions;
import org.hibernate.type.descriptor.java.JavaType;
/**
* Specialized type mapping for {@code JSON} and the BLOB SQL data type.
*
* @author Christian Beikov
*/
public class JsonBlobJdbcType implements JdbcType {
/**
* Singleton access
*/
public static final JsonBlobJdbcType INSTANCE = new JsonBlobJdbcType();
@Override
public int getJdbcTypeCode() {
return SqlTypes.BLOB;
}
@Override
public int getDefaultSqlTypeCode() {
return SqlTypes.JSON;
}
@Override
public String toString() {
return "JsonBlobJdbcType";
}
@Override
public <T> JdbcLiteralFormatter<T> getJdbcLiteralFormatter(JavaType<T> javaType) {
// No literal support for now
return null;
}
@Override
public <X> ValueBinder<X> getBinder(JavaType<X> javaType) {
return new BasicBinder<>( javaType, this ) {
@Override
protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options)
throws SQLException {
final String json = options.getSessionFactory().getFastSessionServices().getJsonFormatMapper().toString(
value,
getJavaType(),
options
);
st.setBytes( index, json.getBytes( StandardCharsets.UTF_8 ) );
}
@Override
protected void doBind(CallableStatement st, X value, String name, WrapperOptions options)
throws SQLException {
final String json = options.getSessionFactory().getFastSessionServices().getJsonFormatMapper().toString(
value,
getJavaType(),
options
);
st.setBytes( name, json.getBytes( StandardCharsets.UTF_8 ) );
}
};
}
@Override
public <X> ValueExtractor<X> getExtractor(JavaType<X> javaType) {
return new BasicExtractor<>( javaType, this ) {
@Override
protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException {
return getObject( rs.getBytes( paramIndex ), options );
}
@Override
protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException {
return getObject( statement.getBytes( index ), options );
}
@Override
protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException {
return getObject( statement.getBytes( name ), options );
}
private X getObject(byte[] json, WrapperOptions options) throws SQLException {
if ( json == null ) {
return null;
}
return options.getSessionFactory().getFastSessionServices().getJsonFormatMapper().fromString(
new String( json, StandardCharsets.UTF_8 ),
getJavaType(),
options
);
}
};
}
}

View File

@ -32,7 +32,7 @@ public final class JacksonJsonFormatMapper implements FormatMapper {
@Override
public <T> T fromString(CharSequence charSequence, JavaType<T> javaType, WrapperOptions wrapperOptions) {
if ( javaType.getJavaType() == String.class ) {
if ( javaType.getJavaType() == String.class || javaType.getJavaType() == Object.class ) {
return (T) charSequence.toString();
}
try {
@ -45,7 +45,7 @@ public final class JacksonJsonFormatMapper implements FormatMapper {
@Override
public <T> String toString(T value, JavaType<T> javaType, WrapperOptions wrapperOptions) {
if ( javaType.getJavaType() == String.class ) {
if ( javaType.getJavaType() == String.class || javaType.getJavaType() == Object.class ) {
return (String) value;
}
try {

View File

@ -33,7 +33,7 @@ public final class JacksonXmlFormatMapper implements FormatMapper {
@Override
public <T> T fromString(CharSequence charSequence, JavaType<T> javaType, WrapperOptions wrapperOptions) {
if ( javaType.getJavaType() == String.class ) {
if ( javaType.getJavaType() == String.class || javaType.getJavaType() == Object.class ) {
return (T) charSequence.toString();
}
try {
@ -46,7 +46,7 @@ public final class JacksonXmlFormatMapper implements FormatMapper {
@Override
public <T> String toString(T value, JavaType<T> javaType, WrapperOptions wrapperOptions) {
if ( javaType.getJavaType() == String.class ) {
if ( javaType.getJavaType() == String.class || javaType.getJavaType() == Object.class ) {
return (String) value;
}
try {

View File

@ -33,7 +33,7 @@ public final class JsonBJsonFormatMapper implements FormatMapper {
@Override
public <T> T fromString(CharSequence charSequence, JavaType<T> javaType, WrapperOptions wrapperOptions) {
if ( javaType.getJavaType() == String.class ) {
if ( javaType.getJavaType() == String.class || javaType.getJavaType() == Object.class ) {
return (T) charSequence.toString();
}
try {
@ -46,7 +46,7 @@ public final class JsonBJsonFormatMapper implements FormatMapper {
@Override
public <T> String toString(T value, JavaType<T> javaType, WrapperOptions wrapperOptions) {
if ( javaType.getJavaType() == String.class ) {
if ( javaType.getJavaType() == String.class || javaType.getJavaType() == Object.class ) {
return (String) value;
}
try {

View File

@ -51,7 +51,7 @@ public final class JaxbXmlFormatMapper implements FormatMapper {
@Override
public <T> T fromString(CharSequence charSequence, JavaType<T> javaType, WrapperOptions wrapperOptions) {
if ( javaType.getJavaType() == String.class ) {
if ( javaType.getJavaType() == String.class || javaType.getJavaType() == Object.class ) {
return (T) charSequence.toString();
}
try {
@ -194,7 +194,7 @@ public final class JaxbXmlFormatMapper implements FormatMapper {
@Override
public <T> String toString(T value, JavaType<T> javaType, WrapperOptions wrapperOptions) {
if ( javaType.getJavaType() == String.class ) {
if ( javaType.getJavaType() == String.class || javaType.getJavaType() == Object.class ) {
return (String) value;
}
try {

View File

@ -16,7 +16,7 @@ earlier versions, see any other pertinent migration guides as well.
=== UUID mapping changes on MariaDB
On MariaDB, the type code `SqlType.UUID` now by default refers to the DDL type `uuid`, whereas before it was using `binary(16)`.
On MariaDB, the type code `SqlTypes.UUID` now by default refers to the DDL type `uuid`, whereas before it was using `binary(16)`.
Due to this change, schema validation errors could occur on existing databases.
The migration to `uuid` requires a migration expression like `cast(old as uuid)`.
@ -25,13 +25,24 @@ To retain backwards compatibility, configure the setting `hibernate.type.preferr
=== UUID mapping changes on SQL Server
On SQL Server, the type code `SqlType.UUID` now by default refers to the DDL type `uniqueidentifier`, whereas before it was using `binary(16)`.
On SQL Server, the type code `SqlTypes.UUID` now by default refers to the DDL type `uniqueidentifier`, whereas before it was using `binary(16)`.
Due to this change, schema validation errors could occur on existing databases.
The migration to `uuid` requires a migration expression like `cast(old as uuid)`.
To retain backwards compatibility, configure the setting `hibernate.type.preferred_uuid_jdbc_type` to `BINARY`.
=== JSON mapping changes on Oracle
On Oracle 12.1+, the type code `SqlTypes.JSON` now by default refers to the DDL type `blob` and on 21+ to `json`, whereas before it was using `clob`.
Due to this change, schema validation errors could occur on existing databases.
The migration to `blob` and `json` requires a migration expression like `cast(old as blob)` and `cast(old as json)` respectively.
To get the old behavior, annotate the column with `@Column(definition = "clob")`.
This change was done because `blob` and `json` are way more efficient and because we don't expect wide usage of `SqlTypes.JSON` yet.
=== Column type inference for `number(n,0)` in native SQL queries on Oracle
Previously, since Hibernate 6.0, columns of type `number` with scale 0 on Oracle were interpreted as `boolean`, `tinyint`, `smallint`, `int`, or `bigint`,