
在本文中,我们将看到如何使用 Hibernate 的 @SoftDelete 注解来为 JPA 实体启用软删除功能。
Tag 实体的映射如下:
@Entity
@Table(name = "tag")
@SoftDelete
public class Tag {
@Id
@GeneratedValue
private Long id;
@NaturalId
private String name;
}@SoftDelete 注解是在 Hibernate 6.4 中引入的,允许我们启用原生的软删除机制。
Post 实体的映射如下:
@Entity
@Table(name = "post")
@SoftDelete
public class Post {
@Id
private Long id;
private String title;
@OneToMany(
mappedBy = "post",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<PostComment> comments = new ArrayList<>();
@OneToOne(
mappedBy = "post",
cascade = CascadeType.ALL,
orphanRemoval = true,
fetch = FetchType.LAZY
)
private PostDetails details;
@ManyToMany
@JoinTable(
name = "post_tag",
joinColumns = @JoinColumn(name = "post_id"),
inverseJoinColumns = @JoinColumn(name = "tag_id")
)
@SoftDelete
private List<Tag> tags = new ArrayList<>();
public Post addComment(PostComment comment) {
comments.add(comment);
comment.setPost(this);
return this;
}
public Post removeComment(PostComment comment) {
comments.remove(comment);
comment.setPost(null);
return this;
}
public Post addDetails(PostDetails details) {
this.details = details;
details.setPost(this);
return this;
}
public Post removeDetails() {
this.details.setPost(null);
this.details = null;
return this;
}
public Post addTag(Tag tag) {
tags.add(tag);
return this;
}
}注意,Post 实体和 tags 集合都使用了 @SoftDelete Hibernate 注解。
前者用于软删除 post 表记录,后者用于软删除 post_tag 表行。
post_details 表通过 @MapsId 注解映射到 PostDetails 实体,该注解允许我们在父 post 和子 post_details 表之间重用主键,如下所示:
@Entity
@Table(name = "post_details")
@SoftDelete
public class PostDetails {
@Id
private Long id;
@Column(name = "created_on")
private Date createdOn;
@Column(name = "created_by")
private String createdBy;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id")
@MapsId
private Post post;
}post_comments 表映射到 PostComment 实体如下:
@Entity
@Table(name = "post_comment")
@SoftDelete
public class PostComment {
@Id
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@NotFound(action = NotFoundAction.EXCEPTION)
private Post post;
private String review;
}当我们使用 @SoftDelete 注解时,还需要在使用 FetchType.LAZY 策略的 @ManyToOne 关联上添加 @NotFound 注解。这是因为外键列的存在并不一定意味着父实体仍然存在,因为它可能已经被软删除。
Tag 实体上的 Hibernate @SoftDelete 注解假设我们创建了以下 Tag 实体:
entityManager.persist(
new Tag().setName("Java")
);
entityManager.persist(
new Tag().setName("JPA")
);
entityManager.persist(
new Tag().setName("Hibernate")
);
entityManager.persist(
new Tag().setName("Misc")
);当删除其中一个 Tag 实体时:
Tag miscTag = entityManager.unwrap(Session.class)
.bySimpleNaturalId(Tag.class)
.getReference("Misc");
entityManager.remove(miscTag);Hibernate 将执行以下 UPDATE 语句,将 deleted 列设置为 true:
UPDATE tag
SET
deleted = true
WHERE
id = 4 AND
deleted = false在 Tag 实体被软删除后,我们可以看到 JPQL 查询无法找到它:
Boolean exists = entityManager.createQuery("""
select count(t) = 1
from Tag t
where t.name = :name
""", Boolean.class)
.setParameter("name", "Misc")
.getSingleResult();
assertFalse(exists);在后台,Hibernate 在引用带有 @SoftDelete 注解的实体时,会在 WHERE 子句中添加 deleted = false 谓词:
SELECT
count(t.id)=1
FROM
tag t
WHERE
t.name = 'Misc' AND
t.deleted = falsePostDetails 实体上的 Hibernate @SoftDelete 注解考虑到我们有一个 Post 实体,它有一个一对一的 PostDetails 子实体:
Post post = new Post()
.setId(1L)
.setTitle("High-Performance Java Persistence");
post.addDetails(
new PostDetails()
.setCreatedOn(
Timestamp.valueOf(
LocalDateTime.of(2023, 7, 20, 12, 0, 0)
)
)
);
entityManager.persist(post);当通过父 Post 实体上的 removeDetails 方法删除 PostDetails 时:
Post post = entityManager.find(Post.class, 1L);
assertNotNull(post.getDetails());
post.removeDetails();Hibernate 生成以下 SQL UPDATE 语句,软删除 post_details 记录:
UPDATE
post_details
SET
deleted = true
WHERE
id = 1 AND
deleted = false在 PostDetails 实体被软删除后,我们将无法使用 find 方法获取它:
assertNull(entityManager.find(PostDetails.class, 1L));PostComment 实体上的 Hibernate @SoftDelete 注解假设我们有一个 Post 实体,它有两个 PostComment 子实体:
entityManager.persist(
new Post()
.setId(1L)
.setTitle("High-Performance Java Persistence")
.addComment(
new PostComment()
.setId(1L)
.setReview("Great!")
)
.addComment(
new PostComment()
.setId(2L)
.setReview("To read")
)
);当通过父 Post 实体上的 removeComment 方法删除一个 PostComment 实体时:
Post post = entityManager.find(Post.class, 1L);
assertEquals(2, post.getComments().size());
assertNotNull(entityManager.find(PostComment.class, 2L));
post.removeComment(post.getComments().get(1));Hibernate 生成以下 UPDATE 语句:
UPDATE
post_comment
SET
deleted = true
WHERE
id = 2 AND
deleted = false在 PostComment 被软删除后,Hibernate 会在尝试直接通过 find 方法或间接通过获取 comments 集合时隐藏 PostComment 实体:
Post post = entityManager.find(Post.class, 1L);
assertEquals(1, post.getComments().size());
assertNull(entityManager.find(PostComment.class, 2L));Post 实体上的 Hibernate @SoftDelete 注解如果我们创建一个包含所有关联的 Post 实体,如以下示例所示:
entityManager.persist(
new Post()
.setId(1L)
.setTitle("High-Performance Java Persistence")
.addComment(
new PostComment()
.setId(1L)
.setReview("Great!")
)
.addComment(
new PostComment()
.setId(2L)
.setReview("To read")
)
);当删除 Post 实体时:
Post post = entityManager.createQuery("""
select p
from Post p
join fetch p.comments
join fetch p.details
where p.id = :id
""", Post.class)
.setParameter("id", 1L)
.getSingleResult();
entityManager.remove(post);Hibernate 生成以下 SQL UPDATE 语句:
Query:["update post_tag set deleted=true where post_id=? and deleted=false"], Params:[(1)]
Query:["update post_comment set deleted=true where id=? and deleted=false"], Params:[(1)]
Query:["update post_comment set deleted=true where id=? and deleted=false"], Params:[(2)]
Query:["update post_details set deleted=true where id=? and deleted=false"], Params:[(1)]
Query:["update post set deleted=true where id=? and deleted=false"], Params:[(1)]注意,通过简单地使用 @SoftDelete Hibernate 注解,每个表记录都被软删除。
如果你喜欢这篇文章,我相信你也会喜欢我的书和视频课程。
与我们之前必须实现的机制相比,Hibernate @SoftDelete 注解非常容易使用。
如果你想受益于这种原生机制,那么你应该将你的 Hibernate 版本升级到 6.4 或更新版本。