1. 概述

在这个教程中,我们将探讨如何在JPA中持久化类型为List<String>的属性。我们将探讨实现这一目标的各种方法,它们之间的差异,并通过一个示例来解释其优势。

2. 示例

我们使用实体库作为模型,它有一个自动生成的ID,一个名称,一个包含地址的List<String>,以及一个包含书名的List<String>

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

对于List<String>我们可以创建一个具有ID和字符串的第二个实体,并使用@OneToMany注解对其进行标记。我们将探讨两种更简单的JPA方法。

3. @ElementCollection

第一种选择是使用@ElementCollection。这个注解允许我们指定集合的目标类,例如String。此外,我们可以指定列表是否以懒加载或 eager 方式加载。默认值是懒加载。但为了简化示例,我们将值设置为 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<>();

我们还可以在想要访问列表的方法上使用@Transactional注解,或者在仓库方法上使用@Query("SELECT l FROM library l JOIN FETCH l.books WHERE l.id = (:id)")来避免LazyInitializationException(/hibernate-initialize-proxy-exception)。

这些注解导致以下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);

我们可以看到@CollectionTable设置了第二个表的名称和引用我们的图书馆表的列。此外,还正确创建了外键。因此,在这个方法中使用@ElementCollection我们省去了通常用于OneToMany链接所需的第二个实体。

4. 属性转换器

另一种选择是使用转换器。为此,我们需要实现所需对象的通用AttributeConverter。在我们的例子中,这是List<String>;理想的数据类型可能是String。在convertToDatabaseColumn(List<String> stringList)方法中,返回值是最终应存储在数据库中的对象的数据类型,参数是我们列表。

另一方面,convertToEntityAttribute(String string)方法定义了如何将列中的字符串转换回List<String>。在我们的示例中,我们使用分号“;”来分隔字符串:

@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)
@Column(name = "addresses", nullable = false)
private List<String> addresses = new ArrayList<>();

或者,我们可以将列表作为JSON字符串存储在列中。当我们决定使用AttributeConverter时,需要考虑列表的大小,因为它必须适应所选列的大小

在我们的例子中,它必须适合varchar(255)addresses列。在ElementCollection方法中,列表中的项目数量没有限制,每个项目仅受其自身varchar(255)列长度的限制。

5. 比较

接下来,我们将创建LibraryRepository并测试我们的解决方案:

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

现在,当我们执行代码时,我们将像平常一样向library实体添加列表项:

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

我们将得到以下输出:

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

正如我们所见,两种实现方式都按预期工作,并各有优势:

  • Element Collection

  • Converter

  • 默认加载类型

    • 懒加载
    • Eager
  • 列表限制

    • 无限制的列表项
    • 受列长度限制
    • 每个字符串限制
    • 受列长度和列表项数量限制
    • 创建额外的表
    • 不需要自己的表

6. 总结

在这篇文章中,我们讨论了在JPA中存储实体列表中List<String>的不同可能性。虽然我们展示了可能存在的限制以及各自方法的区别。

如往常一样,示例代码可在GitHub上获取。