Как сопоставить столбец JSON карты с объектом Java с помощью JPA

У нас есть большая таблица с большим количеством столбцов. После того, как мы перешли в MySQL Cluster, таблица не может быть создана из-за:

ОШИБКА 1118 (42000): Размер строки слишком большой. Максимальный размер строки для используемого типа таблицы, не считая BLOB, составляет 14000. Это включает в себя накладные расходы на хранение, проверьте руководство. Вы должны изменить некоторые столбцы на TEXT или BLOB

В качестве примера:

@Entity @Table (name = "appconfigs", schema = "myproject")
public class AppConfig implements Serializable
{
    @Id @Column (name = "id", nullable = false)
    @GeneratedValue (strategy = GenerationType.IDENTITY)
    private int id;

    @OneToOne @JoinColumn (name = "app_id")
    private App app;

    @Column(name = "param_a")
    private ParamA parama;

    @Column(name = "param_b")
    private ParamB paramb;
}

Это таблица для хранения параметров конфигурации. Я думал, что мы можем объединить несколько столбцов в один и сохранить его как объект JSON и преобразовать его в некоторый объект Java.

Например:

@Entity @Table (name = "appconfigs", schema = "myproject")
public class AppConfig implements Serializable
{
    @Id @Column (name = "id", nullable = false)
    @GeneratedValue (strategy = GenerationType.IDENTITY)
    private int id;

    @OneToOne @JoinColumn (name = "app_id")
    private App app;

    @Column(name = "params")
    //How to specify that this should be mapped to JSON object?
    private Params params;
}

Где мы определили:

public class Params implements Serializable
{
    private ParamA parama;
    private ParamB paramb;
}

Используя это, мы можем объединить все столбцы в один и создать нашу таблицу. Или мы можем разбить всю таблицу на несколько таблиц. Лично я предпочитаю первое решение.

В любом случае мой вопрос заключается в том, как сопоставить столбец Params, который является текстом, и содержит строку JSON объекта Java?

Ответ 1

Вы можете использовать конвертер JPA, чтобы сопоставить вашу сущность с базой данных. Просто добавьте аннотацию, похожую на эту, в поле params:

@Convert(converter = JpaConverterJson.class)

и затем создайте класс аналогичным образом (это преобразует универсальный объект, вы можете захотеть специализировать его):

@Converter(autoApply = true)
public class JpaConverterJson implements AttributeConverter<Object, String> {

  private final static ObjectMapper objectMapper = new ObjectMapper();

  @Override
  public String convertToDatabaseColumn(Object meta) {
    try {
      return objectMapper.writeValueAsString(meta);
    } catch (JsonProcessingException ex) {
      return null;
      // or throw an error
    }
  }

  @Override
  public Object convertToEntityAttribute(String dbData) {
    try {
      return objectMapper.readValue(dbData, Object.class);
    } catch (IOException ex) {
      // logger.error("Unexpected IOEx decoding json from database: " + dbData);
      return null;
    }
  }

}

Вот и все: вы можете использовать этот класс для сериализации любого объекта в json в таблице.

Ответ 2

Как я объяснил в этой статье, JPA AttributeConverter слишком ограничен для отображения типов объектов JSON, особенно если вы хотите сохранить их как двоичный файл JSON.

Вам не нужно создавать все эти типы вручную, вы можете просто получить их через Maven Central, используя следующую зависимость:

<dependency>
    <groupId>com.vladmihalcea</groupId>
    <artifactId>hibernate-types-52</artifactId>
    <version>${hibernate-types.version}</version> 
</dependency> 

Для получения дополнительной информации, проверьте проект с открытым исходным кодом hibernate-types.

Теперь, чтобы объяснить, как все это работает.

Я написал статью о том, как вы можете отобразить объекты JSON на PostgreSQL и MySQL.

Для PostgreSQL вам нужно отправить объект JSON в двоичном виде:

public class JsonBinaryType
    extends AbstractSingleColumnStandardBasicType<Object> 
    implements DynamicParameterizedType {

    public JsonBinaryType() {
        super( 
            JsonBinarySqlTypeDescriptor.INSTANCE, 
            new JsonTypeDescriptor()
        );
    }

    public String getName() {
        return "jsonb";
    }

    @Override
    public void setParameterValues(Properties parameters) {
        ((JsonTypeDescriptor) getJavaTypeDescriptor())
            .setParameterValues(parameters);
    }

}

JsonBinarySqlTypeDescriptor выглядит следующим образом:

public class JsonBinarySqlTypeDescriptor
    extends AbstractJsonSqlTypeDescriptor {

    public static final JsonBinarySqlTypeDescriptor INSTANCE = 
        new JsonBinarySqlTypeDescriptor();

    @Override
    public <X> ValueBinder<X> getBinder(
        final JavaTypeDescriptor<X> javaTypeDescriptor) {
        return new BasicBinder<X>(javaTypeDescriptor, this) {
            @Override
            protected void doBind(
                PreparedStatement st, 
                X value, 
                int index, 
                WrapperOptions options) throws SQLException {
                st.setObject(index, 
                    javaTypeDescriptor.unwrap(
                        value, JsonNode.class, options), getSqlType()
                );
            }

            @Override
            protected void doBind(
                CallableStatement st, 
                X value, 
                String name, 
                WrapperOptions options)
                    throws SQLException {
                st.setObject(name, 
                    javaTypeDescriptor.unwrap(
                        value, JsonNode.class, options), getSqlType()
                );
            }
        };
    }
}

и JsonTypeDescriptor как это:

public class JsonTypeDescriptor
        extends AbstractTypeDescriptor<Object> 
        implements DynamicParameterizedType {

    private Class<?> jsonObjectClass;

    @Override
    public void setParameterValues(Properties parameters) {
        jsonObjectClass = ( (ParameterType) parameters.get( PARAMETER_TYPE ) )
            .getReturnedClass();

    }

    public JsonTypeDescriptor() {
        super( Object.class, new MutableMutabilityPlan<Object>() {
            @Override
            protected Object deepCopyNotNull(Object value) {
                return JacksonUtil.clone(value);
            }
        });
    }

    @Override
    public boolean areEqual(Object one, Object another) {
        if ( one == another ) {
            return true;
        }
        if ( one == null || another == null ) {
            return false;
        }
        return JacksonUtil.toJsonNode(JacksonUtil.toString(one)).equals(
                JacksonUtil.toJsonNode(JacksonUtil.toString(another)));
    }

    @Override
    public String toString(Object value) {
        return JacksonUtil.toString(value);
    }

    @Override
    public Object fromString(String string) {
        return JacksonUtil.fromString(string, jsonObjectClass);
    }

    @SuppressWarnings({ "unchecked" })
    @Override
    public <X> X unwrap(Object value, Class<X> type, WrapperOptions options) {
        if ( value == null ) {
            return null;
        }
        if ( String.class.isAssignableFrom( type ) ) {
            return (X) toString(value);
        }
        if ( Object.class.isAssignableFrom( type ) ) {
            return (X) JacksonUtil.toJsonNode(toString(value));
        }
        throw unknownUnwrap( type );
    }

    @Override
    public <X> Object wrap(X value, WrapperOptions options) {
        if ( value == null ) {
            return null;
        }
        return fromString(value.toString());
    }

}

Теперь вам нужно объявить новый тип либо на уровне класса, либо в дескрипторе уровня пакета package-info.java:

@TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)

И отображение сущности будет выглядеть так:

@Type(type = "jsonb")
@Column(columnDefinition = "json")
private Location location;

Если вы используете Hibernate 5 или более позднюю версию, тип JSON автоматически регистрируется Postgre92Dialect.

В противном случае вам необходимо зарегистрировать его самостоятельно:

public class PostgreSQLDialect extends PostgreSQL91Dialect {

    public PostgreSQLDialect() {
        super();
        this.registerColumnType( Types.JAVA_OBJECT, "json" );
    }
}

Ответ 3

У меня была аналогичная проблема, и я решил ее с помощью аннотации @Externalizer и Jackson для сериализации/десериализации данных (@Externalizer - это аннотация, специфичная для OpenJPA, поэтому вам нужно проверить аналогичную возможность реализации JPA).

@Persistent
@Column(name = "params")
@Externalizer("toJSON")
private Params params;

Реализация класса Params:

public class Params {
    private static final ObjectMapper mapper = new ObjectMapper();

    private Map<String, Object> map;

    public Params () {
        this.map = new HashMap<String, Object>();
    }

    public Params (Params another) {
        this.map = new HashMap<String, Object>();
        this.map.putAll(anotherHolder.map);
    }

    public Params(String string) {
        try {
            TypeReference<Map<String, Object>> typeRef = new TypeReference<Map<String, Object>>() {
            };
            if (string == null) {
                this.map = new HashMap<String, Object>();
            } else {
                this.map = mapper.readValue(string, typeRef);
            }
        } catch (IOException e) {
            throw new PersistenceException(e);
        }
    }

    public String toJSON() throws PersistenceException {
        try {
            return mapper.writeValueAsString(this.map);
        } catch (IOException e) {
            throw new PersistenceException(e);
        }
    }

    public boolean containsKey(String key) {
        return this.map.containsKey(key);
    }

    // Hash map methods
    public Object get(String key) {
        return this.map.get(key);
    }

    public Object put(String key, Object value) {
        return this.map.put(key, value);
    }

    public void remove(String key) {
        this.map.remove(key);
    }

    public Object size() {
        return map.size();
    }
}

НТН

Ответ 4

Это просто

@Column(name = "json_input", columnDefinition = "json")
private String field;

и в базе данных mysql ваш столбец "json_input" json type

enter image description here