相关文章推荐
爱运动的移动电源  ·  pyspark ...·  1 年前    · 
正直的桔子  ·  java - ...·  1 年前    · 
精彩文章免费看

JPA逻辑删除

一、前言

逻辑删除指的是修改数据的某个字段,使其表示为已删除状态,而非删除数据,保留该数据在数据库中,但是查询时不显示该数据(查询时过滤掉该数据)。

Spring Data Jpa没有现成的逻辑删除。本文列举了两个方案:
一是自己写一个Repository ,把SimpleJpaRepository 代码复制过来。再将里面的查询语句进行修改。同时实现逻辑删除的代码。
二是使用注解改写delete操作为update。

二、方案一

自定义BaseRepostitory。

2.1 BaseEntry

自定义一个BaseEntry,封装公用的字段 id createdDate updatedDate 等,代码如下:

package com.erbadagang.data.jpa.base;
import lombok.Data;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
 * @description Entry的基类,所有的entry都可以继承它。包括了常规的公共字段。
 * @ClassName: BaseEntry
 * @author: 郭秀志 jbcode@126.com
 * @date: 2020/7/14 17:11
@Data
@MappedSuperclass //这个注解表示在父类上面的,用来标识父类。
public class BaseEntry implements Serializable {
    private static final long serialVersionUID = 1521226766659220492L;
    //自增主键
    @GeneratedValue(strategy = GenerationType.IDENTITY,  // strategy 设置使用数据库主键自增策略;
            generator = "JDBC") // generator 设置插入完成后,查询最后生成的 ID 填充到该属性中。
    protected Long id;
    //创建时间
    @Temporal(TemporalType.TIMESTAMP)
    protected Date createdDate;
    //创建人
    protected String createdBy;
    //更新时间
    @Temporal(TemporalType.TIMESTAMP)
    protected Date updatedDate;
    //修改人
    protected String updatedBy;
    //逻辑删除(0 未删除、1 删除)
    protected Integer deleted = 0;
    // 版本号(用于乐观锁, 默认为 1)
    @Basic
    @Column(name = "version")
    protected Integer version;
@MappedSuperclass说明:
@MappedSuperclass注解表示在父类上面的,用来标识父类。
基于代码复用的思想,在项目开发中使用JPA的@MappedSuperclass注解将实体类的多个公共属性分别封装到不同的非实体类中。例如,数据库表中都需要id来表示编号,id是这些映射实体类的通用的属性,交给JPA统一生成主键id编号,那么使用一个父类来封装这些通用属性,并用@MappedSuperclas标识。
1.标注为@MappedSuperclass的类将不是一个完整的实体类,他将不会映射到数据库表也即是不会生成对应的表,但是他的属性都将映射到其子类的数据库字段中。
2.标注为@MappedSuperclass的类不能再标注@Entity或@Table注解,也无需实现序列化接口。

2.2 BaseRepostitory

package com.erbadagang.data.jpa.base;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.transaction.annotation.Transactional;
import java.io.Serializable;
import java.util.List;
 * @description 自己封装的带有逻辑删除的Repostitory基类,entity也必须继承BaseEntry。
 * @ClassName: BaseRepostitory
 * @author: 郭秀志 jbcode@126.com
 * @date: 2020/7/14 17:33
@NoRepositoryBean
public interface BaseRepostitory<T extends BaseEntry, ID extends Serializable> extends JpaRepository<T, ID> {
    @Override
    @Transactional(readOnly = true)
    @Query("select e from #{#entityName} e where e.deleted = 0")
    List<T> findAll();
    @Override
    @Transactional(readOnly = true)
    @Query("select e from #{#entityName} e where e.id in ?1 and e.deleted = 0")
    List<T> findAllById(Iterable<ID> iterable);
    @Override
    @Transactional(readOnly = true)
    @Query("select e from #{#entityName} e where e.id = ?1 and e.deleted = 0")
    T getOne(ID id);
    @Override
    @Transactional(readOnly = true)
    @Query("select count(e) from #{#entityName} e where e.deleted = 0")
    long count();
    @Override
    @Transactional(readOnly = true)
    default boolean existsById(ID id) {
        return getOne(id) != null;
    @Query("update #{#entityName} e set e.deleted = 1 where e.id = ?1")
    @Transactional
    @Modifying
    void logicDelete(ID id);
    @Transactional
    default void logicDelete(T entity) {
        logicDelete((ID) entity.getId());
    @Transactional
    default void logicDelete(Iterable<? extends T> entities) {
        entities.forEach(entity -> logicDelete((ID) entity.getId()));
    @Query("update #{#entityName} e set e.deleted = 1 ")
    @Transactional
    @Modifying
    void logicDeleteAll();

2.3 UserBaseEntity

package com.erbadagang.data.jpa.entity;
import com.erbadagang.data.jpa.base.BaseEntry;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
 * @description 继承自BaseEntry,很多公用属性不用定义,会自动生成表结构。
 * @ClassName: UserBaseEntity
 * @author: 郭秀志 jbcode@126.com
 * @date: 2020/7/14 17:56
@Data
@Entity
@Table(name = "t_user_base")
//User类的@Accessors(chain = true)注解代表可以链式写法来赋值。
@Accessors(chain = true)
public class UserBaseEntity extends BaseEntry {
    @Column(nullable = false)
    private String username;
     * 密码(明文)
     * ps:生产环境下,千万不要明文噢
    @Column(nullable = false)
    private String password;

继承自BaseEntry,很多公用属性不用定义使用父类的定义,会自动生成表t_user_base的字段。

2.4 UserBaseRepository

package com.erbadagang.data.jpa.repository;
import com.erbadagang.data.jpa.base.BaseRepostitory;
import com.erbadagang.data.jpa.entity.UserBaseEntity;
 * @description 继承自定义的BaseRepostitory。
 * @ClassName: UserBaseRepository
 * @author: 郭秀志 jbcode@126.com
 * @date: 2020/7/14 19:09
public interface UserBaseRepository extends BaseRepostitory<UserBaseEntity, Integer> {

UserBaseRepository使用的是自定义的UserBaseEntity。

2.5 简单测试

2.5.1 测试类

package com.erbadagang.data.jpa;
import com.erbadagang.data.jpa.entity.UserBaseEntity;
import com.erbadagang.data.jpa.repository.UserBaseRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.Assert;
import java.io.Serializable;
import java.util.Date;
import java.util.Optional;
import java.util.UUID;
 * @description 自定义的repository,可以进行逻辑删除
 * @ClassName: UserBaseRepositoryTest
 * @author: 郭秀志 jbcode@126.com
 * @date: 2020/7/14 19:38
@SpringBootTest(classes = JpaApplication.class)
public class UserBaseRepositoryTest {
    @Autowired
    private UserBaseRepository userRepository;
    @Test // 插入一条记录
    public void testSave() {
        UserBaseEntity user = new UserBaseEntity().
                setUsername(UUID.randomUUID().toString()).
                setPassword("guoxiuzhi");
        user.setCreatedDate(new Date());
        user.setCreatedBy("郭秀志");
        userRepository.save(user);
    @Test // 更新一条记录
    public void testUpdate() {
        // 先查询一条记录
        Optional<UserBaseEntity> userDO = userRepository.findById(1);
        Assert.isTrue(userDO.isPresent(), "记录不能为空");
        // 更新一条记录
        UserBaseEntity updateUser = userDO.get();
        updateUser.setPassword("updatedPwd");
        userRepository.save(updateUser);
    @Test // 根据 ID 编号,逻辑删除一条记录
    public void testLogicDelete() {
        userRepository.logicDelete(1);
     * getOne方法是自定义附加逻辑删除条件的查询方法。详见:{@link com.erbadagang.data.jpa.base.BaseRepostitory#getOne(Serializable)}  getOne方法使用了逻辑删除字段}
     * 本测试是根据 ID 编号,查询一条没被逻辑删除记录。
     * 输出的sql包括逻辑删除的条件:
     * Hibernate: select userbaseen0_.id as id1_1_, userbaseen0_.created_by as created_2_1_, userbaseen0_.created_date as created_3_1_, userbaseen0_.deleted as deleted4_1_, userbaseen0_.updated_by as updated_5_1_, userbaseen0_.updated_date as updated_6_1_, userbaseen0_.version as version7_1_, userbaseen0_.password as password8_1_, userbaseen0_.username as username9_1_ from t_user_base userbaseen0_ where userbaseen0_.id=? and userbaseen0_.deleted=0
    @Test
    public void testGetOne() {
        UserBaseEntity userBaseEntity = userRepository.getOne(1);
        System.out.println("userBaseEntity = " + userBaseEntity);
        System.out.println("CreatedBy属性值 = " + userBaseEntity.getCreatedBy());
        System.out.println("Id属性值 = " + userBaseEntity.getId());

2.5.2 自动生成新表

修改配置文件,使其根据Entity自动生成表:

# Hibernate 配置内容
spring.jpa.hibernate.ddl-auto=update

启动Application类,控制台可以看到创建表的信息:

Hibernate: create table t_user_base (id bigint not null auto_increment, created_by varchar(255), created_date datetime, deleted integer not null, updated_by varchar(255), updated_date datetime, version integer, password varchar(255) not null, username varchar(255) not null, primary key (id)) engine=InnoDB 2020-07-14 18:11:32.919 INFO 19052 --- [ task-1] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform] 2020-07-14 18:11:32.924 INFO 19052 --- [ task-1] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default' 2020-07-14 18:11:33.257 INFO 19052 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''

数据库可以看到创建的表结构,前面几个公用字段是来自定义在父类BaseEntry的属性。

3.1 新增删除标记

在上篇文章Spring Boot JPA 入门的表结构基础上,增加删除标记列。对应的创建表的 SQL 点击角标[1]

ALTER TABLE `orders_1`.`users` 
ADD COLUMN `deleted` tinyint(0) ZEROFILL COMMENT '删除标记,0未删除;1已删除。' AFTER `create_time`;

3.2 新建实体类UserWithSQLDeleteAnnotation

注意逻辑删除使用了@SQLDelete@Where注解。

package com.erbadagang.data.jpa.entity;
import lombok.Data;
import lombok.experimental.Accessors;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.Where;
import javax.persistence.*;
import java.util.Date;
@Data
@Entity
@Table(name = "users")
//User类的@Accessors(chain = true)注解代表可以链式写法来赋值。
@Accessors(chain = true)
//逻辑删除注解,删除sql变成了update
@SQLDelete(sql = "update users set deleted = 1 where id = ?")
//where条件带上了逻辑删除条件
@Where(clause = "deleted = 0")
public class UserWithSQLDeleteAnnotation {
     * 用户编号
    @GeneratedValue(strategy = GenerationType.IDENTITY,  // strategy 设置使用数据库主键自增策略;
            generator = "JDBC") // generator 设置插入完成后,查询最后生成的 ID 填充到该属性中。
    private Integer id;
    @Column(nullable = false)
    private String username;
     * 密码(明文)
     * ps:生产环境下,千万不要明文噢
    @Column(nullable = false)
    private String password;
     * 创建时间
    @Column(name = "create_time", nullable = false)
    private Date createTime;
    //逻辑删除(0 未删除、1 删除)
    private Integer deleted = 0;

3.3 UserRepositoryLogicalDel

package com.erbadagang.data.jpa.repository;
import com.erbadagang.data.jpa.entity.UserWithSQLDeleteAnnotation;
import org.springframework.data.jpa.repository.JpaRepository;
 * @description 官方版本的逻辑删除注解
 * 删除sql变成了update
 * @SQLDelete(sql = "update users set deleted = 1 where id = ?")
 * where条件带上了逻辑删除条件
 * @Where(clause = "deleted = 0")
 * 查看实体类注解:{@link com.erbadagang.data.jpa.entity.UserWithSQLDeleteAnnotation}
 * @ClassName: UserRepositoryLogiclDel
 * @author: 郭秀志 jbcode@126.com
 * @date: 2020/7/14 20:14
public interface UserRepositoryLogicalDel extends JpaRepository<UserWithSQLDeleteAnnotation, Integer> {

3.4 测试类

package com.erbadagang.data.jpa;
import com.erbadagang.data.jpa.entity.UserWithSQLDeleteAnnotation;
import com.erbadagang.data.jpa.repository.UserRepositoryLogicalDel;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Optional;
 * @description 官方版本的逻辑删除实现。
 * @ClassName: UserRepositoryLogicalDelTest
 * @author: 郭秀志 jbcode@126.com
 * @date: 2020/7/14 20:28
@SpringBootTest(classes = JpaApplication.class)
public class UserRepositoryLogicalDelTest {
    @Autowired
    private UserRepositoryLogicalDel userRepository;
    @Test // 根据 ID 编号,删除一条记录,会变成逻辑删除的update语句。
    public void testDelete() {
        userRepository.deleteById(5);
    @Test // 根据 ID 编号,查询一条记录
    public void testSelectById() {
        Optional<UserWithSQLDeleteAnnotation> userDO = userRepository.findById(1);
        System.out.println(userDO.get());

3.5 运行测试

运行前数据

testDelete()输出SQL:

Hibernate: select userwithsq0_.id as id1_2_0_, userwithsq0_.create_time as create_t2_2_0_, userwithsq0_.deleted as deleted5_2_0_, userwithsq0_.password as password3_2_0_, userwithsq0_.username as username4_2_0_ from users userwithsq0_ where userwithsq0_.id=? and ( userwithsq0_.deleted = 0)
Hibernate: update users set deleted = 1 where id = ?

testSelectById()输出:

Hibernate: select userwithsq0_.id as id1_2_0_, userwithsq0_.create_time as create_t2_2_0_, userwithsq0_.deleted as deleted5_2_0_, userwithsq0_.password as password3_2_0_, userwithsq0_.username as username4_2_0_ from users userwithsq0_ where userwithsq0_.id=? and ( userwithsq0_.deleted = 0)
UserWithSQLDeleteAnnotation(id=1, username=35e89c7b-bd4b-45d1-98d0-567db0181b48, password=oliverpwd, createTime=2020-07-14 12:46:24.0, deleted=0)

缺陷:本方案较前一方案相比,将所有的删除方法都覆盖成逻辑删除了。当我想要用到物理删除的时候。就不能用了。

本文源代码使用 Apache License 2.0开源许可协议,可从Gitee代码地址通过git clone命令下载到本地或者通过浏览器方式查看源代码。

CREATE TABLE users (
id int(11) NOT NULL AUTO_INCREMENT COMMENT '用户编号',
username varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '账号',
password varchar(32) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密码',
create_time datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (id),
UNIQUE KEY idx_username (username)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;