Hibernate SoftDelete 注解


文章目录
  • Hibernate SoftDelete 注解
  • 1、简介
  • 2、软删除模型
  • 3、测试 `Tag` 实体上的 Hibernate `@SoftDelete` 注解
  • 4、测试 `PostDetails` 实体上的 Hibernate `@SoftDelete` 注解
  • 5、测试 `PostComment` 实体上的 Hibernate `@SoftDelete` 注解
  • 6、测试 `Post` 实体上的 Hibernate `@SoftDelete` 注解
  • 7、结论


1、简介

在本文中,我们将看到如何使用 Hibernate 的 @SoftDelete 注解来为 JPA 实体启用软删除功能。

2、软删除模型

Tag 实体的映射如下:

@Entity
@Table(name = "tag")
@SoftDelete
public class Tag {
    
    @Id
    @GeneratedValue
    private Long id;
    
    @NaturalId
    private String name;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

@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;
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.

注意,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;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

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;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

当我们使用 @SoftDelete 注解时,还需要在使用 FetchType.LAZY 策略的 @ManyToOne 关联上添加 @NotFound 注解。这是因为外键列的存在并不一定意味着父实体仍然存在,因为它可能已经被软删除。

3、测试 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")
);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

当删除其中一个 Tag 实体时:

Tag miscTag = entityManager.unwrap(Session.class)
    .bySimpleNaturalId(Tag.class)
    .getReference("Misc");

entityManager.remove(miscTag);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

Hibernate 将执行以下 UPDATE 语句,将 deleted 列设置为 true

UPDATE tag
SET
    deleted = true
WHERE
    id = 4 AND
    deleted = false
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

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);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

在后台,Hibernate 在引用带有 @SoftDelete 注解的实体时,会在 WHERE 子句中添加 deleted = false 谓词:

SELECT
    count(t.id)=1
FROM
    tag t
WHERE
    t.name = 'Misc' AND
    t.deleted = false
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

4、测试 PostDetails 实体上的 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);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

当通过父 Post 实体上的 removeDetails 方法删除 PostDetails 时:

Post post = entityManager.find(Post.class, 1L);
assertNotNull(post.getDetails());

post.removeDetails();
  • 1.
  • 2.
  • 3.
  • 4.

Hibernate 生成以下 SQL UPDATE 语句,软删除 post_details 记录:

UPDATE
    post_details
SET
    deleted = true
WHERE
    id = 1 AND
    deleted = false
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

PostDetails 实体被软删除后,我们将无法使用 find 方法获取它:

assertNull(entityManager.find(PostDetails.class, 1L));
  • 1.

5、测试 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")
        )
);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

当通过父 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));
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

Hibernate 生成以下 UPDATE 语句:

UPDATE
    post_comment
SET
    deleted = true
WHERE
    id = 2 AND
    deleted = false
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

PostComment 被软删除后,Hibernate 会在尝试直接通过 find 方法或间接通过获取 comments 集合时隐藏 PostComment 实体:

Post post = entityManager.find(Post.class, 1L);
assertEquals(1, post.getComments().size());

assertNull(entityManager.find(PostComment.class, 2L));
  • 1.
  • 2.
  • 3.
  • 4.

6、测试 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")
        )
);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

当删除 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);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

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)]
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

注意,通过简单地使用 @SoftDelete Hibernate 注解,每个表记录都被软删除。

如果你喜欢这篇文章,我相信你也会喜欢我的书和视频课程。

7、结论

与我们之前必须实现的机制相比,Hibernate @SoftDelete 注解非常容易使用。

如果你想受益于这种原生机制,那么你应该将你的 Hibernate 版本升级到 6.4 或更新版本。