Spring Data JPA 级联删除单向关联
简介
本文将带你了解,当无法依赖 CascadeType
机制来将状态转换从父实体传播到子实体时,如何使用 Spring Data JPA 级联删除单向关联,
Domain Model
假设我们的系统中有以下实体:
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
操作到 PostComment
、PostDetails
或 PostTag
子实体。
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
实体:
- 一个
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 单向关联?
现在,我们希望有一种方法来移除给定的 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_details
、post_comment
和 post_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("""
delete from UserVote
where comment.id in (
select id
from PostComment
where post.id = :postId
)
""")
@Modifying
void deleteAllByPostId(@Param("postId") Long postId);
}
接下来,使用单个批量 DELETE
语句调用 PostCommentRepository
中的 deleteAllByPostId
方法,删除 PostComment
子实体:
@Repository
public interface PostCommentRepository
extends BaseJpaRepository<PostComment, Long> {
@Query("""
delete from PostComment
where post.id = :postId
""")
@Modifying
void deleteAllByPostId(@Param("postId") Long postId);
}
之后,使用批量 DELETE
语句调用 PostTagRepository
中的 deleteAllByPostId
方法,删除 PostTag
子实体:
@Repository
public interface PostTagRepository
extends BaseJpaRepository<PostTag, PostTagId> {
@Query("""
delete from PostTag
where post.id = :postId
""")
@Modifying
void deleteAllByPostId(@Param("postId") 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/