How to map an ArrayList of primitives to a single column?
Asked Answered
V

4

13

Let's say I have the following situation:

Object Car has an ArrayList of prices, which are all numbers. Is it possible in Hibernate to save all the prices in a single column? I know this violates the first normal form but there might be cases when you don't want them to be saved in a separate table like it's classically done in One-To-Many or Many-To-Many relationships.

In JDO I'd do this easily by saving the ArrayList in a BLOB column.

Some useful related SOF questions: ArrayList of primitive types in Hibernate and Map ArrayList with Hibernate .

Any idea will be highly appreciated!

Vermicular answered 4/5, 2013 at 11:32 Comment(0)
J
29

I know this is an old question but for anyone trying to do this in a JPA context you can do this

import org.apache.commons.lang3.StringUtils;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.Collections;

@Converter
public class IntArrayToStringConverter implements AttributeConverter<List<Integer>,String>{
    @Override
    public String convertToDatabaseColumn(List<Integer> attribute) {
        return attribute == null ? null : StringUtils.join(attribute,",");
    }

    @Override
    public List<Integer> convertToEntityAttribute(String dbData) {
        if (StringUtils.isBlank(dbData))
            return Collections.emptyList();

        try (Stream<String> stream = Arrays.stream(dbData.split(","))) {
            return stream.map(Integer::parseInt).collect(Collectors.toList());
        }
    }
}

Then to use it something like this in your entity

@Entity
public class SomeEntity
{

   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Integer id;

   @Column
   @Convert(converter = IntArrayToStringConverter.class)
   private List<Integer> integers;

   ...
}
Jujutsu answered 15/3, 2016 at 18:28 Comment(5)
Nice solution, but note that this is only available since JPA 2.1.Heribertoheringer
In my case I get a WARN: "Could not find matching type descriptor for requested Java class [java.util.List]; using fallback"Ers
I still get Basic attribute should not be a containerFreeholder
I used import org.thymeleaf.util.StringUtils instead which changes StringUtils.isBlank into StringUtils.isEmpty and it works fine!Whelan
Cool feature indeed... I used ObjectMapper to do my conversions ...Martel
T
7

AFAIR, Hibernate will use native serialization and store the resulting bytes in your column. I wouldn't do that though, and use a dedicated transformation that would make the prices readable in the database, and at least be able to use the data without needing Java native serialization:

@Basic
private String prices;

public void setPrices(List<Integer> prices) {
    this.prices = Joiner.on(',').join(prices);
}

public List<Integer> getPrices() {
    List<Integer> result = new ArrayList<Integer>();
    for (String s : Splitter.on(',').split(this.prices)) {
        result.add(Integer.valueOf(s));
    }
    return result;
}
Typhus answered 4/5, 2013 at 11:46 Comment(1)
Right. That was another thing I was thinking about. Write them as comma separated values or something but it seemed like a hack. Related to native serialization, this is something I tried and hibernate complained because it couldn't map List to a column type. I'll keep trying though and see what comes out of it. Thanks a lot for your answer.Petree
M
3

I updated Chris's code to utilize the Jackson JSON library for lists and maps:

import java.util.List;

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

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

@Converter
public class ListToJsonConverter<T> implements AttributeConverter<List<T>, String> {

    private static ObjectMapper mapper = new ObjectMapper();

    @Override
    public String convertToDatabaseColumn(List<T> attribute) {
        if (attribute == null) {
            return null;
        }
        try {
            return mapper.writeValueAsString(attribute);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return null;
    }

    @SuppressWarnings("unchecked")
    @Override
    public List<T> convertToEntityAttribute(String dbData) {
        if (dbData == null || dbData.isEmpty()) {
            return null;
        }
        try {
            return mapper.readValue(dbData, List.class);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
import java.util.Map;

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

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

@Converter
public class MapToJsonConverter<T, K> implements AttributeConverter<Map<T, K>, String> {

    private static ObjectMapper mapper = new ObjectMapper();

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

    @SuppressWarnings("unchecked")
    @Override
    public Map<T, K> convertToEntityAttribute(String dbData) {
        if (dbData == null || dbData.isEmpty()) {
            return null;
        }
        try {
            return mapper.readValue(dbData, Map.class);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
@Column(name = "example_list")
@Convert(converter = ListToJsonConverter.class)
public List<String> getExampleList() {
    return exampleList;
}

@Column(name = "example_map")
@Convert(converter = MapToJsonConverter.class)
public Map<String, String> getExampleMap() {
    return exampleMap;
}

This allows storing just about any types in a human-readable way in a string column, and makes it so you don't need a separate class for every type of list or hashmap. It also automatically escapes the strings.

Million answered 28/12, 2020 at 15:49 Comment(1)
This actually creates a List of Maps instead of list of Objects. You can replace List.class with new TypeReference<List<T>>(){} and Map.class with new TypeReference<Map<T, K>>(){} to ensure proper types.Balbur
L
1

You can implement your own custom type as an array:

http://docs.jboss.org/hibernate/core/3.6/reference/en-US/html/types.html#types-custom

Also, it is not that hard to find some implementations, some of them going as far to let you compare those arrays in a HQL where clause.

https://forum.hibernate.org/viewtopic.php?t=946973

I personally never thought i would try something like this. But now I am very curious.

Lemniscus answered 7/5, 2013 at 22:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.