ChatGPT解决这个技术问题 Extra ChatGPT

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

持久化具有 List 类型字段的实体的最聪明方法是什么?

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

O
Opal

使用一些 JPA 2 实现:它添加了一个类似于 Hibernate 的 @ElementCollection 注释,这正是您需要的。有一个示例 here

编辑

正如下面评论中提到的,正确的 JPA 2 实现是

javax.persistence.ElementCollection

@ElementCollection
Map<Key, Value> collection;

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


我的错误是也添加了 @OneToMany 注释......在删除它并离开 @ElementCollection 之后它起作用了
T
TylerH

如果有人正在寻找一种替代解决方案,您可以将字符串列表作为一个字段存储在数据库中,这就是我解决这个问题的方法。像这样创建一个转换器:

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

现在在您的实体上使用它,如下所示:

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

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


它会与 jpa 存储库一起使用,以按该字段的内容过滤结果吗?
@Please_Dont_Bully_Me_SO_Lords 它不太适合该用例,因为您的数据将作为“foo;bar;foobar”在数据库中。如果您想查询数据,那么 ElementCollection + JoinTable 可能是适合您情况的方法。
这也意味着您的字符串中不能出现任何 SPLIT_CHAR
@crush 这是正确的。当然,您可以通过例如在正确分隔字符串后对字符串进行编码来允许它。但是我在这里发布的解决方案主要是针对简单的用例;对于更复杂的情况,使用 ElementCollection + JoinTable 可能会更好
@Al-Mothafar 谢谢,很好的补充。由于我通常不喜欢返回 null,因此我编辑了代码以返回空字符串或空列表。但是当然,如果其他人更喜欢 null 而不是他们可以自由地这样做。
d
diogo

似乎没有一个答案探讨了 @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;
    
}

基本的@ElementCollection 注释(您可以在其中定义已知的 fetch 和 targetClass 首选项)@CollectionTable 注释在为将要生成的表命名时非常有用,以及像 joinColumns、foreignKey's、索引这样的定义, uniqueConstraints 等。@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)
);

我喜欢这个解决方案,因为它是唯一提出的解决方案,它提供了包括 TABLE 结构在内的完整描述并解释了为什么我们需要不同的注释。
b
billjamesdev

这个答案是在 JPA2 之前实现的,如果您使用的是 JPA2,请参阅上面的 ElementCollection 答案:

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

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

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

我仍然建议做第一个案例。这是以后真正建立关系的好习惯。


从 ArrayList 更改为使用逗号分隔值的 String 对我有用。
但这会迫使您在查询该字段时使用(恕我直言)丑陋的语句。
是的,正如我所说...做第一个选项,它会更好。如果你不能让自己去做,选项 2 可以工作。
J
Jaimin Patel

我们也可以使用这个。

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

可能加上@JoinTable。
t
toolkit

根据Java Persistence with Hibernate

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

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

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

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


佚名

在使用 JPA 的 Hibernate 实现时,我发现只需将类型声明为 ArrayList 而不是 List 就可以让 hibernate 存储数据列表。

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

@Entity
public class Command implements Serializable {

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

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


}

谢谢。这适用于所有 JPA 实现,Arraylist is Serializable 保存在 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 异常。因为hibernates希望你使用集合接口。
A
Anthony

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

所以我有

// a ; separated list of arguments
String arguments;

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

这样列表在数据库表中很容易阅读/编辑;


这是完全有效的,但请考虑您的应用程序的增长和模式的演变。在(不久的)将来的某个时候,您最终可能会切换到基于实体的方法。
我同意,这是完全有效的。但是,我确实建议全面审查代码的逻辑和实现。如果字符串 arguments 是访问权限列表,则具有特殊字符 separator 可能容易受到权限提升攻击。
这是一个非常糟糕的建议,您的字符串可能包含 ; 这会破坏您的应用程序。
也许最好检查一下您是否可以进行拆分return (arguments.contains(";")) ? Arrays.asList(arguments.split(";")) : null;
I
Inverce

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

documentation 中所写:

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

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

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

@Basic
ArrayList<Color> lovedColors;

@Basic
ArrayList<String> catNames;

但是请记住,这将使用系统序列化,因此需要付出一些代价,例如:

如果序列化对象模型发生变化,您可能无法恢复数据

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

简而言之

存储标志或少量元素非常简单,但我不建议使用它来存储可能会变大的数据。


试过这个,但 sql 表使数据类型成为一个 tinyblob。这不会使插入和检索字符串列表非常不方便吗?还是 jpa 会自动为您序列化和反序列化?
jpa 会自动为你反序列化
Z
Zia

蒂亚戈的回答是正确的,添加更具体到问题的示例,@ElementCollection 将在您的数据库中创建新表,但没有映射两个表,这意味着该集合不是实体的集合,而是简单类型(字符串等)的集合.) 或可嵌入元素的集合(使用@Embeddable 注释的类)。

这是持久化字符串列表的示例

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

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

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

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

@Embeddable
public class Car {
}

g
gosuer1921

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

在您的实体类中:

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

字符串集转换器:

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

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


r
razvang

我想要的是一种在表格列中持久保存一组字符串的简单方法。

我最终使用了 JSON,因为 MySQL 5.7+ 具有原生支持。这是我的解决方案

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

然后写一个很基础的转换器

@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

由于我的声誉还不足以评论@razvang 写的被低估的答案:

正如十多年前提出的这个问题一样,请记住,从那时起,世界大部分地区都发生了变化。我们现在拥有支持本机 JSON 列的数据库,并且可以使用此功能,而不是使用其他答案使用的单独实体、连接或自定义字符串到列表转换器。

不过,让我建议对@razvang 的出色答案进行两个纯可选的更改,根据您的具体情况,这可能会很有趣:

您可以省略 auto_apply = true 并将 @Convert(converter = .class) 添加到实体字段以控制何时使用转换器。与其在转换失败时抛出 RuntimeException ,不如在此处处理错误(例如传递一个空列表并编写一条日志消息)以使其失败有点优雅。