HHH-16320 Handle json columns with native ddl type on H2

This commit is contained in:
Marco Belladelli 2023-03-14 12:38:47 +01:00 committed by Christian Beikov
parent 39f4fdda5e
commit c3fa3ae777
4 changed files with 229 additions and 0 deletions

View File

@ -75,6 +75,7 @@ import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorH2
import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorLegacyImpl;
import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorNoOpImpl;
import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor;
import org.hibernate.type.descriptor.jdbc.H2FormatJsonJdbcType;
import org.hibernate.type.descriptor.jdbc.InstantJdbcType;
import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.hibernate.type.descriptor.jdbc.UUIDJdbcType;
@ -94,6 +95,7 @@ import static org.hibernate.type.SqlTypes.DOUBLE;
import static org.hibernate.type.SqlTypes.FLOAT;
import static org.hibernate.type.SqlTypes.GEOMETRY;
import static org.hibernate.type.SqlTypes.INTERVAL_SECOND;
import static org.hibernate.type.SqlTypes.JSON;
import static org.hibernate.type.SqlTypes.LONG32NVARCHAR;
import static org.hibernate.type.SqlTypes.LONG32VARBINARY;
import static org.hibernate.type.SqlTypes.LONG32VARCHAR;
@ -248,6 +250,9 @@ public class H2LegacyDialect extends Dialect {
if ( getVersion().isSameOrAfter( 1, 4, 198 ) ) {
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( INTERVAL_SECOND, "interval second($p,$s)", this ) );
}
if ( getVersion().isSameOrAfter( 1, 4, 200 ) ) {
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "json", this ) );
}
}
}
@ -265,6 +270,9 @@ public class H2LegacyDialect extends Dialect {
if ( getVersion().isSameOrAfter( 1, 4, 198 ) ) {
jdbcTypeRegistry.addDescriptorIfAbsent( H2DurationIntervalSecondJdbcType.INSTANCE );
}
if ( getVersion().isSameOrAfter( 1, 4, 200 ) ) {
jdbcTypeRegistry.addDescriptorIfAbsent( H2FormatJsonJdbcType.INSTANCE );
}
}
@Override
@ -392,6 +400,9 @@ public class H2LegacyDialect extends Dialect {
if ( "GEOMETRY".equals( columnTypeName ) ) {
return jdbcTypeRegistry.getDescriptor( GEOMETRY );
}
else if ( "JSON".equals( columnTypeName ) ) {
return jdbcTypeRegistry.getDescriptor( JSON );
}
break;
}
return super.resolveSqlTypeDescriptor( columnTypeName, jdbcTypeCode, precision, scale, jdbcTypeRegistry );

View File

@ -68,6 +68,7 @@ import org.hibernate.sql.model.jdbc.OptionalTableUpdateOperation;
import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorH2DatabaseImpl;
import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorLegacyImpl;
import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor;
import org.hibernate.type.descriptor.jdbc.H2FormatJsonJdbcType;
import org.hibernate.type.descriptor.jdbc.InstantJdbcType;
import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.hibernate.type.descriptor.jdbc.UUIDJdbcType;
@ -88,6 +89,7 @@ import static org.hibernate.type.SqlTypes.DOUBLE;
import static org.hibernate.type.SqlTypes.FLOAT;
import static org.hibernate.type.SqlTypes.GEOMETRY;
import static org.hibernate.type.SqlTypes.INTERVAL_SECOND;
import static org.hibernate.type.SqlTypes.JSON;
import static org.hibernate.type.SqlTypes.LONG32NVARCHAR;
import static org.hibernate.type.SqlTypes.LONG32VARBINARY;
import static org.hibernate.type.SqlTypes.LONG32VARCHAR;
@ -229,6 +231,9 @@ public class H2Dialect extends Dialect {
if ( getVersion().isSameOrAfter( 1, 4, 198 ) ) {
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( INTERVAL_SECOND, "interval second($p,$s)", this ) );
}
if ( getVersion().isSameOrAfter( 1, 4, 200 ) ) {
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "json", this ) );
}
}
@Override
@ -243,6 +248,9 @@ public class H2Dialect extends Dialect {
if ( getVersion().isSameOrAfter( 1, 4, 198 ) ) {
jdbcTypeRegistry.addDescriptorIfAbsent( H2DurationIntervalSecondJdbcType.INSTANCE );
}
if ( getVersion().isSameOrAfter( 1, 4, 200 ) ) {
jdbcTypeRegistry.addDescriptorIfAbsent( H2FormatJsonJdbcType.INSTANCE );
}
}
@Override
@ -370,6 +378,9 @@ public class H2Dialect extends Dialect {
if ( "GEOMETRY".equals( columnTypeName ) ) {
return jdbcTypeRegistry.getDescriptor( GEOMETRY );
}
else if ( "JSON".equals( columnTypeName ) ) {
return jdbcTypeRegistry.getDescriptor( JSON );
}
break;
}
return super.resolveSqlTypeDescriptor( columnTypeName, jdbcTypeCode, precision, scale, jdbcTypeRegistry );

View File

@ -0,0 +1,48 @@
/*
* 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 org.hibernate.dialect.Dialect;
import org.hibernate.metamodel.mapping.EmbeddableMappingType;
import org.hibernate.metamodel.spi.RuntimeModelCreationContext;
import org.hibernate.sql.ast.spi.SqlAppender;
/**
* Specialized type mapping for {@code JSON} that utilizes the custom
* '{@code ? format json}' write expression for H2.
*
* @author Marco Belladelli
*/
public class H2FormatJsonJdbcType extends JsonJdbcType {
/**
* Singleton access
*/
public static final H2FormatJsonJdbcType INSTANCE = new H2FormatJsonJdbcType( null );
protected H2FormatJsonJdbcType(EmbeddableMappingType embeddableMappingType) {
super( embeddableMappingType );
}
@Override
public String toString() {
return "FormatJsonJdbcType";
}
@Override
public AggregateJdbcType resolveAggregateJdbcType(
EmbeddableMappingType mappingType,
String sqlType,
RuntimeModelCreationContext creationContext) {
return new H2FormatJsonJdbcType( mappingType );
}
@Override
public void appendWriteExpression(String writeExpression, SqlAppender appender, Dialect dialect) {
appender.append( writeExpression );
appender.append( " format json" );
}
}

View File

@ -0,0 +1,159 @@
/*
* 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.orm.test.type;
import java.util.List;
import java.util.UUID;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.dialect.H2Dialect;
import org.hibernate.type.SqlTypes;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.Jira;
import org.hibernate.testing.orm.junit.RequiresDialect;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import static org.assertj.core.api.Assertions.assertThat;
@SessionFactory
@DomainModel( annotatedClasses = { H2JsonListTest.Path.class, H2JsonListTest.PathClob.class } )
@RequiresDialect( H2Dialect.class )
@Jira( "https://hibernate.atlassian.net/browse/HHH-16320" )
public class H2JsonListTest {
@BeforeAll
public void setUp(SessionFactoryScope scope) {
scope.inTransaction( session -> {
session.persist( new Path( List.of( UUID.randomUUID(), UUID.randomUUID() ) ) );
session.persist( new PathClob( List.of( UUID.randomUUID(), UUID.randomUUID() ) ) );
} );
}
@AfterAll
public void tearDown(SessionFactoryScope scope) {
scope.inTransaction( session -> {
session.createMutationQuery( "delete from Path" ).executeUpdate();
session.createMutationQuery( "delete from PathClob" ).executeUpdate();
} );
}
@Test
public void testRetrievalJson(SessionFactoryScope scope) {
scope.inTransaction( session -> {
final Path path = session.find( Path.class, 1L );
assertThat( path ).isNotNull();
assertThat( path.getRelativePaths() ).hasSize( 2 );
} );
}
@Test
public void testNativeSyntaxJson(SessionFactoryScope scope) {
scope.inTransaction( session -> {
session.createNativeMutationQuery( "insert into paths (relativePaths,id) values (?1 FORMAT JSON, ?2)" )
.setParameter(
1,
"[\"2b099c92-95ff-42e0-9f8c-f08c2518792d\", \"8d2164db-86b4-460a-91d0-bf821a8ca3d7\"]"
)
.setParameter( 2, 99L )
.executeUpdate();
} );
scope.inTransaction( session -> {
final Path path = session.createNativeQuery(
"select * from paths_clob where id = 99",
Path.class
).getSingleResult();
assertThat( path ).isNotNull();
assertThat( path.getRelativePaths() ).hasSize( 2 );
} );
}
@Test
public void testRetrievalClob(SessionFactoryScope scope) {
scope.inTransaction( session -> {
final PathClob path = session.find( PathClob.class, 1L );
assertThat( path ).isNotNull();
assertThat( path.getRelativePaths() ).hasSize( 2 );
} );
}
@Test
public void testNativeSyntaxClob(SessionFactoryScope scope) {
scope.inTransaction( session -> {
session.createNativeMutationQuery( "insert into paths_clob (relativePaths,id) values (?1 FORMAT JSON, ?2)" )
.setParameter(
1,
"[\"2b099c92-95ff-42e0-9f8c-f08c2518792d\", \"8d2164db-86b4-460a-91d0-bf821a8ca3d7\"]"
)
.setParameter( 2, 99L )
.executeUpdate();
} );
scope.inTransaction( session -> {
final PathClob path = session.createNativeQuery(
"select * from paths_clob where id = 99",
PathClob.class
).getSingleResult();
assertThat( path ).isNotNull();
assertThat( path.getRelativePaths() ).hasSize( 2 );
} );
}
@Entity( name = "Path" )
@Table( name = "paths" )
public static class Path {
@Id
@GeneratedValue
public Long id;
@JdbcTypeCode( SqlTypes.JSON )
public List<UUID> relativePaths;
public Path() {
}
public Path(List<UUID> relativePaths) {
this.relativePaths = relativePaths;
}
public List<UUID> getRelativePaths() {
return relativePaths;
}
}
@Entity( name = "PathClob" )
@Table( name = "paths_clob" )
public static class PathClob {
@Id
@GeneratedValue
public Long id;
@JdbcTypeCode( SqlTypes.JSON )
@Column( columnDefinition = "clob" )
public List<UUID> relativePaths;
public PathClob() {
}
public PathClob(List<UUID> relativePaths) {
this.relativePaths = relativePaths;
}
public List<UUID> getRelativePaths() {
return relativePaths;
}
}
}