38

I have a MySQL column declared as type JSON and I have problems to map it with JPA/Hibernate. I'm using Spring Boot on back-end.

Here is small part of my code:

@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;

The program returns me an error and tells me that I can't map the column.

In mysql table the column is defined as:

json_value JSON NOT NULL;
4
  • 3
    Try use columnDefinition = "json" : @Column(name = "json_value", columnDefinition = "json") Commented Jun 1, 2017 at 13:02
  • Please, share exception you received. Commented Jun 1, 2017 at 13:24
  • Problem can be JSONArray type, because everything is working fine if I put String instead. Commented Jun 12, 2017 at 7:26
  • Exception: Could not determine type for: org.json.JSONArray, at table: some_table_name. Commented Jun 12, 2017 at 7:27

7 Answers 7

25

You don’t have to create all these types manually. You can simply get them via Maven Central using the following dependency:

<dependency>
    <groupId>io.hypersistence</groupId>
    <artifactId>hypersistence-utils-hibernate-63</artifactId>
    <version>${hypersistence-utils.version}</version> 
</dependency> 

For more info, check out the Hibernate Types open-source project.

Now, to explain how it all works.

Assuming you have the following entity:

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

Notice two things in the code snippet above:

  • the @Type is used to define a new custom Hibernate Type, json which is handled by the JsonType
  • the properties attribute has a json column type, and it's mapped as a String

That's it!

Now, if you save an entity:

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 is going to generate the following SQL statement:

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

And you can also load it back and modify it:

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 taking caare of the UPDATE statement for you:

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

All code is available on GitHub.

Sign up to request clarification or add additional context in comments.

16 Comments

@TiggerToo, Of course, it breaks your H2 tests. MySQL functions and features are meant to be tested on MySQL, not H2. Unless you are using H2 in production, which is highly unlikely, then, there is no reason why you'd want to test your data access layer against a different DB engine than the one you are using in production.
I believe it was you I said this to in another question, but yes, there is a very good reason to use a different DB engine, and it's called unit tests.
Testing a database is called integration testing. Unit testing is for units, like when you test a method of a POJO or a Service with Mocks.
It works with both String, DTOs, Map or other entity attributes
The @TypeDef annotation is global and allows you to define an alias you'd reference later at the property level. Without it, you'd have to use the String-based fully-qualified class name on each @Type you add to an entity property.
|
22

I prefer to do this way:

  • Creating converter (attribute converter) from Map to String and vice versa.
  • Using Map to map mysql JSON column type in domain (entity) class

The code is bellow.

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;
        }
    }
}

Part of domain (entity-mapping) class

...

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

...

This solution perfectly works for me.

9 Comments

hi, i get this error . Unexpected error occurred: Error attempting to apply AttributeConverter
@Heril Same here
You mixed up in the definition of the convertor. It should implement AttributeConverter<Map<String, Object>, String>
Doesn't work for me. If I change the database data type to String then it works.
What will be the data type in DTO.
|
10

For anyone can't make @J. Wang answer work :

Try add this dependency(it's for hibernate 5.1 and 5.0, other version check here)

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

And add this line to the entity

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

So full version of the entity 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;
}

I test the code with spring boot 1.5.9 and hibernate-types-5 1.2.0 .

1 Comment

This is also a good solution, using hibernate types but I think maybe is better to use attribute converter and map. Check out my answer bellow.
6

If the values inside your json array are simple strings you can do this:

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

2 Comments

The @Type is a org.hibernate.annotations.Type
Cannot resolve method 'type'
6

Heril Muratovic's answer is good, but I think the JsonToMapConverter should implement AttributeConverter<Map<String, Object>, String>, not AttributeConverter<String, Map<String, Object>>. Here is the code that works for me

@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;
        }
    }
}

1 Comment

I am getting this error after using the above class. My problem is - My postgres table has a JSON column, need to map them in aggEntity class. So used the above class but getting error.
0

In Kotlin, the following variation/combination of the above suggestions worked for me:

@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
        }
    }
}

Comments

0

For anyone can't make @J. Wang answer work :

Try add this dependency

<dependency>
  <groupId>com.googlecode.json-simple</groupId>
  <artifactId>json-simple</artifactId>
  <version>1.1.1</version>
</dependency>

Creating converter

JSONObjectConverter.java

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

 private static final Logger logger = (Logger) LoggerFactory.getLogger(JSONArrayConverter.class);

 @Override
 public String convertToDatabaseColumn(JSONObject obj)
 {
    String data = null;
    try
    {
        data = obj.toString();
    }
    catch (final Exception e)
    {
        logger.error("JSON writing error", e);
    }

    return data;
 }

 @Override
 public JSONObject convertToEntityAttribute(String data)
 {
    JSONObject obj = null;

    try
    {
        Object temp = JSONValue.parse(data);
        obj = (JSONObject) temp ;
    }
    catch (final Exception e)
    {
        logger.error("JSON reading error", e);
    }

    return obj;
 }
}

Part of domain (entity-mapping) class

...

@Lob
@Column(name = "pano_data", columnDefinition = "JSON", nullable = true)
@Convert(converter = JSONObjectConverter.class)
private JSONObject panoData;

...

This solution perfectly works for me.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.