hibernate / hibernate-reactive

A reactive API for Hibernate ORM, supporting non-blocking database drivers and a reactive style of interaction with the database.
https://hibernate.org/reactive
Apache License 2.0
432 stars 89 forks source link

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

Open dan1els opened 1 year ago

dan1els commented 1 year ago

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

DavideD commented 1 year ago

Which db are you using?

DavideD commented 1 year ago

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 commented 1 year ago

Please, let me know if this solve your problem

dan1els commented 1 year ago

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

DavideD commented 1 year ago

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

dan1els commented 1 year ago

I'm looking for it.

dan1els commented 1 year ago

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