如何在JPA中持久保存List <String>类型的属性?


158

获取具有持久性类型List的实体的最聪明方法是什么?

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

此代码产生:

> 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: 
> ...

Answers:


197

使用一些JPA 2实现:它添加了@ElementCollection注释,与Hibernate注释类似,可以完全满足您的需求。有一个例子在这里

编辑

如以下评论中所述,正确的JPA 2实现是

javax.persistence.ElementCollection

@ElementCollection
Map<Key, Value> collection;

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


1
我的错误是也添加了@OneToMany注释...删除后,只留下了@ElementCollection它起作用了
Willi Mentzel

47

抱歉,我们希望恢复一个旧线程,但是如果有人在寻找一种替代解决方案,在其中将字符串列表存储为数据库中的一个字段,这就是我解决的方法。创建一个转换器,如下所示:

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

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

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

    @Override
    public String convertToDatabaseColumn(List<String> stringList) {
        return String.join(SPLIT_CHAR, stringList);
    }

    @Override
    public List<String> convertToEntityAttribute(String string) {
        return Arrays.asList(string.split(SPLIT_CHAR));
    }
}

现在像这样在您的实体上使用它:

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

在数据库中,您的列表将存储为foo; bar; foobar,在Java对象中,您将获得包含这些字符串的列表。

希望这对某人有帮助。


它可以与jpa存储库一起使用,以按该字段的内容过滤结果吗?
Please_Dont_Bully_Me_SO_Lords '19

1
@Please_Dont_Bully_Me_SO_Lords它不太适合该用例,因为您的数据将以“ foo; bar; foobar”的形式出现在数据库中。如果要查询数据,则可能需要使用ElementCollection + JoinTable。
Jonck van der Kogel

这也意味着SPLIT_CHAR您的字符串中不能有任何出现。
粉碎

@crush是正确的。虽然当然可以允许,例如在正确分隔字符串之后对字符串进行编码。但是我在这里发布的解决方案主要是针对简单的用例。对于更复杂的情况,您最好使用ElementCollection + JoinTable
Jonck van der Kogel

请修正您的代码。我将其视为“库代码”,因此应具有防御性,例如至少应具有null检查
ZZ 5

30

此答案是JPA2之前的实现,如果您使用的是JPA2,请参见上面的ElementCollection答案:

通常将模型对象内部的对象列表视为与另一个对象的“一对多”关系。但是,字符串(本身)不是一对多关系的允许客户,因为它没有ID。

因此,您应该将字符串列表转换为包含ID和字符串的Argument类JPA对象列表。您可能会使用String作为ID,这可能会在表中节省一些空间,既可以避免删除ID字段,也可以通过合并String相等的行来节省表空间,但是您将失去将参数重新设置为原始顺序的能力(因为您没有存储任何订购信息)。

或者,您可以将列表转换为@Transient,然后将另一个字段(argStorage)添加到类中,该字段可以是VARCHAR()或CLOB。然后,您需要添加3个函数:其中2个是相同的,并且应将字符串列表转换为单个字符串(在argStorage中),并以可以轻松分离它们的方式将它们分隔。用@PrePersist和@PreUpdate注释这两个函数(每个函数都做同样的事情)。最后,添加第三个函数,该函数将argStorage再次拆分为字符串列表,并在@PostLoad处进行注释。每当您要存储Command时,这将使您的CLOB使用字符串进行更新,并在将argStorage字段存储到数据库之前对其进行更新。

我仍然建议做第一种情况。这是以后建立真实关系的好习惯。


从ArrayList <String>更改为String(用逗号分隔的值)对我有用。
克里斯·戴尔2009年

2
但这迫使您在查询该字段时使用(恕我直言)丑陋的语句。
whiskeysierra

是的,就像我说的那样...做第一个选择,会更好。如果您无法完成任务,则选项2可以使用。
billjamesdev

15

根据Java Persistence with Hibernate

用注解映射值类型的集合[...]。在撰写本文时,它还不是Java Persistence标准的一部分

如果您使用的是Hibernate,则可以执行以下操作:

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

更新:请注意,这在JPA2中现在可用。



9

使用JPA的Hibernate实现时,我发现仅将类型声明为ArrayList而不是List即可使hibernate存储数据列表。

显然,与创建实体对象列表相比,这具有许多缺点。没有延迟加载,没有能力从其他对象引用列表中的实体,也许在构造数据库查询时更加困难。但是,当您要处理的是相当原始类型的列表时,您总是希望与实体一起快速获取它们,那么这种方法对我来说似乎很好。

@Entity
public class Command implements Serializable {

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

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


}

2
谢谢。与所有JPA实现一起使用时,Arraylist是可序列化的保存在BLOB字段中。这种方法的问题在于1)BLOB大小是固定的2)您可以搜索或索引数组元素3)只有了解Java序列化格式的客户端才能读取这些元素。
安德烈·弗朗西亚

如果您尝试使用此方法,@OneToMany @ManyToOne @ElementCollection它将Caused by: org.hibernate.AnnotationException: Illegal attempt to map a non collection as a @OneToMany, @ManyToMany or @CollectionOfElements在服务器启动时给您例外。因为休眠状态希望您使用集合接口。
Paramvir Singh Karwal,

9

我遇到了同样的问题,因此我投入了给出的可能解决方案,但最后我决定实施我的';'。字符串的分隔列表。

所以我有

// a ; separated list of arguments
String arguments;

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

这样,列表可以在数据库表中轻松读取/编辑。


1
这是完全有效的,但请考虑您的应用程序的增长和架构的演变。在不久的将来,您可能最终会转向基于实体的方法。
whiskeysierra 2010年

我同意,那是完全正确的。但是,我确实建议全面检查逻辑以及代码的实现。如果String arguments是访问权限列表,则具有特殊字符a separator可能会受到特权提升攻击的攻击。
唐潘

1
这确实是一个糟糕的建议,您的字符串可能包含;将破坏您的应用程序的字符串。
agilob

9

似乎没有一个答案探讨了@ElementCollection映射的最重要设置。

当您使用此批注映射列表时,让JPA / Hibernate自动生成表,列等时,它也会使用自动生成的名称。

因此,让我们分析一个基本示例:

@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;

}
  1. 基本@ElementCollection注释(您可以在其中定义已知fetchtargetClass首选项)
  2. @CollectionTable注释是非常有用的,当涉及到提供一个名称即会生成的表,以及像定义joinColumnsforeignKey的,indexesuniqueConstraints,等。
  3. @Column定义将存储varchar列表值的列的名称很重要。

生成的DDL创建将是:

-- 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)
);

4
我喜欢这个解决方案,因为它是唯一提出的解决方案,它提供了包括TABLE结构的完整描述并解释了为什么我们需要不同的注释。
朱利安·克朗格

6

好的,我知道有点晚了。但是对于那些勇敢的灵魂,他们会随着时间的流逝而看到这一点。

文档中所写:

@Basic:映射到数据库列的最简单类型。基本注释可以应用于以下任何类型的持久属性或实例变量:Java基本类型,枚举和实现java.io.Serializable的任何其他类型。

重要的部分是实现Serializable的类型

因此,到目前为止,最简单,最容易使用的解决方案就是使用ArrayList而不是List(或任何可序列化的容器):

@Basic
ArrayList<Color> lovedColors;

@Basic
ArrayList<String> catNames;

但是请记住,这将使用系统序列化,因此会带来一些代价,例如:

  • 如果序列化的对象模型将更改,则您可能无法还原数据

  • 为每个存储的元素添加少量开销。

简而言之

存储标志或少量元素非常简单,但我不建议您存储可能增长的数据。


尝试了此操作,但是sql表使数据类型成为tinyblob。这是否会使插入和检索字符串列表变得非常不便?还是jpa为您自动序列化和反序列化?
Dzhao

3

Thiago的答案是正确的,添加了更特定于问题的示例,@ ElementCollection将在数据库中创建新表,但不映射两个表,这意味着该集合不是实体的集合,而是简单类型(字符串等的集合) )或可嵌入元素的集合(用@Embeddable注释的类)。

这是持久化String列表的示例

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

这是保留自定义对象列表的示例

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

对于这种情况,我们需要使类Embeddable

@Embeddable
public class Car {
}

3

这是使用@Converter和StringTokenizer存储Set的解决方案。对@ jonck-van-der-kogel解决方案进行了更多检查。

在您的Entity类中:

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

1

我针对此问题的解决方法是将主键与外键分开。如果您使用eclipse并进行了上述更改,请记住刷新数据库浏览器。然后从表中重新创建实体。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.