56

We have a big table with a lot of columns. After we moved to MySQL Cluster, the table cannot be created because of:

ERROR 1118 (42000): Row size too large. The maximum row size for the used table type, not counting BLOBs, is 14000. This includes storage overhead, check the manual. You have to change some columns to TEXT or BLOBs

As an example:

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

It's a table for storing configuration parameters. I was thinking that we can combine some columns into one and store it as JSON object and convert it to some Java object.

For example:

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

Where we have defined:

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

By using this we can combine all columns into one and create our table. Or we can split the whole table into several tables. Personally I prefer the first solution.

Anyway my question is how to map the Params column which is text and contains JSON string of a Java object?

Rad
  • 4,292
  • 8
  • 33
  • 71
  • If you have many configuration parameters, just use plain table with 2 columns: key and value and load it to map. If you want to store params as JSON or XML, just store/read it as Text and convert later. – user1516873 Sep 09 '14 at 07:37
  • @Rad does [this](http://stackoverflow.com/questions/22637733/mysql-error-code-1118-row-size-too-large-8126-changing-some-columns-to-te) help you – Ankur Singhal Sep 09 '14 at 07:51
  • @user1516873 we considered this as the final solution. If I'm not mistaken, it increases the complexity while one tries to modify the data. Thanks anyway. – Rad Sep 09 '14 at 08:30
  • @ankur-singhal, I'm not sure. We are using MySQL cluster with NDBCluster engine for our tables. Is it still applicable? – Rad Sep 09 '14 at 08:31
  • https://stackoverflow.com/a/59942878/1776132 is worth checking – Smile Jan 28 '20 at 09:11

9 Answers9

92

You can use a JPA converter to map your Entity to the database. Just add an annotation similar to this one to your params field:

@Convert(converter = JpaConverterJson.class)

and then create the class in a similar way (this converts a generic Object, you may want to specialize it):

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

}

That's it: you can use this class to serialize any object to json in the table.

Alessandro Polverini
  • 2,301
  • 19
  • 29
  • 1
    Note: This solution does not work if you are also using Hibernate Envers and a Hibernate version less than 5 (which isn't out yet at time of writing). See [HHH-9042](https://hibernate.atlassian.net/browse/HHH-9042). – Flavin Apr 26 '16 at 19:49
  • 2
    If the json stored in the DB is an array, meaning something like: [{...},{...},{...}], will the converter throw an exception or will the mapper handle it? – Norbert Bicsi Jan 29 '18 at 08:11
  • Excellent it worked with hibernate ymanager core/entitymanager of 4.3.6 version itself. – Kanagavelu Sugumar Feb 01 '18 at 07:16
  • Also the converter class should be annotated with ```@Converter``` – Vadiraj Purohit Sep 02 '18 at 21:02
  • 3
    It just feels like there should be an annotation that does this for us. This is such a generic use case and seems like something that many people would desire. Alas I have not found one. Thank you for your answer. I ended up doing this myself, and in searching for a better way I stumbled onto your answer, which is essentially the same as mine. This just reassures me that my way is "the cleanest" unfortunately. – Stevers Mar 25 '19 at 15:42
  • @AlessandroPolverini I am getting this error `Internal Server Error: org.hibernate.PropertyAccessException: Could not set field value [{id=123, nama=adada}] value by reflection : [class referral.code.domain.DompetResellers.refData] setter of referral.code.domain.DompetResellers.refData"` – Bobby Jun 11 '19 at 09:10
  • That probably means you wrongly defined your Entity class – Alessandro Polverini Jun 11 '19 at 13:24
  • 4
    @Converter(autoApply = true) do not put this on class as it will automatically apply this converter to all object as having issue with date type and datetime sql type while serializing and deserializing – Mangesh Bhapkar Dec 16 '19 at 18:39
  • 2
    JPA will return the result in `LinkedHashMap`, each Json attribute from jso- db will be converted to a `LinkedHashMap` because of object.class – Ravi Parekh Oct 20 '20 at 13:47
  • It gives error → argument "content" is null – Talha Akbar May 23 '22 at 13:38
25

The JPA AttributeConverter is way too limited to map JSON object types, especially if you want to save them as JSON binary.

You don’t have to create a custom Hibernate Type to get JSON support, All you need to do is use the Hibernate Types OSS project.

For instance, if you're using Hibernate 5.2 or newer versions, then you need to add the following dependency in your Maven pom.xml configuration file:

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

Now, you need to declare the new type either at the entity attribute level or, even better, at the class level in a base class using @MappedSuperclass:

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

And the entity mapping will look like this:

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

If you're using Hibernate 5.2 or later, then the JSON type is registered automatically by MySQL57Dialect.

Otherwise, you need to register it yourself:

public class MySQLJsonDialect extends MySQL55Dialect {

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

And, set the hibernate.dialect Hibernate property to use the fully-qualified class name of the MySQLJsonDialect class you have just created.

Vlad Mihalcea
  • 142,745
  • 71
  • 566
  • 911
  • Could you please take a look at this [question](https://stackoverflow.com/questions/62409871/how-can-i-implement-i18n-as-a-json-object) – MJBZA Jun 16 '20 at 14:57
15

If you need to map json type property to json format when responding to the client (e.g. rest API response), add @JsonRawValue as the following:

@Column(name = "params", columnDefinition = "json")
@JsonRawValue
private String params;

This might not do the DTO mapping for server-side use, but the client will get the property properly formatted as json.

mhsallam
  • 235
  • 3
  • 5
10

It is simple

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

and in mysql database your column 'json_input' json type

enter image description here

Saeed Zhiany
  • 2,051
  • 9
  • 30
  • 41
abhimanyu kumar
  • 101
  • 1
  • 3
2

There is a workaround for those don't want write too much code.

Frontend -> Encode your JSON Object to string base64 in POST method, decode it to json in GET method

In POST Method
data.components = btoa(JSON.stringify(data.components));

In GET
data.components = JSON.parse(atob(data.components))

Backend -> In your JPA code, change the column to String or BLOB, no need Convert.

@Column(name = "components", columnDefinition = "json")
private String components;
marqueen
  • 40
  • 5
1

In this newer version of spring boot and MySQL below code is enough

@Column( columnDefinition = "json" )
private String string;

I was facing quotes issue so I commented below line in my project

#spring.jpa.properties.hibernate.globally_quoted_identifiers=true
Procrastinator
  • 2,526
  • 30
  • 27
  • 36
1

If you are using JPA version 2.1 or higher you can go with this case. Link Persist Json Object

public class HashMapConverter implements AttributeConverter<Map<String, Object>, String> {

@Override
public String convertToDatabaseColumn(Map<String, Object> customerInfo) {

    String customerInfoJson = null;
    try {
        customerInfoJson = objectMapper.writeValueAsString(customerInfo);
    } catch (final JsonProcessingException e) {
        logger.error("JSON writing error", e);
    }

    return customerInfoJson;
}

@Override
public Map<String, Object> convertToEntityAttribute(String customerInfoJSON) {

    Map<String, Object> customerInfo = null;
    try {
        customerInfo = objectMapper.readValue(customerInfoJSON, 
            new TypeReference<HashMap<String, Object>>() {});
    } catch (final IOException e) {
        logger.error("JSON reading error", e);
    }

    return customerInfo;
}

}

A standard JSON object would represent those attributes as a HashMap:

@Convert(converter = HashMapConverter.class) private Map<String, Object> entityAttributes;

Seenu
  • 11
  • 2
0

I had a similar problem, and solved it by using @Externalizer annotation and Jackson to serialize/deserialize data (@Externalizer is OpenJPA-specific annotation, so you have to check with your JPA implementation similar possibility).

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

Params class implementation:

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

HTH

Magic Wand
  • 1,572
  • 10
  • 9
  • Thanks for your answer. We use "spring-data-jpa". As maven says it depends on org.eclipse.persistence, and org.hibernate for JPA. Is it what you noted? – Rad Sep 09 '14 at 08:35
  • Nope, we are using JPA based on OpenJPA, and this annotation is OpenJPA-specific. I think Hibernate as JPA provider does not provide such functionality, only if you use it as plain Hibernate (HQL) and not JPA. And Spring Data JPA, as the name says, uses JPA... – Magic Wand Sep 09 '14 at 12:41
0

Make use of hibernate json types with this dependency in your pom.xml

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

and defining column with json data type in database as follows:

@Type(type = "json")
@Column(columnDefinition = "json")
private JsonNode column;

Here, columnDefinition attribute in the @Column annotation to tell Hibernate that the score column should be mapped to a JSON data type column in the database.

Palak
  • 11
  • 1