hibernate-reactive icon indicating copy to clipboard operation
hibernate-reactive copied to clipboard

Can't use hibernate6 json embeddable together with vertx-reactive -- expected String but got JsonObject

Open dan1els opened this issue 2 years ago • 7 comments

Vertx reactive driver return jsonb fields as a JsonObject, but hibernate only expects Strings, and seems theres no built-in user type to support vertx JsonObject There is JsonType in here, but to use new features the type should implement aggregate type which it doesn't

dan1els avatar Apr 20 '23 08:04 dan1els

Which db are you using?

DavideD avatar Apr 20 '23 08:04 DavideD

Hibernate Reactive recognizes the JsonObject as a type or you can use a converter.

Or, you can use the user type Json.

Sorry, we need to update the documentation with some examples

DavideD avatar Apr 20 '23 08:04 DavideD

Please, let me know if this solve your problem

DavideD avatar Apr 20 '23 08:04 DavideD

PostgreSQL 11 I can not use that unfortunately, just becuase I'm using new hibernate 6.2 feature when you can embed an object as json and make a query using their fields Here's the link https://docs.jboss.org/hibernate/orm/6.2/userguide/html_single/Hibernate_User_Guide.html#embeddable-mapping-aggregate

So my entities look like:

@Entity
class Foo {
  @JdbcTypeCode(SqlTypes.JSON)
  private EmbeddedObj embedded;

  @Embeddable
  public static class EmbeddedObj {
    string bar;
  }
}

And I also have a query like select f from Foo f where f.bar = 'baz'

The only I found is that SqlTypes.JSON maps to org.hibernate.dialect.PostgreSQLCastingJsonJdbcType and this class expects a string from resultSet (see org.hibernate.type.descriptor.jdbc.JsonJdbcType#getExtractor ) but vert.x driver puts JsonObject into RS.

As a work around I implemented custom JsonObject jsbc type which implements org.hibernate.type.descriptor.jdbc.AggregateJdbcType and by now it's ok to me, but I don't want to maintain this type and I guess that should be done somewhere in the box as it's done for basic type like here: https://github.com/hibernate/hibernate-reactive/pull/907/commits/1d1c56f3ce50a17bbc00da75b665157e93ba12ab but for embeddable types as well

dan1els avatar Apr 20 '23 13:04 dan1els

Thanks, it's an ORM new feature that we haven't implemented yet. But we will look into it

DavideD avatar Apr 20 '23 13:04 DavideD

I'm looking for it.

dan1els avatar Apr 20 '23 13:04 dan1els

By the way I could share what I have just implemented as I workaround, I'm sure that there are plenty of issues in the code but for now at least it work for me

import io.vertx.core.json.JsonObject;
import org.hibernate.dialect.PostgreSQLCastingJsonJdbcType;
import org.hibernate.metamodel.mapping.EmbeddableMappingType;
import org.hibernate.metamodel.spi.RuntimeModelCreationContext;
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.AggregateJdbcType;
import org.hibernate.type.descriptor.jdbc.BasicBinder;
import org.hibernate.type.descriptor.jdbc.BasicExtractor;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;

public class JsonObjectJdbcType extends PostgreSQLCastingJsonJdbcType {

    private final EmbeddableMappingType embeddableMappingType;

    public JsonObjectJdbcType() {
        this(true, null);
    }

    public JsonObjectJdbcType(boolean jsonb, EmbeddableMappingType embeddableMappingType) {
        super(true, embeddableMappingType);
        this.embeddableMappingType = embeddableMappingType;
    }

    @SuppressWarnings("unchecked")
    protected <X> X fromJsonObject(JsonObject obj, JavaType<X> javaType, WrapperOptions options) {
        if (obj == null) {
            return null;
        }
        if (embeddableMappingType != null) {
            //org.hibernate.sql.results.graph.embeddable.internal.NestedRowProcessingState#getJdbcValue wants to get array of field values for some reason
            return (X) Arrays.stream(embeddableMappingType.getJavaType().getJavaTypeClass().getDeclaredFields())
                    .filter(field -> !Modifier.isTransient(field.getModifiers()))
                    .map(Field::getName)
                    .map(obj::getValue)
                    .toArray(Object[]::new);
        }
        return obj.mapTo(javaType.getJavaTypeClass());
    }

    protected <X> JsonObject toJsonObject(X value, JavaType<X> javaType, WrapperOptions options) {
        if (value == null) {
            return null;
        }
        return JsonObject.mapFrom(value);
    }

    @Override
    public AggregateJdbcType resolveAggregateJdbcType(
                                                      EmbeddableMappingType mappingType,
                                                      String sqlType,
                                                      RuntimeModelCreationContext creationContext) {
        return new JsonObjectJdbcType(true, mappingType);
    }

    @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 JsonObject json = ((JsonObjectJdbcType) getJdbcType()).toJsonObject(value, getJavaType(), options);
                st.setObject(index, json);
            }

            @Override
            protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) throws SQLException {
                final JsonObject json = ((JsonObjectJdbcType) getJdbcType()).toJsonObject(value, getJavaType(), options);
                st.setObject(name, json);
            }
        };
    }

    @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 fromJsonObject(rs.getObject(paramIndex, JsonObject.class), getJavaType(), options);
            }

            @Override
            protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException {
                return fromJsonObject(statement.getObject(index, JsonObject.class), getJavaType(), options);
            }

            @Override
            protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException {
                return fromJsonObject(statement.getObject(name, JsonObject.class), getJavaType(), options);
            }
        };
    }
}

And I just register the type for package

@JdbcTypeRegistration(registrationCode = SqlTypes.JSON, value = JsonObjectJdbcType.class)

package com.example.dan1els.entity;

import org.hibernate.annotations.JdbcTypeRegistration;
import org.hibernate.type.SqlTypes;
import com.example.dan1els.misc.JsonObjectJdbcType;

Maybe it would help

dan1els avatar Apr 20 '23 13:04 dan1els