EntityManager多对多关联子实体在连接表中的持久化问题
多对多关联下的实体持久化问题处理
问题背景
我有一个Book实体,它与Author和Genre均为ManyToMany关系,配有对应的连接表。Book类定义如下:
@Entity @Getter @NoArgsConstructor @AllArgsConstructor @ToString public class Book { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "book_seq") @GenericGenerator(name = "book_seq", strategy = "com.example.bookappjpa.dao.StringPrefixedSequenceIdGenerator", parameters = { @org.hibernate.annotations.Parameter(name = StringPrefixedSequenceIdGenerator.INCREMENT_PARAM, value = "1"), @org.hibernate.annotations.Parameter(name = StringPrefixedSequenceIdGenerator.VALUE_PREFIX_PARAMETER, value = "B"), @org.hibernate.annotations.Parameter(name = StringPrefixedSequenceIdGenerator.NUMBER_FORMAT_PARAMETER, value = "%08d"), }) private String id; @NaturalId private String title; @Column(name = "rank_no") private int rank; private int listId; private Double rating; private int pages; private LocalDate publishedOn; @ManyToMany() @JoinTable(name = "BOOK_AUTHOR", joinColumns = @JoinColumn(name = "BOOK_ID"), inverseJoinColumns = @JoinColumn(name = "AUTHOR_ID") ) private Set<Author> authors = new HashSet<>(); @ManyToMany @JoinTable(name = "BOOK_GENRE", joinColumns = @JoinColumn(name = "BOOK_ID"), inverseJoinColumns = @JoinColumn(name = "GENRE_ID")) private Set<Genre> genres = new HashSet<>(); public void setTitle(String title) { this.title = title; } public void setRankNo(int rankNo) { this.rank = rankNo; } public void setListId(int listId) { this.listId = listId; } public void setRating(Double rating) { this.rating = rating; } public void setPages(int pages) { this.pages = pages; } public void setPublishedOn(LocalDate publishedOn) { this.publishedOn = publishedOn; } public boolean addAuthor(Author author) { return this.authors.add(author); } public boolean addGenre(Genre genre) { return this.genres.add(genre); } }
当前持久化实现(待完善)
下面是我写的持久化方法,逻辑是通过自然ID查询实体是否存在,仅当不存在时才持久化,但代码存在缺陷:
@Transactional public void addBook(BookDTO book) { Session session = entityManager.unwrap(Session.class); Book b = session.bySimpleNaturalId(Book.class).load(book.getTitle()); if (null == b) { Book bookToDB = new Book(); bookToDB.setTitle(book.getTitle()); bookToDB.setPages(book.getPages()); bookToDB.setRating(book.getRating()); bookToDB.setRankNo(book.getRank()); bookToDB.setPublishedOn(book.getPublishedOn()); book.getAuthors().forEach(a -> { Author authorToDB = new Author(a.getName(), a.getAge(), a.getNationality()); if(null == session.bySimpleNaturalId(Author.class).load(a.getName())) { authorToDB.addBook(bookToDB); entityManager.persist(authorToDB); } }); book.getGenres().forEach(g -> { if(null == session.bySimpleNaturalId(Genre.class).load(g.getName())) { Genre genreToDB = new Genre(g.getName()); genreToDB.addBook(bookToDB); entityManager.persist(genreToDB); } }); entityManager.persist(bookToDB); } else { String message = "Book with title " + book.getTitle() + " already exists!"; log.info(message); throw new RuntimeException(message); } session.close(); }
现存问题
当前方案的问题很明显:当某本书籍与特定Author和Genre持久化后,后续新增另一本包含相同Author/Genre的书籍时,子实体的持久化会被跳过,导致连接表中没有生成Book与已有Author/Genre的关联记录。
我现在纠结两个点:
- 保留存在性检查:新
Book无法关联已有Author/Genre,连接表无数据 - 去掉检查:
Author/Genre表中会生成大量名称相同但主键不同的重复实体,完全不符合最佳实践
想知道生产级代码中有没有更成熟高效的方案处理这类场景。
示例数据
下面是两个包含同一作者的Book对象(第二个Book不会写入连接表BOOK_AUTHOR,因为作者已存在,当前代码不会处理关联):
{ "rank": 8, "title": "Clap When You Land", "authors": [ { "name": "Elizabeth Acevedo", "age": 0, "nationality": null } ], "rating": 0, "genres": [ { "name": "Young Adult" }, { "name": "Poetry" }, { "name": "Contemporary" }, { "name": "Fiction" }, { "name": "Audiobook" }, { "name": "LGBT" }, { "name": "Realistic Fiction" } ], "pages": 432, "publishedOn": "2020-05-05", "listId": 1 } { "rank": 18, "title": "The Poet X", "authors": [ { "name": "Elizabeth Acevedo", "age": 0, "nationality": null } ], "rating": 0, "genres": [ { "name": "Poetry" }, { "name": "Young Adult" }, { "name": "Contemporary" }, { "name": "Fiction" }, { "name": "Audiobook" }, { "name": "Realistic Fiction" }, { "name": "Romance" } ], "pages": 368, "publishedOn": "2018-03-06", "listId": 1 }
生产级解决方案
核心思路
不管Author/Genre是否存在,先通过自然ID查询获取实体:存在则直接复用,不存在则创建新实体;然后将获取到的实体与新Book建立关联,最后持久化Book(关联关系会自动同步到连接表)。
优化后的持久化代码
@Transactional public void addBook(BookDTO book) { Session session = entityManager.unwrap(Session.class); // 检查Book是否已存在,存在则抛出异常 Book existingBook = session.bySimpleNaturalId(Book.class).load(book.getTitle()); if (existingBook != null) { String message = "Book with title " + book.getTitle() + " already exists!"; log.info(message); throw new RuntimeException(message); } // 初始化要持久化的Book对象 Book bookToDB = new Book(); bookToDB.setTitle(book.getTitle()); bookToDB.setPages(book.getPages()); bookToDB.setRating(book.getRating()); bookToDB.setRankNo(book.getRank()); bookToDB.setPublishedOn(book.getPublishedOn()); bookToDB.setListId(book.getListId()); // 补充设置listId,原代码漏了 // 处理Author关联:复用已有或创建新实体,再关联到Book book.getAuthors().forEach(authorDTO -> { // 通过自然ID查询已有Author Author existingAuthor = session.bySimpleNaturalId(Author.class).load(authorDTO.getName()); Author authorToUse; if (existingAuthor == null) { // 不存在则创建并持久化 authorToUse = new Author(authorDTO.getName(), authorDTO.getAge(), authorDTO.getNationality()); entityManager.persist(authorToUse); } else { // 存在则直接复用 authorToUse = existingAuthor; } // 建立Book和Author的双向关联 bookToDB.addAuthor(authorToUse); authorToUse.addBook(bookToDB); }); // 处理Genre关联:逻辑同Author book.getGenres().forEach(genreDTO -> { Genre existingGenre = session.bySimpleNaturalId(Genre.class).load(genreDTO.getName()); Genre genreToUse; if (existingGenre == null) { genreToUse = new Genre(genreDTO.getName()); entityManager.persist(genreToUse); } else { genreToUse = existingGenre; } bookToDB.addGenre(genreToUse); genreToUse.addBook(bookToDB); }); // 持久化Book,关联关系会自动同步到连接表 entityManager.persist(bookToDB); }
关键优化点
- 复用优先,避免重复:通过自然ID查询确保同名称的
Author/Genre只会存在一条记录 - 强制关联所有实体:不管是新创建的还是复用的
Author/Genre,都要和当前Book建立关联,同时维护双向关联的另一方(保证ORM能正确同步连接表) - 事务原子性:整个操作在一个事务内完成,确保实体持久化和关联关系的一致性
- 补全遗漏字段:原代码漏了设置
listId,优化后补充完整
额外生产级建议
- 数据库层面加唯一约束:在
Author和Genre的name字段上添加唯一约束,即使代码出现疏漏,数据库也会抛出异常,避免重复数据@Entity public class Author { @NaturalId @Column(unique = true, nullable = false) private String name; // 其他字段、方法... } @Entity public class Genre { @NaturalId @Column(unique = true, nullable = false) private String name; // 其他字段、方法... } - 批量查询优化:如果一次导入大量书籍,可以先收集所有
Author/Genre的名称,批量查询数据库中已存在的记录,减少单次查询的次数,提升性能 - 避免手动关闭Session:Spring管理的Session会在事务结束后自动关闭,原代码中的
session.close()可能导致异常,建议移除
内容的提问来源于stack exchange,提问作者Soumik Banerjee
相关产品推荐
相关产品推荐

