如何使用 JPA 和 Hibernate 将 MySQL JSON 列映射到 Java 实体属性

Posted

技术标签:

【中文标题】如何使用 JPA 和 Hibernate 将 MySQL JSON 列映射到 Java 实体属性【英文标题】:How to map a MySQL JSON column to a Java entity property using JPA and Hibernate 【发布时间】:2017-11-02 15:21:00 【问题描述】:

我有一个声明为 JSON 类型的 mysql 列,但在将其映射到 JPA/Hibernate 时遇到问题。我在后端使用 Spring Boot。

这是我的一小部分代码:

@Entity
@Table(name = "some_table_name")
public class MyCustomEntity implements Serializable 

private static final long serialVersionUID = 1L;

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

@Column(name = "json_value")
private JSONArray jsonValue;

程序返回错误并告诉我无法映射列。

在mysql表中列定义为:

json_value JSON NOT NULL;

【问题讨论】:

尝试使用 columnDefinition = "json" : @Column(name = "json_value", columnDefinition = "json") 请分享您收到的例外情况。 问题可以是 JSONArray 类型,因为如果我用 String 代替,一切正常。 异常:无法确定类型:org.json.JSONArray,在表:some_table_name。 【参考方案1】:

你不必手动创建所有这些类型,你可以简单地得到 他们通过 Maven Central 使用以下依赖项:

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

欲了解更多信息,请查看Hibernate Types open-source project。

现在,解释一下它是如何工作的。

假设您有以下实体:

@Entity(name = "Book")
@Table(name = "book")
@TypeDef(
    name = "json", 
    typeClass = JsonType.class
)
public class Book 
 
    @Id
    @GeneratedValue
    private Long id;
 
    @NaturalId
    private String isbn;
 
    @Type(type = "json")
    @Column(columnDefinition = "json")
    private String properties;
 
    //Getters and setters omitted for brevity

注意上面代码sn-p中的两点:

@TypeDef 用于定义新的自定义 Hibernate 类型,json,由 JsonType 处理 properties 属性具有json 列类型,它被映射为String

就是这样!

现在,如果你保存一个实体:

Book book = new Book();
book.setIsbn("978-9730228236");
book.setProperties(
    "" +
    "   \"title\": \"High-Performance Java Persistence\"," +
    "   \"author\": \"Vlad Mihalcea\"," +
    "   \"publisher\": \"Amazon\"," +
    "   \"price\": 44.99" +
    ""
);
 
entityManager.persist(book);

Hibernate 将生成以下 SQL 语句:

INSERT INTO
    book 
(
    isbn, 
    properties, 
    id
) 
VALUES
(
    '978-9730228236', 
    '"title":"High-Performance Java Persistence","author":"Vlad Mihalcea","publisher":"Amazon","price":44.99',  
    1
)

你也可以加载回来修改它:

Book book = entityManager
    .unwrap(Session.class)
    .bySimpleNaturalId(Book.class)
    .load("978-9730228236");
     
book.setProperties(
    "" +
    "   \"title\": \"High-Performance Java Persistence\"," +
    "   \"author\": \"Vlad Mihalcea\"," +
    "   \"publisher\": \"Amazon\"," +
    "   \"price\": 44.99," +
    "   \"url\": \"https://www.amazon.com/High-Performance-Java-Persistence-Vlad-Mihalcea/dp/973022823X/\"" +
    ""
);

Hibernate 为您处理 UPDATE 声明:

SELECT  b.id AS id1_0_
FROM    book b
WHERE   b.isbn = '978-9730228236'
 
SELECT  b.id AS id1_0_0_ ,
        b.isbn AS isbn2_0_0_ ,
        b.properties AS properti3_0_0_
FROM    book b
WHERE   b.id = 1    
 
UPDATE
    book 
SET
    properties = '"title":"High-Performance Java Persistence","author":"Vlad Mihalcea","publisher":"Amazon","price":44.99,"url":"https://www.amazon.com/High-Performance-Java-Persistence-Vlad-Mihalcea/dp/973022823X/"'
WHERE
    id = 1

GitHub 上提供的所有代码。

【讨论】:

打破我的 H2 单元测试:JdbcSQLNonTransientException: Unknown data type: "JSON"; @TiggerToo,当然,它会破坏你的 H2 测试。 MySQL 函数和特性旨在在 MySQL 上进行测试,而不是在 H2 上进行测试。除非您在生产中使用 H2(这不太可能),否则您没有理由要针对与生产中使用的数据库引擎不同的数据库引擎来测试数据访问层。 我相信是你我在另一个问题中说过这个,但是是的,有一个很好的理由使用不同的数据库引擎,它被称为单元测试。 测试数据库称为集成测试。单元测试是针对单元的,比如当你测试 POJO 的方法或使用 Mocks 的服务时。 它适用于 String、DTO、Map 或其他实体属性【参考方案2】:

在 Kotlin 中,上述建议的以下变体/组合对我有用:

@Entity
@Table(name = "product_menu")
@TypeDef(name = "json", typeClass = JsonStringType::class)
data class ProductMenu(

    @Type(type = "json")
    @Column(name = "menu_json", columnDefinition = "json")
    @Convert(attributeName = "menuJson", converter = JsonToMapConverter::class)
    val menuJson: HashMap<String, Any> = HashMap()

) : Serializable



import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.ObjectMapper
import org.slf4j.LoggerFactory
import java.io.IOException
import javax.persistence.AttributeConverter

class JsonToMapConverter : AttributeConverter<String, HashMap<String, Any>> 

    companion object 
        private val LOGGER = LoggerFactory.getLogger(JsonToMapConverter::class.java)
    

    override fun convertToDatabaseColumn(attribute: String?): HashMap<String, Any> 
        if(attribute == null) 
            return HashMap()
        
        try 
            val objectMapper = ObjectMapper()
            @Suppress("UNCHECKED_CAST")
            return objectMapper.readValue(attribute, HashMap::class.java) as HashMap<String, Any>
         catch (e: IOException) 
            LOGGER.error("Convert error while trying to convert string(JSON) to map data structure.")
        
        return HashMap()
    

    override fun convertToEntityAttribute(dbData: HashMap<String, Any>?): String? 
        return try 
            val objectMapper = ObjectMapper()
            objectMapper.writeValueAsString(dbData)
         catch (e: JsonProcessingException) 
            LOGGER.error("Could not convert map to json string.")
            return null
        
    


【讨论】:

【参考方案3】:

Heril Muratovic's answer 很好,但我认为JsonToMapConverter 应该实现AttributeConverter&lt;Map&lt;String, Object&gt;, String&gt;,而不是AttributeConverter&lt;String, Map&lt;String, Object&gt;&gt;。这是适合我的代码

@Slf4j
@Converter
public class JsonToMapConverter implements AttributeConverter<Map<String, Object>, String> 
    @Override
    @SuppressWarnings("unchecked")
    public Map<String, Object> convertToEntityAttribute(String attribute) 
        if (attribute == null) 
            return new HashMap<>();
        
        try 
            ObjectMapper objectMapper = new ObjectMapper();
            return objectMapper.readValue(attribute, HashMap.class);
         catch (IOException e) 
            log.error("Convert error while trying to convert string(JSON) to map data structure.", e);
        
        return new HashMap<>();
    

    @Override
    public String convertToDatabaseColumn(Map<String, Object> dbData) 
        try 
            ObjectMapper objectMapper = new ObjectMapper();
            return objectMapper.writeValueAsString(dbData);
         catch (JsonProcessingException e) 
            log.error("Could not convert map to json string.", e);
            return null;
        
    

【讨论】:

我在使用上述类后收到此错误。我的问题是 - 我的 postgres 表有一个 JSON 列,需要将它们映射到 aggEntity 类中。所以使用了上面的类但是出错了。【参考方案4】:

我更喜欢这样做:

创建从 Map 到 String 的转换器(属性转换器),反之亦然。 在域(实体)类中使用Map映射mysql JSON列类型

代码如下。

JsonToMapConverted.java

@Converter
public class JsonToMapConverter 
                    implements AttributeConverter<String, Map<String, Object>> 

    private static final Logger LOGGER = LoggerFactory.getLogger(JsonToMapConverter.class);

    @Override
    @SuppressWarnings("unchecked")
    public Map<String, Object> convertToDatabaseColumn(String attribute)
    
        if (attribute == null) 
           return new HashMap<>();
        
        try
        
            ObjectMapper objectMapper = new ObjectMapper();
            return objectMapper.readValue(attribute, HashMap.class);
        
        catch (IOException e) 
            LOGGER.error("Convert error while trying to convert string(JSON) to map data structure.");
        
        return new HashMap<>();
    

    @Override
    public String convertToEntityAttribute(Map<String, Object> dbData)
    
        try
        
            ObjectMapper objectMapper = new ObjectMapper();
            return objectMapper.writeValueAsString(dbData);
        
        catch (JsonProcessingException e)
        
            LOGGER.error("Could not convert map to json string.");
            return null;
        
    

域(实体映射)类的一部分

...

@Column(name = "meta_data", columnDefinition = "json")
@Convert(attributeName = "data", converter = JsonToMapConverter.class)
private Map<String, Object> metaData = new HashMap<>();

...

这个解决方案非常适合我。

【讨论】:

嗨,我收到了这个错误。 Unexpected error occurred: Error attempting to apply AttributeConverter @Heril 相同 您混淆了转换器的定义。它应该实现AttributeConverter&lt;Map&lt;String, Object&gt;, String&gt; 对我不起作用。如果我将数据库数据类型更改为字符串,那么它可以工作。 DTO 中的数据类型是什么。【参考方案5】:

对于任何人都无法制作@J。王答题:

尝试添加这个依赖(它适用于hibernate 5.1和5.0,其他版本检查here)

<dependency>
    <groupId>com.vladmihalcea</groupId>
    <artifactId>hibernate-types-5</artifactId>
    <version>1.2.0</version>
</dependency>

并将这一行添加到实体中

@TypeDef(name = "json", typeClass = JsonStringType.class)

那么完整版的实体类:

@Entity
@Table(name = "some_table_name")
@TypeDef(name = "json", typeClass = JsonStringType.class)
public class MyCustomEntity implements Serializable 

   private static final long serialVersionUID = 1L;

   @Id
   @GeneratedValue(strategy = GenerationType.AUTO)
   private Long id;

   @Type( type = "json" )
   @Column( columnDefinition = "json" )
   private List<String> jsonValue;

我使用 spring boot 1.5.9 和 hibernate-types-5 1.2.0 测试代码。

【讨论】:

这也是一个很好的解决方案,使用休眠类型,但我认为使用属性转换器和映射可能更好。在下面查看我的答案。【参考方案6】:

如果你的 json 数组中的值是简单的字符串,你可以这样做:

@Type( type = "json" )
@Column( columnDefinition = "json" )
private String[] jsonValue;

【讨论】:

@Typeorg.hibernate.annotations.Type

以上是关于如何使用 JPA 和 Hibernate 将 MySQL JSON 列映射到 Java 实体属性的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 JPA 和 Hibernate 将 MySQL JSON 列映射到 Java 实体属性

Spring boot、JPA、Hibernate 和 H2 DB 测试 - 序列不起作用 - 意外的代码路径”;SQL 语句:调用 seq_my_jobs [50000-193] 的下一个值

如何使用 JPA 和 Hibernate 在非主键上连接表

如何使用 JPA 和 Hibernate 映射 PostgreSQL 枚举

如何使用 JPA 和 Hibernate 在 UTC 时区中存储日期/时间和时间戳

如何使用 JPA 和 Hibernate 连接两个不相关的实体