ChatGPT解决这个技术问题 Extra ChatGPT

How to persist a property of type List<String> in JPA?

What is the smartest way to get an entity with a field of type List persisted?

Command.java

package persistlistofstring;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Basic;
import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Persistence;

@Entity
public class Command implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    Long id;
    @Basic
    List<String> arguments = new ArrayList<String>();

    public static void main(String[] args) {
        Command command = new Command();

        EntityManager em = Persistence
                .createEntityManagerFactory("pu")
                .createEntityManager();
        em.getTransaction().begin();
        em.persist(command);
        em.getTransaction().commit();
        em.close();

        System.out.println("Persisted with id=" + command.id);
    }
}

This code produces:

> Exception in thread "main" javax.persistence.PersistenceException: No Persistence provider for EntityManager named pu: Provider named oracle.toplink.essentials.PersistenceProvider threw unexpected exception at create EntityManagerFactory: 
> oracle.toplink.essentials.exceptions.PersistenceUnitLoadingException
> Local Exception Stack: 
> Exception [TOPLINK-30005] (Oracle TopLink Essentials - 2.0.1 (Build b09d-fcs (12/06/2007))): oracle.toplink.essentials.exceptions.PersistenceUnitLoadingException
> Exception Description: An exception was thrown while searching for persistence archives with ClassLoader: sun.misc.Launcher$AppClassLoader@11b86e7
> Internal Exception: javax.persistence.PersistenceException: Exception [TOPLINK-28018] (Oracle TopLink Essentials - 2.0.1 (Build b09d-fcs (12/06/2007))): oracle.toplink.essentials.exceptions.EntityManagerSetupException
> Exception Description: predeploy for PersistenceUnit [pu] failed.
> Internal Exception: Exception [TOPLINK-7155] (Oracle TopLink Essentials - 2.0.1 (Build b09d-fcs (12/06/2007))): oracle.toplink.essentials.exceptions.ValidationException
> Exception Description: The type [interface java.util.List] for the attribute [arguments] on the entity class [class persistlistofstring.Command] is not a valid type for a serialized mapping. The attribute type must implement the Serializable interface.
>         at oracle.toplink.essentials.exceptions.PersistenceUnitLoadingException.exceptionSearchingForPersistenceResources(PersistenceUnitLoadingException.java:143)
>         at oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider.createEntityManagerFactory(EntityManagerFactoryProvider.java:169)
>         at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:110)
>         at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:83)
>         at persistlistofstring.Command.main(Command.java:30)
> Caused by: 
> ...

O
Opal

Use some JPA 2 implementation: it adds a @ElementCollection annotation, similar to the Hibernate one, that does exactly what you need. There's one example here.

Edit

As mentioned in the comments below, the correct JPA 2 implementation is

javax.persistence.ElementCollection

@ElementCollection
Map<Key, Value> collection;

See: http://docs.oracle.com/javaee/6/api/javax/persistence/ElementCollection.html


My mistake was to add @ OneToMany annotation as well... after removing it and just leaving @ ElementCollection it worked
T
TylerH

Should anyone be looking for an alternative solution where you store your string lists as one field in your database, here's how I solved that. Create a Converter like this:

import java.util.Arrays;
import java.util.List;

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

import static java.util.Collections.*;

@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();
    }
}

Now use it on your Entities like this:

@Convert(converter = StringListConverter.class)
private List<String> yourList;

In the database, your list will be stored as foo;bar;foobar, and in your Java object you will get a list with those strings.


Will it work with jpa repositories for filtering results by content of that field?
@Please_Dont_Bully_Me_SO_Lords It is less suitable for that use case since your data will be in the database as "foo;bar;foobar". If you want to query for the data then probably an ElementCollection + JoinTable is the way to go for your situation.
This also means that you can't have any SPLIT_CHAR occurrences in your string.
@crush that is correct. Though of course you could allow for it by for example encoding your string after you have correctly delimited it. But the solution I posted here is primarily meant for simple use cases; for more complex situations probably you'll fare better with an ElementCollection + JoinTable
@Al-Mothafar thanks, good addition. Since I generally dislike returning null I have edited the code to either return an empty string or an empty list. But of course if someone else prefers a null instead they are free to do so.
d
diogo

It seems none of the answers explored the most important settings for an @ElementCollection mapping.

When you map a list with this annotation and let JPA/Hibernate auto-generate the tables, columns, etc., it'll use auto-generated names as well.

So, let's analyze a basic example:

@Entity
@Table(name = "sample")
public class MySample {

    @Id
    @GeneratedValue
    private Long id;

    @ElementCollection // 1
    @CollectionTable(name = "my_list", joinColumns = @JoinColumn(name = "id")) // 2
    @Column(name = "list") // 3
    private List<String> list;
    
}

The basic @ElementCollection annotation (where you can define the known fetch and targetClass preferences) The @CollectionTable annotation is very useful when it comes to giving a name to the table that'll be generated, as well as definitions like joinColumns, foreignKey's, indexes, uniqueConstraints, etc. @Column is important to define the name of the column that'll store the varchar value of the list.

The generated DDL would be:

-- table sample
CREATE TABLE sample (
  id bigint(20) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (id)
);

-- table my_list
CREATE TABLE IF NOT EXISTS my_list (
  id bigint(20) NOT NULL,
  list varchar(255) DEFAULT NULL,
  FOREIGN KEY (id) REFERENCES sample (id)
);

I like this solution because it is the only proposed solution which gives the full description including the TABLE structures and explains why we need the different annotations.
b
billjamesdev

This answer was made pre-JPA2 implementations, if you're using JPA2, see the ElementCollection answer above:

Lists of objects inside a model object are generally considered "OneToMany" relationships with another object. However, a String is not (by itself) an allowable client of a One-to-Many relationship, as it doesn't have an ID.

So, you should convert your list of Strings to a list of Argument-class JPA objects containing an ID and a String. You could potentially use the String as the ID, which would save a little space in your table both from removing the ID field and by consolidating rows where the Strings are equal, but you would lose the ability to order the arguments back into their original order (as you didn't store any ordering information).

Alternatively, you could convert your list to @Transient and add another field (argStorage) to your class that is either a VARCHAR() or a CLOB. You'll then need to add 3 functions: 2 of them are the same and should convert your list of Strings into a single String (in argStorage) delimited in a fashion that you can easily separate them. Annotate these two functions (that each do the same thing) with @PrePersist and @PreUpdate. Finally, add the third function that splits the argStorage into the list of Strings again and annotate it @PostLoad. This will keep your CLOB updated with the strings whenever you go to store the Command, and keep the argStorage field updated before you store it to the DB.

I still suggest doing the first case. It's good practice for real relationships later.


Changing from ArrayList to String with comma separated values worked for me.
But this forces you to use (imho) ugly like statements when querying that field.
Yes, as I said... do the first option, it's better. If you just can't bring yourself to do it, option 2 can work.
J
Jaimin Patel

We can also use this.

@Column(name="arguments")
@ElementCollection(targetClass=String.class)
private List<String> arguments;

probably plus @JoinTable.
t
toolkit

According to Java Persistence with Hibernate

mapping collections of value types with annotations [...]. At the time of writing it isn't part of the Java Persistence standard

If you were using Hibernate, you could do something like:

@CollectionOfElements(targetElement = String.class)
@JoinTable(name = "foo", joinColumns = @JoinColumn(name = "foo_id"))
@IndexColumn(name = "POSITION", base = 1)
@Column(name = "baz", nullable = false)
private List<String> arguments = new ArrayList<String>();

Update: Note, this is now available in JPA2.


佚名

When using the Hibernate implementation of JPA , I've found that simply declaring the type as an ArrayList instead of List allows hibernate to store the list of data.

Clearly this has a number of disadvantages compared to creating a list of Entity objects. No lazy loading, no ability to reference the entities in the list from other objects, perhaps more difficulty in constructing database queries. However when you are dealing with lists of fairly primitive types that you will always want to eagerly fetch along with the entity, then this approach seems fine to me.

@Entity
public class Command implements Serializable {

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

    ArrayList<String> arguments = new ArrayList<String>();


}

Thanks. This work with all JPA implementations, Arraylist is Serializable is saved in a BLOB field. The problems with this method are that 1) the BLOB size is fixed 2) you can search or index the array elements 3) only a client aware about the Java serialization format can read these elements.
In case if you try this approach with @OneToMany @ManyToOne @ElementCollection it will give you Caused by: org.hibernate.AnnotationException: Illegal attempt to map a non collection as a @OneToMany, @ManyToMany or @CollectionOfElements exception on server startup. Because hibernates wants you to use collection interfaces.
A
Anthony

I had the same problem so I invested the possible solution given but at the end I decided to implement my ';' separated list of String.

so I have

// a ; separated list of arguments
String arguments;

public List<String> getArguments() {
    return Arrays.asList(arguments.split(";"));
}

This way the list is easily readable/editable in the database table;


This is totally valid but consider the growth of your application and the schema evolution. Sometime in the (near) future you might eventually switch to the entity based approach.
I agree, that is totally valid. However, I do suggest to fully review the logic as well as the implementation of the code. If String arguments is a list of access permissions, then having a special character, a separator, might be vulnerable for privilege escalation attacks.
This is really bad advice, your string might contain ; which will break your app.
Maybe it's better to check that you can do the split return (arguments.contains(";")) ? Arrays.asList(arguments.split(";")) : null;
I
Inverce

Ok i know its bit late. But for those brave souls that will see this as time passes.

As written in documentation:

@Basic: The simplest type of mapping to a database column. The Basic annotation can be applied to a persistent property or instance variable of any of the following types: Java primitive types, [...], enums, and any other type that implements java.io.Serializable.

The important part is type that implements Serializable

So by far the most simple and easiest to use solution is simply using ArrayList instead of List (or any serializable container):

@Basic
ArrayList<Color> lovedColors;

@Basic
ArrayList<String> catNames;

Remember however that this will use system serialization, so it will come with some price, such as:

if serialized object model will change, u might not be able to restore data

small overhead is added for each element stored.

In short

it is quite simple to store flags or few elements, but i would not recomend it to store data that might grow big.


tried this but the sql table made the datatype a tinyblob. Doesn't this make inserting and retrieving the list of strings very inconvenient? Or does jpa automatically serialize and deserialize for you?
jpa will automatically deserialize this for you
Z
Zia

Thiago answer is correct, adding sample more specific to question, @ElementCollection will create new table in your database, but without mapping two tables, It means that the collection is not a collection of entities, but a collection of simple types (Strings, etc.) or a collection of embeddable elements (class annotated with @Embeddable).

Here is the sample to persist list of String

@ElementCollection
private Collection<String> options = new ArrayList<String>();

Here is the sample to persist list of Custom object

@Embedded
@ElementCollection
private Collection<Car> carList = new ArrayList<Car>();

For this case we need to make class Embeddable

@Embeddable
public class Car {
}

g
gosuer1921

Here is the solution for storing a Set using @Converter and StringTokenizer. A bit more checks against @jonck-van-der-kogel solution.

In your Entity class:

@Convert(converter = StringSetConverter.class)
@Column
private Set<String> washSaleTickers;

StringSetConverter:

package com.model.domain.converters;

import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import java.util.HashSet;
import java.util.Set;
import java.util.StringTokenizer;

@Converter
public class StringSetConverter implements AttributeConverter<Set<String>, String> {
    private final String GROUP_DELIMITER = "=IWILLNEVERHAPPEN=";

    @Override
    public String convertToDatabaseColumn(Set<String> stringList) {
        if (stringList == null) {
            return new String();
        }
        return String.join(GROUP_DELIMITER, stringList);
    }

    @Override
    public Set<String> convertToEntityAttribute(String string) {
        Set<String> resultingSet = new HashSet<>();
        StringTokenizer st = new StringTokenizer(string, GROUP_DELIMITER);
        while (st.hasMoreTokens())
            resultingSet.add(st.nextToken());
        return resultingSet;
    }
}

p
powers meat

My fix for this issue was to separate the primary key with the foreign key. If you are using eclipse and made the above changes please remember to refresh the database explorer. Then recreate the entities from the tables.


r
razvang

What I wanted was a simple way of persisting a set of Strings, in a table column.

I ended up using JSON, as MySQL 5.7+, has native support. Here's my solution

    @Column(name = "eligible_approvers", columnDefinition = "json")
    @Convert(converter = ArrayJsonConverter.class)
    private Set<String> eligibleApprovers;

And then write a very basic converter

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

    static final ObjectMapper mapper = new ObjectMapper();

    @Override
    public String convertToDatabaseColumn(Set list) {
        if (list == null)
            return null;
        try {
            return mapper.writeValueAsString(list);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }


    @Override
    public Set convertToEntityAttribute(String dbJson) {
        if (dbJson == null)
            return null;
        try {
            return mapper.readValue(dbJson, new TypeReference<Set<String>>() {
            });
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }
}

J
Julian Eckhardt

As my reputation is not enough yet to comment on the much underrated answer written by @razvang:

As this question was asked over a decade ago, keep in mind much of the world has changed in the time since. We now have databases with native JSON column support and can use this functionality instead of using separate entities, joins or custom String-to-List converters, which are used by the other answers.

Let me suggest two purely optional changes to @razvang's superb answer though, which might be interesting depending on your specific situation:

You could omit the auto_apply = true and add @Convert(converter = .class) to the entity field to keep control over when your converter is used. Instead of throwing a RuntimeException whenever a conversion fails, you could handle the error right there (for example pass an empty list and write a log message) to make it fail somewhat gracefully.