7

I have a JPA entity object with following structure:

@Table(name="item_info")
class Item(){
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    @Column(name="item_name")
    private String itemName;

    @Column(name="product_sku")
    private String productSku;

    @Column(name="item_json")
    private String itemJsonString;

    @Transient
    private ItemJson itemJson;

    //Getters and setters

}

The itemJsonString field contains a json string value such as '{"key1":"value1","key2":"value2"}'

And the itemJson field contains the corresponding object which maps to the json string.

I get this entity object from database as follows:

Item item = itemRepository.findOne(1L);    // Returns item with id 1

Now, the itemJson field is null since it is a transient field. And I have to set it manually using Jackson's ObjectMapper as follows:

itemJson = objectMapper.readValue(item.getItemJsonString(), ItemJson.class);

How can I make it such that when I do itemRepository.findOne(), it returns an Item object with the itemJson field mapped to the json String automatically?

Petr Mensik
  • 26,874
  • 17
  • 90
  • 115
drunkenfist
  • 2,958
  • 12
  • 39
  • 73

3 Answers3

14

Your best bet would be to implement a javax.persistence.Converter. It would look something like:

import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

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

    @Override
    public String convertToDatabaseColumn(ItemJson entityValue) {
        if( entityValue == null )
            return null;

        ObjectMapper mapper = new ObjectMapper();

        return mapper.writeValueAsString(entityValue);
    }

    @Override
    public ItemJson convertToEntityAttribute(String databaseValue) {
        if( databaseValue == null )
            return null;

        ObjectMapper mapper = new ObjectMapper();

        return mapper.readValue(databaseValue, ItemJson.class);

    }
}

I've used this with WildFly and didn't have to do anything except have it be in the war file I was deploying.

stdunbar
  • 16,263
  • 11
  • 31
  • 53
  • I have tried to use `@Autowired` for ObjectMapper: `@Autowired private ObjectMapper om` but without success. Only works if mapper is defined as static: `private static ObjectMapper om` and initialized via autowired setter. Can you please explain why simple field autowirign not working? I'm asking because you have used `ObjectMapper mapper = new ObjectMapper();` in every converter method instead of autowiring it. – chill appreciator Oct 04 '19 at 22:35
  • @standalone I'm not using a Spring based solution in this example, just standard JEE. In this class a single `private static final ObjectMapper` may be more efficient depending on your use case. – stdunbar Oct 04 '19 at 23:03
  • I don't know why, but for me `ObjectMapper mapper = new ObjectMapper();` at each convertor method just not working. Mapper just can't convert what I need. But when I autowiring mapper it works. In case of autowiring I afraid that actions like `registerModule()` and `setDateFormat()` will lead to side effects. Because I checked with debugger that autowired `ObjectMapper` is always the same object. Do you think it is bad to modify the state of autowired `ObjectMapper` object? – chill appreciator Oct 05 '19 at 18:14
3

Here is the full working version of AttributeConverter + JPA + Kotlin.

Entity Class

In my case, database was mysql (8.x), which supports JSON as the underlying data type for column definition, and we can apply a custom converter using @Convert annotation.

@Entity
data class Monitor (
    @Id
    val id: Long? = null,

    @Column(columnDefinition = "JSON")
    @Convert(converter = AlertConverter::class)
    var alerts: List<Alert> = emptyList(),

    var active: Boolean = false
)

Converter Definition Attribute converter needs to specify the conversion mechanism from data to db and reverse. We are using Jackson to convert a java object into String format and vice versa.

@Converter(autoApply = true)
class AlertConverter : AttributeConverter<List<Alert>, String> {

private val objectMapper = ObjectMapper()

 override fun convertToDatabaseColumn(data: List<Alert>?): String {
    return if (data != null && !data.isEmpty())
        objectMapper.writeValueAsString(data)
    else ""
 }

 override fun convertToEntityAttribute(dbData: String?): List<Alert> {
    if (StringUtils.isEmpty(dbData)) {
        return emptyList()
    }
    return objectMapper.readValue(dbData, object : TypeReference<List<Alert>>() {})
 }
}
Munish Chandel
  • 3,572
  • 3
  • 24
  • 35
0

You could postLoad callback for manipulating entity after it's loaded. So try something like this inside your entity class

@PostLoad
public void afterLoad() {
    ObjectMapper mapper = new ObjectMapper();
    itemJson = mapper.readValue(item.getItemJsonString(), ItemJson.class);
}
Petr Mensik
  • 26,874
  • 17
  • 90
  • 115
  • I was planning to use PostLoad, but was not sure if this would cover all scenarios. – drunkenfist Feb 24 '16 at 22:28
  • Well, ti should, everytime the `EntityManager` loads the entity it call the post load method, even with queries. See also this answer https://stackoverflow.com/questions/10313535/initializing-a-transient-attribute-of-a-jpa-entity-during-criteriaquery – Petr Mensik Feb 25 '16 at 08:10
  • Used @stdunbar's method as I also had to convert the JSON back to string before storing it in DB. – drunkenfist Mar 01 '16 at 01:48