简介 {#简介}
本文将带你了解,当无法依赖 CascadeType
机制来将状态转换从父实体传播到子实体时,如何使用 Spring Data JPA 级联删除单向关联,
Domain Model {#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-实体}
创建一个包含以下内容的 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_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/