Skip to content

Commit

Permalink
fix: Hibernate would insert JSON as STRING (#944)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
olavloite committed Mar 6, 2024
1 parent 75fa7e8 commit 1d8b855
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 8 deletions.
7 changes: 7 additions & 0 deletions google-cloud-spanner-hibernate-dialect/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@
<artifactId>hibernate-ant</artifactId>
<scope>test</scope>
</dependency>
<!-- Jackson is needed for testing JSON. -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-spanner</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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 {
Expand All @@ -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 <X> ValueBinder<X> getBinder(JavaType<X> javaType) {
return new BasicBinder<X>(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
Expand Down Expand Up @@ -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<Table> getTableExporter() {
return this.spannerTableExporter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 1d8b855

Please sign in to comment.