1. 概述

在这个教程中,我们将学习如何在JPA中从一对多关系中移除实体。

2. 多对多关系

多对多关系是通过一个额外的关联表连接两个实体的情况。为了有效地映射这些实体,我们应该遵循一些准则。

首先,在定义多对多关系时,应该考虑使用Set而不是List作为JPA实现,Hibernate并不以高效的方式从List中移除实体。

当使用List时,Hibernate会从关联表中移除所有实体,然后插入剩余的实体。这可能导致性能问题。通过使用Set,我们可以轻松避免这个问题。

其次,不应该在映射中使用CascadeType.REMOVE,以及因此的CascadeType.ALL

在多对多关系中,两个实体彼此独立。例如,假设我们有两个实体,PostCategory。当我们从Post实体中删除记录时,通常不希望同时删除关联的Category实体。使用CascadeType.REMOVE,JPA将移除所有关联的实体,包括可能仍然与其他实体关联的实体。

要在JPA中定义多对多关系,我们可以使用@ManyToMany注解。

多对多关联可以是单向或双向的。

3. 从单向@ManyToMany中移除实体

现在,让我们看看如何从单向多对多关联中移除实体。

首先,我们定义将在示例中使用的模型,PostCategory

@Entity
@Table(name = "post")
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "title")
    private String title;

    @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH})
    @JoinTable(name = "post_category",
      joinColumns = @JoinColumn(name = "post_id"),
      inverseJoinColumns = @JoinColumn(name = "category_id")
    )
    private Set<Category> categories = new HashSet<>();

    // getters and setters
}
@Entity
@Table(name = "category")
public class Category {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "name")
    private String name;

    // getters and setters
}

这里,我们定义了单向关系,因为不需要从Category表中使用关联。此外,我们声明Post实体负责管理关系。

现在,假设我们有两个Post实体,都与同一个Category关联:

Category category1 = new Category("JPA");
Category category2 = new Category("Persistence");

Post post1 = new Post("Many-to-Many Relationship");
post1.getCategories().add(category1);
post1.getCategories().add(category2);

Post post2 = new Post("Entity Manager");
post2.getCategories().add(category1);

entityManager.persist(post1);
entityManager.persist(post2);

接下来,从第一个Post实体中移除类别:

void givenEntities_whenRemove_thenRemoveAssociation() {
   Post post1 = entityManager.find(Post.class, 1L);
   Post post2 = entityManager.find(Post.class, 2L);
   Category category = entityManager.find(Category.class, 1L);

   post1.getCategories().remove(category);

   assertEquals(1, post1.getCategories().size());
   assertEquals(1, post2.getCategories().size());
}

结果,第一个PostCategory之间的关联被移除。此外,JPA没有删除关联的类别。

由于我们使用的是Set而不是List,JPA生成一条删除语句来从关联表中移除关联

4. 从双向@ManyToMany中移除实体

在双向关系中,可以从两个方面管理关联。

首先,让我们在BookAuthor实体之间创建双向关联:

@Entity
@Table(name = "book")
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "title")
    private String title;

    @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    @JoinTable(name = "book_author",
      joinColumns = @JoinColumn(name = "book_id"),
      inverseJoinColumns = @JoinColumn(name = "author_id")
    )
    private Set<Author> authors = new HashSet<>();

    // getters and setters
}
@Entity
@Table(name = "author")
public class Author {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "name")
    private String name;

    @ManyToMany(mappedBy = "authors")
    private Set<Book> books = new HashSet<>();

    // getters and setters
}

JPA会创建一个额外的关联表来存储两个实体之间的连接。这个表作为关联的子侧。因此,AuthorBook实体都代表父侧。

然而,尽管父母看起来相同,但它们并不一样。关系的所有权由mappedBy属性决定。非拥有者实体将具有mappedBy属性。

在我们的例子中,Book实体是拥有者。因此,它将传播关联的更改到关联表。

4.1. 从拥有者实体中移除

让我们看看如何从拥有者一侧移除实体。

我们将添加一个辅助方法到作为关联所有者的实体上。 在我们的案例中,这是Book实体:

public void removeAuthor(Author author){
   this.authors.remove(author);
   author.getBooks().remove(this);
}

删除作者时,我们可以调用辅助方法来移除关联:

void givenEntities_whenRemoveFromOwner_thenRemoveAssociation() {
   Author author = (Author) entityManager
     .createQuery("SELECT author from Author author where author.name = ?1")
     .setParameter(1, "Ralph Johnson")
     .getSingleResult();

   Book book1 = entityManager.find(Book.class, 1L);
   Book book2 = entityManager.find(Book.class, 2L);

   book1.removeAuthor(author);
   entityManager.persist(book1);

   assertEquals(3, book1.getAuthors().size());
   assertEquals(1, book2.getAuthors().size());
}

4.2. 从非拥有者实体中移除

从非关系拥有者实体中删除记录时,我们需要手动处理删除关联

for (Book book : author1.getBooks()) {
   book.getAuthors().remove(author1);
}
entityManager.remove(author1);

此外,让我们将代码放在方法中,并使用@PreRemove注解标记它:

@PreRemove
private void removeBookAssociations() {
   for (Book book: this.books) {
       book.getAuthors().remove(this);
   }
}

JPA将在删除实体之前执行方法内的所有内容。

最后,让我们创建一个测试,检查功能是否正常工作:

void givenEntities_whenRemoveFromNotOwner_thenRemoveAssociation() {
    Author author = (Author) entityManager
      .createQuery("SELECT author from Author author where author.name = ?1")
      .setParameter(1, "Ralph Johnson")
      .getSingleResult();
    Book book1 = entityManager.find(Book.class, 1L);
    Book book2 = entityManager.find(Book.class, 2L);

    entityManager.remove(author);

    assertEquals(3, book1.getAuthors().size());
    assertEquals(0, book2.getAuthors().size());
}

5. 总结

在这篇文章中,我们学习了如何在JPA中从多对多关系中移除实体。

如往常一样,示例代码可以在GitHub上找到。