1. Overview

In this tutorial, we’ll discuss how to persist a property of type List in JPA. We take a look at the possibilities to realize this, how they differ, and explain the advantages with the help of an example.

2. Example

As a model, we use the entity library, which has an automatically generated ID, a name, a List containing addresses, and a List containing book names:

@Entity(name = "library")
public class Library {

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

    private String name;

    private List<String> addresses = new ArrayList<>();

    private List<String> books = new ArrayList<>();

    // getter, setter, and constructor
}

For the List, it would be possible to create a second entity with an id and the string and then annotate this with a OneToMany relationship. We’ll look at two other possibilities with JPA that simplify this behavior.

3. @ElementCollection

The first option is to use @ElementCollection. This annotation allows us to specify the target class of the collection. In our case, this is a String. Furthermore, we can specify if the list should be loaded in a lazy or eager way. The default value is lazy. However, for the sake of simplicity for the examples, we set the value to eager:

@ElementCollection(targetClass = String.class, fetch = FetchType.EAGER)
@CollectionTable(name = "books", joinColumns = @JoinColumn(name = "library_id"))
@Column(name = "book", nullable = false)
private List<String> books = new ArrayList<>();

We can also annotate the method that wants to access the list with @Transactional or the repository method with @Query(“SELECT l FROM library l JOIN FETCH l.books WHERE l.id = (:id)”) to avoid a LazyInitializationException.

The annotations result in the following DDL:

CREATE TABLE books
(
    library_id BIGINT       NOT NULL,
    book      VARCHAR(255) NOT NULL
);

CREATE TABLE library
(
    id        BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
    name      VARCHAR(255),
    addresses VARCHAR(255)                            NOT NULL,
    CONSTRAINT pk_library PRIMARY KEY (id)
);

ALTER TABLE books
    ADD CONSTRAINT fk_books_on_library FOREIGN KEY (library_id) REFERENCES library (id);

We can see that @CollectionTable sets the name of the second table and the column that references our library table. In addition, the Foreign Key is also created appropriately. So by using @ElementCollection in this method, we save the second entity we would normally need for an OneToMany link.

4. Attribute Converter

Another alternative is to use a converter. For this, we have to implement the generic AttributeConverter with our desired object. In our case, this is the List; the desired format could be a String, for example. In the convertToDatabaseColumn(List stringList) method, the return value is the data type that the object should eventually have in the database, and the parameter is our list.

The convertToEntityAttribute(String string) method, on the other hand, defines how the string from the column is converted back to the List. In our example, we use the character “;” to separate the strings:

@Converter
public class StringListConverter implements AttributeConverter<List<String>, String> {
    private static final String SPLIT_CHAR = ";";

    @Override
    public String convertToDatabaseColumn(List<String> stringList) {
        return stringList != null ? String.join(SPLIT_CHAR, stringList) : "";
    }

    @Override
    public List<String> convertToEntityAttribute(String string) {
        return string != null ? Arrays.asList(string.split(SPLIT_CHAR)) : emptyList();
    }
}

We also have to add our converter to the field with @Convert:

@Convert(converter = StringListConverter.class)
@Column(name = "addresses", nullable = false)
private List<String> addresses = new ArrayList<>();

Alternatively, we could store the list as a JSON string in the column. When we decide to use the AttributeConverter, we have to keep in mind how big our list will grow since it has to fit in the selected size of the column.

In our case, it must fit into the varchar(255) addresses column. In the ElemenCollection approach, we can have an unlimited number of items in our list, each only limited by the varchar(255)  of the column itself.

5. Comparison

Next, we’ll create the LibraryRepository and test our solutions:

@Repository
public interface LibraryRepository extends CrudRepository<Library, Long> {
}

When we now execute the code, we’ll add the list items to the library entity as usual:

Library library = new Library();
library.setAddresses(Arrays.asList("Address 1", "Address 2"));
library.setBooks(Arrays.asList("Book 1", "Book 2"));
        
libraryRepository.save(library);
Library lib = libraryRepository.findById(library.getId().longValue());
System.out.println("lib.getAddresses() = " + lib.getAddresses());
System.out.println("lib.getBooks() = " + lib.getBooks());

We’ll get the following output:

lib.getAddresses() = [Address 1, Address 2]
lib.getBooks() = [Book 1, Book 2]

As we can see, both implementation work as expected and have their own advantages:

Element Collection

Converter

Default Fetch Type

Lazy

Eager

Limit of List

Unlimited list items

Limited by the column length

Limit of each String

Limited by the column length

Limited by the number of list items and so also limited by the column length

Table

Creates an extra table

It doesn’t need its own table

6. Conclusion

In this article, we have discussed what kind of possibilities there are in JPA to store a list of strings of an entity. While we showed where there are possible limitations and what differences there are in the respective possibilities.

As always, the example code is available over on GitHub.