8

I have data class/table "User" that has column "preferences"

CREATE table "user"; 
ALTER TABLE "user" ADD COLUMN preferences TEXT;

Preferences type is TEXT and I am storing JSON there.

public class User extends AbstractEntity{
public String preferences;
}

so user.preferences value is "{notifyByEmail:1, favouriteColor:"blue" }"

How can I wrap it with some annotation so I can access it like

user.preferences.notifyByEmail

or without need to wrap into data object

user.preferences.get("notifByEmail");
user.preferences.set("notifByEmail",true);

I imagine there could be some Jackson annotation that I can add to field like

@JsonGenerate
public String preferences;

I am fairly new to JPA and documentation is steep.

I believe my case is quite common. Can anyone give any examples?

Vlad Mihalcea
  • 142,745
  • 71
  • 566
  • 911
Roman
  • 1,920
  • 2
  • 17
  • 17
  • What's the rationale to want to store this data as JSON Strings and not separate, distinct fields (Here, of types boolean and enum)? – Samuel Aug 14 '12 at 09:24
  • Rationale being I don't want to support every key-value preference as separate field. Preference key:values are growing and hanges and there's quite a lot of them. changing model or adding field to database for each of them is an overkill. – Roman Aug 14 '12 at 10:35
  • Have you then considered using an attribute table to story the values? The use case is indeed common, but your proposed solution is not quite fitting into a relational model. – Samuel Aug 14 '12 at 11:16

4 Answers4

16

Can achieve this using JPA Converter.

Entity;

@Id
@GeneratedValue
Long id;

@Column(name = "mapvalue")
@Convert(converter = MapToStringConverter.class)
Map<String, String> mapValue;

Converter:

@Converter
public class MapToStringConverter implements AttributeConverter<Map<String, String>, String> {

    ObjectMapper mapper = new ObjectMapper();

    @Override
    public String convertToDatabaseColumn(Map<String, String> data) {
        String value = "";
        try {
            value = mapper.writeValueAsString(data);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return value;
    }

    @Override
    public Map<String, String> convertToEntityAttribute(String data) {
        Map<String, String> mapValue = new HashMap<String, String>();
        TypeReference<HashMap<String, Object>> typeRef = new TypeReference<HashMap<String, Object>>() {
        };
        try {
            mapValue = mapper.readValue(data, typeRef);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return mapValue;
    }

}

Saving data :

Map<String, String> mapValue = new HashMap<String, String>();
mapValue.put("1", "one");
mapValue.put("2", "two");
DataEntity entity = new DataEntity();
entity.setMapValue(mapValue);
repo.save(entity);

The value will store in DB as

{"1":"three","2":"two"}
Apostolos
  • 10,033
  • 5
  • 24
  • 39
Kumar
  • 867
  • 8
  • 13
6

Honestly I think your best solution is to create a separate table (preference) for your properties.

+------------+
| preference |
+------------+---------+------+-----+
| Field      | Type    | Null | Key |
+------------+---------+------+-----+
| user_id    | bigint  | NO   | PRI |
| key        | varchar | NO   | PRI |
| value      | varchar | NO   |     |
+------------+---------+------+-----+

You can map this in your entity like this:

@Entity
public class User
{
    @Id
    private Long id;

    @ElementCollection
    @MapKeyColumn(name = "key")
    @Column(name = "value")
    @CollectionTable(name = "preference",
        joinColumns = @JoinColumn(name = "user_id"))
    private Map<String, String> preferences;
}

This way your database is more normalized and you don't have to fool around with 'creative solutions' like storing preferences as JSON.

siebz0r
  • 18,867
  • 14
  • 64
  • 107
  • Thank you for providing comprehensive description. I will try it! – Roman Sep 08 '12 at 09:45
  • 16
    This does not do it the way the question requested it. – Hiram Chirino Nov 10 '13 at 17:24
  • 2
    @HiramChirino I do not see anything wrong with providing an alternative to the questioner. Especially when an alternative may provide a better solution. The questioner seemed satisfied with this solution (helpful), the answer isn't effortless or incorrect so I don't see any reason to vote down. Please [see the help pages on when to vote down](http://stackoverflow.com/help/privileges/vote-down), for the sake of Stackoverflow. – siebz0r Nov 11 '13 at 06:24
  • So THAT's how this is done. The way I read how those mapping annotations were used always lead me to believe this type of key-value storage was impossible under JPA. Thanks. – coladict Feb 22 '16 at 09:18
4

It's very easy to persist JSON objects using Hibernate.

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

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

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

Now, you need to add this @TypeDef annotation on the class level or in a package-info.java package-level descriptor to use the JsonType Hibernate Type:

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

And the entity mapping will look like this:

@Type(type = "json")
@Column(columnDefinition = "jsonb")
private String preferences;

That's it!

Vlad Mihalcea
  • 142,745
  • 71
  • 566
  • 911
2

If you really need something like that the most recommendable is to do the next:

public class User extends AbstractEntity{
  @JsonIgnore //This variable is going to be ignored whenever you send data to a client(ie. web browser)
  private String preferences;

  @Transient //This property is going to be ignored whenever you send data to the database
  @JsonProperty("preferences") //Whenever this property is serialized to the client, it is going to be named "perferences" instead "preferencesObj"
  private Preferences preferencesObj;

  public String getPreferences() {
    return new ObjectMapper().writeValueAsString(preferencesObj);
  }

  pbulic void setPreferneces(String preferences) {
    this.preferences = preferences;
    this.preferncesObj = new ObjectMapper().readValue(preferences, Preferences.class);
  }

  pubilc Preferences getPreferencesObj() {
    return preferencesObj;
  }

  public void setPreferencesObj(Preferences preferencesObj) {
    this.preferencesObj = preferencesObj;
  }
}

Additional Notes:

  • Maybe the private property "preferences" could be deleted and only use getter and setter.
  • I haven't test that code.
  • The above code is intended to use Jackson ObjectMapper and Hibernate module.
Luis Vargas
  • 2,466
  • 2
  • 15
  • 32