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上获取。