51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

Spring Data JPA 级联删除单向关联

简介 {#简介}

本文将带你了解,当无法依赖 CascadeType 机制来将状态转换从父实体传播到子实体时,如何使用 Spring Data JPA 级联删除单向关联,

Domain Model {#domain-model}

假设我们的系统中有以下实体:

Post 和其单向的子实体

Post 实体是该实体层次结构的根(root),子实体只使用单向 @ManyToOne@OneToOne 关联,用于映射一个一对多或一对一表关系的底层外键。

Post root 实体如下:

@Entity
@Table(name = "post")
public class Post {
@Id
private Long id;

private String title;

// getter/setter 方法省略

}

请注意,没有双向的 @OneToMany@OneToOne 关联,可以允许我们从父级 Post 级联 DELETE 操作到 PostCommentPostDetailsPostTag 子实体。

PostComment 实体使用单向 @ManyToOne 关联映射 post_id 外键列:

@Entity
@Table(name = "post_comment")
public class PostComment {
@Id
@GeneratedValue
private Long id;

@ManyToOne(fetch = FetchType.LAZY) private Post post;

private String review;

//getter/setter 方法省略

}

PostDetails 实体使用单向 @OneToOne 关联映射 id 外键列:

@Entity
@Table(name = "post_details")
public class PostDetails {
@Id
private Long id;

@OneToOne(fetch = FetchType.LAZY) @MapsId private Post post;

@Column(name = "created_on") private LocalDateTime createdOn;

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

//getter/setter 方法省略

}

PostTag 实体使用单向 @ManyToOne 关联映射 post_id 外键列:

@Entity
@Table(name = "post_tag")
public class PostTag {
@EmbeddedId
private PostTagId id;

@ManyToOne(fetch = FetchType.LAZY) @MapsId("postId") private Post post;

@ManyToOne(fetch = FetchType.LAZY) @MapsId("tagId") private Tag tag;

@Column(name = "created_on") private Date createdOn = new Date();

//getter/setter 方法省略

}

由于 UserVote 实体是 PostComment 的子实体,因此 Post 实体不仅有直接的子关联。因此,它是 Post root 实体的孙子关联:

@Entity
@Table(name = "user_vote")
public class UserVote {
@Id
@GeneratedValue
private Long id;

@ManyToOne(fetch = FetchType.LAZY) private User user;

@ManyToOne(fetch = FetchType.LAZY) private PostComment comment;

private int score;

//getter/setter 方法省略

}

创建 Post 实体 {#创建-post-实体}

创建一个包含以下内容的 Post 实体:

  • 一个 PostDetails 子实体
  • 两个 PostComment 子实体,每个子实体都有一个 UserVote 实体
  • 三个 PostTag 子实体
Post post = new Post()
    .setId(1L)
    .setTitle("High-Performance Java Persistence");
postRepository.persist(post);

postDetailsRepository.persist( new PostDetails() .setCreatedBy("Vlad Mihalcea") .setPost(post) );

PostComment comment1 = new PostComment() .setReview("Best book on JPA and Hibernate!") .setPost(post);

PostComment comment2 = new PostComment() .setReview("A must-read for every Java developer!") .setPost(post);

postCommentRepository.persist(comment1); postCommentRepository.persist(comment2);

User alice = new User() .setId(1L) .setName("Alice");

User bob = new User() .setId(2L) .setName("Bob");

userRepository.persist(alice); userRepository.persist(bob);

userVoteRepository.persist( new UserVote() .setUser(alice) .setComment(comment1) .setScore(Math.random() > 0.5 ? 1 : -1) );

userVoteRepository.persist( new UserVote() .setUser(bob) .setComment(comment2) .setScore(Math.random() > 0.5 ? 1 : -1) );

Tag jdbc = new Tag().setName("JDBC"); Tag hibernate = new Tag().setName("Hibernate"); Tag jOOQ = new Tag().setName("jOOQ");

tagRepository.persist(jdbc); tagRepository.persist(hibernate); tagRepository.persist(jOOQ);

postTagRepository.persist(new PostTag(post, jdbc)); postTagRepository.persist(new PostTag(post, hibernate)); postTagRepository.persist(new PostTag(post, jOOQ));

如何使用 Spring Data JPA 级联 DELETE 单向关联? {#如何使用-spring-data-jpa-级联-delete-单向关联}

现在,我们希望有一种方法来移除给定的 Post 实体,如果我们在之前创建的 Post 实体上使用 PostRepository 的默认 deleteById 方法:

postRepository.deleteById(1L);

将会抛出 ConstraintViolationException

Caused by: org.postgresql.util.PSQLException:
ERROR:
    update or delete on table "post" violates
    foreign key constraint "fk_post_comment_post_id"
    on table "post_comment"
Detail:
    Key (id)=(1) is still referenced
    from table "post_comment".

抛出 ConstraintViolationException 的原因是 post 表记录仍被 post_detailspost_commentpost_tag 表中的子实体引用。

因此,我们需要确保在删除某个 Post 实体之前,先删除所有子实体。

为此,修改 PostRepository,继承 CustomPostRepository 接口:

@Repository
public interface PostRepository extends BaseJpaRepository<Post, Long>,
    CustomPostRepository<Long> {

}

CustomPostRepository 定义了 deleteById 方法,我们打算在自定义 JPA Repository 中覆写该方法,这样就可以将 DELETE 操作从 Post 实体级联到所有单向关联:

public interface CustomPostRepository<ID> {
void deleteById(ID postId);

}

有关自定义 Spring Data JPA Repository 的介绍,可以参考 官方文档

CustomPostRepository 接口如下:

public class CustomPostRepositoryImpl
        implements CustomPostRepository<Long> {
private final PostDetailsRepository postDetailsRepository;

private final UserVoteRepository userVoteRepository;

private final PostCommentRepository postCommentRepository;

private final PostTagRepository postTagRepository;

private final EntityManager entityManager;

public CustomPostRepositoryImpl( PostDetailsRepository postDetailsRepository, UserVoteRepository userVoteRepository, PostCommentRepository postCommentRepository, PostTagRepository postTagRepository, EntityManager entityManager) { this.postDetailsRepository = postDetailsRepository; this.userVoteRepository = userVoteRepository; this.postCommentRepository = postCommentRepository; this.postTagRepository = postTagRepository; this.entityManager = entityManager; }

@Override public void deleteById(Long postId) { Post post = null; PostDetails postDetails = postDetailsRepository.findWithPostById(postId);

if(postDetails != null) {
    postDetailsRepository.delete(postDetails);
    post = postDetails.getPost();
} else {
    post = entityManager.getReference(Post.class, postId);
}

userVoteRepository.deleteAllByPostId(postId);
postCommentRepository.deleteAllByPostId(postId);
postTagRepository.deleteAllByPostId(postId);

entityManager.remove(post);

}

}

deleteById 方法的实现方式是,我们可以清理所有指向 Post 实体的关联子表记录,无论是直接通过外键还是间接通过一系列外键引用,就像 user_vote 表记录的情况一样。

因此,首先加载相关的 PostDetails,如果找到这个子实体,删除它并获取对 Post 实体的引用。如果找不到子实体,就加载一个 Post 实体引用,在最后一个操作中使用它来删除这个实体。

第二步,通过批量 DELETE 语句调用 UserVoteRepository 中的 deleteAllByPostId 方法来删除 UserVotes

@Repository
public interface UserVoteRepository
        extends BaseJpaRepository<UserVote, Long> {
@Query(&quot;&quot;&quot;
    delete from UserVote
    where comment.id in (
        select id
        from PostComment
        where post.id = :postId
    )
    &quot;&quot;&quot;)
@Modifying
void deleteAllByPostId(@Param(&quot;postId&quot;) Long postId);

}

接下来,使用单个批量 DELETE 语句调用 PostCommentRepository 中的 deleteAllByPostId 方法,删除 PostComment 子实体:

@Repository
public interface PostCommentRepository
        extends BaseJpaRepository<PostComment, Long> {
@Query(&quot;&quot;&quot;
    delete from PostComment
    where post.id = :postId
    &quot;&quot;&quot;)
@Modifying
void deleteAllByPostId(@Param(&quot;postId&quot;) Long postId);

}

之后,使用批量 DELETE 语句调用 PostTagRepository 中的 deleteAllByPostId 方法,删除 PostTag 子实体:

@Repository
public interface PostTagRepository
        extends BaseJpaRepository<PostTag, PostTagId> {
@Query(&quot;&quot;&quot;
    delete from PostTag
    where post.id = :postId
    &quot;&quot;&quot;)
@Modifying
void deleteAllByPostId(@Param(&quot;postId&quot;) Long postId);

}

最后,在删除所有子单向关联后,就可以继续删除 root Post 实体了。

当调用 PostRepository 上的 deleteById 方法时:

postRepository.deleteById(1L);

Spring Data JPA 和 Hibernate 将执行以下 SQL 语句:

SELECT
    pd.post_id,
    pd.created_by,
    pd.created_on,
    p.id,
    p.title
FROM post_details pd
JOIN post p ON p.id = pd.post_id
WHERE pd.post_id = 1

DELETE FROM user_vote WHERE comment_id IN ( SELECT pd.id FROM post_comment pd WHERE pd.post_id = 1 )

DELETE FROM post_comment WHERE post_id = 1

DELETE FROM post_tag WHERE post_id = 1

DELETE FROM post_details WHERE post_id = 1

DELETE FROM post WHERE id = 1


参考:https://vladmihalcea.com/cascade-delete-unidirectional-associations-spring/

赞(4)
未经允许不得转载:工具盒子 » Spring Data JPA 级联删除单向关联