0

I'm trying to create mapping that will store my custom object as a String to a single column in a database table.

Article.java

@Entity
public class Article {

    @Getter @Setter
    @Embedded
    @Column(name = "name", columnDefinition = "TEXT")
    @Convert(converter = LocalizedFieldConverter.class,
        attributeName = "name")
    private LocalizedField name = new LocalizedField();

    @Getter @Setter
    @Embedded
    @Column(name = "name", columnDefinition = "TEXT")
    @Convert(converter = LocalizedFieldConverter.class,
        attributeName = "description")
    private LocalizedField description = new LocalizedField();

}

LocalizedField.java

@Embeddable @Getter @Setter
public class LocalizedField {

    // map key is "en-US", value is a translation
    private Map<String, String> data = new HashMap<>();

}

LocalizedFieldConverter.java

@Converter
public class LocalizedFieldConverter
    implements AttributeConverter<LocalizedField, String>, Serializable
{

    @Override
    public String convertToDatabaseColumn(LocalizedField attribute) {
        if (attribute == null) { return null; }
        try {
            return new ObjectMapper().writeValueAsString(attribute);
        }
        catch (JsonProcessingException e) { return null; }
    }

    @Override
    public LocalizedField convertToEntityAttribute(String dbData) {
        if (dbData == null) { return null; }
        try {
            return new ObjectMapper().readValue(dbData, typeReference());
        }
        catch (JsonProcessingException e) { return null; }
    }

    private static final TypeReference<LocalizedField> typeReference() {
        return new TypeReference<>() { /* */ };
    }

}

When Hibernate tries to create the database, I get this one:

Caused by: org.hibernate.MappingException: Could not determine type for: java.util.Map, at table: article, for columns: [org.hibernate.mapping.Column(data)]

I don't want to use @ElementCollection as this would create additional table in my database.


Next try was to change mapping to this:

Article.java

@Getter @Setter
private LocalizedField name = new LocalizedField();

LocalizedField.java

@Convert(converter = LocalizedFieldMapConverter.class)
private Map<String, String> data = new HashMap<>();

LocalizedFieldMapConverter.java

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

    @Override
    public String convertToDatabaseColumn(Map<String, String> attribute) {
        if (attribute == null) { return null; }
        try {
            return new ObjectMapper().writeValueAsString(attribute);
        }
        catch (JsonProcessingException e) { return null; }
    }

    @Override
    public Map<String, String> convertToEntityAttribute(String dbData) {
        if (dbData == null) { return null; }
        try {
            return new ObjectMapper().readValue(dbData, typeReference());
        }
        catch (JsonProcessingException e) { return null; }
    }

    private static final TypeReference<Map<String, String>> typeReference() {
        return new TypeReference<>() { /* */ };
    }

}

In second case I had to remove description property from entity because database column gets a name data which is a name of Map inside LocalizedField class.

In the final I would like to have more than one LocalizedField property handled by the same @Converter (of course, if possible).

But in cotrary to first attempt, at least I get column stored as expected, in JSON format but it's named data instead of name:

{
    "hr-HR" : "Sončnica & vitamin E šampon za kosu i tijelo 1000 ml",
    "en-US" : "Sunflower & Vitamin E Hair & Body Shampoo 1000 ml",
    "sr-RS" : "Сончница & витамин Е шампон за косу и тело 1000 мл"
}

What did I do wrong?

2
  • Well, if you want to store it as a string, why use @Embedded? Commented Jun 30, 2022 at 22:21
  • @crizzis When I remove @Embedded I get the same exception: org.hibernate.MappingException: Could not determine type for: java.util.Map, at table: article, for columns: [org.hibernate.mapping.Column(data)] Commented Jun 30, 2022 at 22:39

2 Answers 2

0

The solution that works with more than one LocalizedField properties needs use of @AttributeOverride.

Changes are:

Article.java

@Getter @Setter
@Embedded
@AttributeOverride(
    name = "data", // refers to Map name inside LocalizedField
    column = @Column(
        name = "name", // column name to use for converted LocalizedField
        columnDefinition = "TEXT"
    )
)
private LocalizedField name = new LocalizedField();

@Getter @Setter
@Embedded
@AttributeOverride(
    name = "data", // refers to Map name inside LocalizedField
    column = @Column(
        name = "description", // column name to use for converted LocalizedField
        columnDefinition = "TEXT"
    )
)
private LocalizedField description = new LocalizedField();

LocalizedField.java

@Convert(converter = LocalizedFieldMapConverter.class)
private Map<String, String> data = new HashMap<>();
Sign up to request clarification or add additional context in comments.

Comments

0

Just drop @Embedded, @Embeddable, and attributeName from the original solution:

@Entity
public class Article {

    @Getter @Setter
    @Column(columnDefinition = "TEXT")
    private LocalizedField name = new LocalizedField();

    @Getter @Setter
    @Column(columnDefinition = "TEXT")
    private LocalizedField description = new LocalizedField();

}

@Getter @Setter
public class LocalizedField {

    private Map<String, String> data = new HashMap<>();

}

@Converter(autoApply=true)
public class LocalizedFieldConverter { ... }

@Embedded and @AttributeConverter are mutually exclusive. You're getting a MappingException because Hibernate cannot figure out how the hell it's supposed to map LocalizedField as an embeddable, since data itself doesn't have an attribute converter.

EDIT Since your AttributeConverter applies to LocalizedField, and NOT to AttributeConverter.data directly, you want:

@Converter
public class LocalizedFieldMapConverter
    implements AttributeConverter<LocalizedField, String>, Serializable {

    @Override
    public String convertToDatabaseColumn(LocalizedField attribute) {
        if (attribute == null) { return null; }
        try {
            return new ObjectMapper().writeValueAsString(attribute.getData());
        }
        catch (JsonProcessingException e) { return null; }
    }

    @Override
    public LocalizedField convertToEntityAttribute(String dbData) {
        if (dbData == null) { return null; }
        try {
            Map<String, String> data = new ObjectMapper().readValue(dbData, typeReference());
LocalizedField result = new LocalizedField();
result.setData(data); // or add an @AllArgsConstructor and use it
return result;
        }
        catch (JsonProcessingException e) { return null; }
    }

    private static final TypeReference<Map<String, String>> typeReference() {
        return new TypeReference<>() { /* */ };
    }

}

Alternatively, if you don't need the extra LocalizedField class for any other purpose, just use:

@Entity
public class Article {

    @Getter @Setter
    @Column(columnDefinition = "TEXT")
    @Convert(converter = LocalizedFieldConverter.class)
    private Map<String, String> name = new HashMap<>();

    @Getter @Setter
    @Column(columnDefinition = "TEXT")
    @Convert(converter = LocalizedFieldConverter.class)
    private Map<String, String> description = new HashMap<>();

}

8 Comments

Been there before at some moment. This one wraps JSON with 'data' which is some kind of overhead: { "data" : { "hr-HR" : "Tekoče milo Lemon Grass 1000 ml", "en-US" : "Liquid Hand Wash Lemon Grass 1000 ml", "sr-RS" : "Tekoče milo Lemon Grass 1000 ml" } }. Yesterday I found working solution which is not so pretty as yours is, but column contents are exactly as expected: { "hr-HR" : "Tekoče milo Lemon Grass 1000 ml", "en-US" : "Liquid Hand Wash Lemon Grass 1000 ml", "sr-RS" : "Tekoče milo Lemon Grass 1000 ml" }
Also spent few hours yesterday because I forgot to remove @Embeddable and was getting whole lot of exceptions.
Well, if you're serializing LocalizedField, then of course the resulting JSON is going to have LocalizedField properties, and that includes data. You need to be serializing the value of LocalizedField.data instead. See my updated answer.
(also note the use of @Converter(autoApply=true) in the updated answer, which makes it even less verbose)
Fair enough, I only posted that option as an alternative. I believe my modified answer should still work for you
|

Your Answer

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