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; ↩