From 1d8b8550acfed940f8909d5685abe395bec0a38a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Wed, 6 Mar 2024 11:30:06 +0100 Subject: [PATCH] fix: Hibernate would insert JSON as STRING (#944) Hibernate tried by default to insert columns that had been annotated with `@JdbcTypeCode(SqlTypes.JSON)` as a STRING instead of JSON. This change adds a default type registration for JSON that does include the correct type code with the value. --- .../pom.xml | 7 +++ .../spanner/hibernate/SpannerDialect.java | 48 +++++++++++++++++++ .../spanner/hibernate/it/SampleModelIT.java | 15 ++++-- .../spanner/hibernate/it/model/Venue.java | 8 ++-- 4 files changed, 70 insertions(+), 8 deletions(-) diff --git a/google-cloud-spanner-hibernate-dialect/pom.xml b/google-cloud-spanner-hibernate-dialect/pom.xml index 5f5850df..ceeb3ff6 100644 --- a/google-cloud-spanner-hibernate-dialect/pom.xml +++ b/google-cloud-spanner-hibernate-dialect/pom.xml @@ -54,6 +54,13 @@ hibernate-ant test + + + com.fasterxml.jackson.core + jackson-databind + 2.15.2 + test + com.google.cloud google-cloud-spanner diff --git a/google-cloud-spanner-hibernate-dialect/src/main/java/com/google/cloud/spanner/hibernate/SpannerDialect.java b/google-cloud-spanner-hibernate-dialect/src/main/java/com/google/cloud/spanner/hibernate/SpannerDialect.java index 586a3848..cea79c1a 100644 --- a/google-cloud-spanner-hibernate-dialect/src/main/java/com/google/cloud/spanner/hibernate/SpannerDialect.java +++ b/google-cloud-spanner-hibernate-dialect/src/main/java/com/google/cloud/spanner/hibernate/SpannerDialect.java @@ -22,9 +22,13 @@ import static org.hibernate.type.SqlTypes.NUMERIC; import com.google.cloud.spanner.hibernate.schema.SpannerForeignKeyExporter; +import com.google.cloud.spanner.jdbc.JsonType; +import java.sql.CallableStatement; import java.sql.Connection; +import java.sql.PreparedStatement; import java.sql.SQLException; import org.hibernate.HibernateException; +import org.hibernate.boot.model.TypeContributions; import org.hibernate.boot.model.relational.Sequence; import org.hibernate.dialect.unique.UniqueDelegate; import org.hibernate.engine.spi.SessionFactoryImplementor; @@ -37,6 +41,7 @@ import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.mutation.spi.SqmMultiTableInsertStrategy; import org.hibernate.query.sqm.tree.insert.SqmInsertStatement; +import org.hibernate.service.ServiceRegistry; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.SqlAstTranslatorFactory; import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory; @@ -50,6 +55,13 @@ import org.hibernate.tool.schema.internal.StandardSequenceExporter; import org.hibernate.tool.schema.internal.StandardUniqueKeyExporter; import org.hibernate.tool.schema.spi.Exporter; +import org.hibernate.type.SqlTypes; +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.type.descriptor.jdbc.JsonAsStringJdbcType; +import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; /** Hibernate 6.x dialect for Cloud Spanner. */ public class SpannerDialect extends org.hibernate.dialect.SpannerDialect { @@ -65,6 +77,33 @@ public int executeInsert( throw new HibernateException("Multi-table inserts are not supported for Cloud Spanner"); } } + + private static class SpannerJsonJdbcType extends JsonAsStringJdbcType { + private SpannerJsonJdbcType() { + super(SqlTypes.LONG32VARCHAR, null); + } + + @Override + public ValueBinder getBinder(JavaType javaType) { + return new BasicBinder(javaType, this) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) + throws SQLException { + final String json = ((SpannerJsonJdbcType) getJdbcType()).toString( + value, getJavaType(), options); + st.setObject(index, json, JsonType.VENDOR_TYPE_NUMBER); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final String json = ((SpannerJsonJdbcType) getJdbcType()).toString( + value, getJavaType(), options); + st.setObject(name, json, JsonType.VENDOR_TYPE_NUMBER); + } + }; + } + } /** * Property name that can be used to disable sequence support in the Cloud Spanner dialect. You @@ -122,6 +161,15 @@ protected String columnType(int sqlTypeCode) { return super.columnType(sqlTypeCode); } + @Override + protected void registerColumnTypes( + TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + super.registerColumnTypes(typeContributions, serviceRegistry); + JdbcTypeRegistry jdbcTypeRegistry = + typeContributions.getTypeConfiguration().getJdbcTypeRegistry(); + jdbcTypeRegistry.addDescriptorIfAbsent(new SpannerJsonJdbcType()); + } + @Override public Exporter getTableExporter() { return this.spannerTableExporter; diff --git a/google-cloud-spanner-hibernate-dialect/src/test/java/com/google/cloud/spanner/hibernate/it/SampleModelIT.java b/google-cloud-spanner-hibernate-dialect/src/test/java/com/google/cloud/spanner/hibernate/it/SampleModelIT.java index 3bb6f1fb..acdd651d 100644 --- a/google-cloud-spanner-hibernate-dialect/src/test/java/com/google/cloud/spanner/hibernate/it/SampleModelIT.java +++ b/google-cloud-spanner-hibernate-dialect/src/test/java/com/google/cloud/spanner/hibernate/it/SampleModelIT.java @@ -242,15 +242,20 @@ public void testSaveTrack() { @Test public void testSaveVenue() { try (Session session = sessionFactory.openSession()) { - Transaction transaction = session.beginTransaction(); - // TODO: Set VenueDescription fields and verify these in Hibernate 6. - // Hibernate 5 does not support JSON without additional plugins. - Venue venue = new Venue("Venue 1", new VenueDescription()); - session.save(venue); + final Transaction transaction = session.beginTransaction(); + VenueDescription description = new VenueDescription(); + description.setCapacity(100); + description.setLocation("some-location"); + description.setType("Arena"); + Venue venue = new Venue("Venue 1", description); + session.persist(venue); transaction.commit(); session.refresh(venue); assertEquals("Venue 1", venue.getName()); + assertEquals(100, venue.getDescription().getCapacity()); + assertEquals("some-location", venue.getDescription().getLocation()); + assertEquals("Arena", venue.getDescription().getType()); } } diff --git a/google-cloud-spanner-hibernate-dialect/src/test/java/com/google/cloud/spanner/hibernate/it/model/Venue.java b/google-cloud-spanner-hibernate-dialect/src/test/java/com/google/cloud/spanner/hibernate/it/model/Venue.java index 8a0bd597..49e92d83 100644 --- a/google-cloud-spanner-hibernate-dialect/src/test/java/com/google/cloud/spanner/hibernate/it/model/Venue.java +++ b/google-cloud-spanner-hibernate-dialect/src/test/java/com/google/cloud/spanner/hibernate/it/model/Venue.java @@ -20,17 +20,19 @@ package com.google.cloud.spanner.hibernate.it.model; import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.OneToMany; -import jakarta.persistence.Transient; import java.io.Serializable; import java.util.List; import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.annotations.Parameter; import org.hibernate.id.enhanced.SequenceStyleGenerator; +import org.hibernate.type.SqlTypes; /** * Venue entity. @@ -94,8 +96,8 @@ public void setLocation(String location) { * This field maps to a JSON column in the database. The value is automatically * serialized/deserialized to a {@link VenueDescription} instance. */ - // TODO: Make this non-transient when we support Hibernate 6. - @Transient + @Column(columnDefinition = "json") + @JdbcTypeCode(SqlTypes.JSON) private VenueDescription description; @OneToMany(mappedBy = "venue", cascade = CascadeType.ALL)