【DB系列】JPA之delete使用姿势详解

文章目录
  1. I. 环境准备
    1. 1. 表准备
    2. 2. 项目配置
    3. 3. 数据准备
  2. II. Delete使用教程
    1. 1. 表关联POJO
    2. 2. Repository API声明
    3. 3. 使用姿势
      1. a. 根据主键id进行删除
      2. b. 条件判断删除
      3. c. 比较删除
    4. 4. 小结
  3. II. 其他
    1. 1. 一灰灰Blog

常见db中的四个操作curd,前面的几篇博文分别介绍了insert,update,接下来我们看下delete的使用姿势,通过JPA可以怎样删除数据

一般来讲是不建议物理删除(直接从表中删除记录)数据的,在如今数据就是钱的时代,更常见的做法是在表中添加一个表示状态的字段,然后通过修改这个字段来表示记录是否有效,从而实现逻辑删除;这么做的原因如下

  • 物理删除,如果出问题恢复比较麻烦
  • 无法保证代码一定准确,在出问题的时候,删错了数据,那就gg了
  • 删除数据,会导致重建索引
  • Innodb数据库对于已经删除的数据只是标记为删除,并不真正释放所占用的磁盘空间,这就导致InnoDB数据库文件不断增长,也会导致表碎片
  • 逻辑删除,保留数据,方便后续针对数据的挖掘或者分析

I. 环境准备

在开始之前,当然得先准备好基础环境,如安装测试使用mysql,创建SpringBoot项目工程,设置好配置信息等,关于搭建项目的详情可以参考前一篇文章

下面简单的看一下演示添加记录的过程中,需要的配置

1. 表准备

沿用前一篇的表,结构如下

1
2
3
4
5
6
7
8
9
10
CREATE TABLE `money` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
`money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0',
`create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

2. 项目配置

配置信息,与之前有一点点区别,我们新增了更详细的日志打印;本篇主要目标集中在添加记录的使用姿势,对于配置说明,后面单独进行说明

1
2
3
4
5
6
7
8
9
10
11
## DataSource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=
## jpa相关配置
spring.jpa.database=MYSQL
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=true
spring.jackson.serialization.indent_output=true
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

3. 数据准备

数据修改嘛,所以我们先向表里面插入两条数据,用于后面的操作

1
2
3
4
5
6
INSERT INTO `money` (`id`, `name`, `money`, `is_deleted`, `create_at`, `update_at`)
VALUES
(20, 'jpa 一灰灰5', 2323, 0, '2019-07-02 08:42:41', '2019-07-02 08:42:41'),
(21, 'jpa 一灰灰6', 2333, 0, '2019-07-02 08:42:41', '2019-07-02 08:42:41'),
(22, 'jpa 一灰灰7', 6666, 0, '2019-07-02 08:42:41', '2019-07-02 08:42:41'),
(23, 'jpa 一灰灰8', 2666, 0, '2019-07-02 08:42:41', '2019-07-02 08:42:41');

II. Delete使用教程

下面谈及到的删除,都是物理删除,可以理解为直接将某些记录从表中抹除掉(并不是说删了就完全没有办法恢复)针对CURD四种操作而言,除了read之外,另外三个insert,update,delete都会加写锁(一般来将会涉及到行锁和gap锁,从后面也会看到,这三个操作要求显示声明事物)

1. 表关联POJO

前面插入篇已经介绍了POJO的逐步创建过程,已经对应的注解含义,下面直接贴出成果

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
@Data
@DynamicUpdate
@DynamicInsert
@Entity
@Table(name = "money")
public class MoneyPO {
@Id
// 如果是auto,则会报异常 Table 'mysql.hibernate_sequence' doesn't exist
// @GeneratedValue(strategy = GenerationType.AUTO)
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Integer id;

@Column(name = "name")
private String name;

@Column(name = "money")
private Long money;

@Column(name = "is_deleted")
private Byte isDeleted;

@Column(name = "create_at")
@CreatedDate
private Timestamp createAt;

@Column(name = "update_at")
@CreatedDate
private Timestamp updateAt;

}

上面类中的几个注解,说明如下

  • @Data 属于lombok注解,与jpa无关,自动生成getter/setter/equals/hashcode/tostring等方法
  • @Entity, @Table jpa注解,表示这个类与db的表关联,具体匹配的是表 money
  • @Id @GeneratedValue 作用与自增主键
  • @Column表明这个属性与表中的某列对应
  • @CreateDate根据当前时间来生成默认的时间戳

2. Repository API声明

接下来我们新建一个api继承自CurdRepository,然后通过这个api来与数据库打交道

1
2
3
4
5
6
7
8
public interface MoneyDeleteRepository extends CrudRepository<MoneyPO, Integer> {
/**
* 查询测试
* @param id
* @return
*/
List<MoneyPO> queryByIdGreaterThanEqual(int id);
}

3. 使用姿势

先写一个用于查询数据的方法,用于校验我们执行删除之后,是否确实被删除了

1
2
3
4
private void showLeft() {
List<MoneyPO> records = moneyDeleteRepository.queryByIdGreaterThanEqual(20);
System.out.println(records);
}

在执行下面操作之前,先调用上面的,输出结果如

1
[MoneyPO(id=20, name=jpa 一灰灰5, money=2323, isDeleted=0, createAt=2019-07-02 08:42:41.0, updateAt=2019-07-02 08:42:41.0), MoneyPO(id=21, name=jpa 一灰灰6, money=2333, isDeleted=0, createAt=2019-07-02 08:42:41.0, updateAt=2019-07-02 08:42:41.0), MoneyPO(id=22, name=jpa 一灰灰7, money=6666, isDeleted=0, createAt=2019-07-02 08:42:41.0, updateAt=2019-07-02 08:42:41.0), MoneyPO(id=23, name=jpa 一灰灰8, money=2666, isDeleted=0, createAt=2019-07-02 08:42:41.0, updateAt=2019-07-02 08:42:41.0)]

a. 根据主键id进行删除

这种应该属于最常见的删除方式了,为了避免误删,通过精确的主键id来删除记录,是一个非常好的使用姿势,CrudRepository这个接口已经提供了对应的方法,所以我们可以直接使用

1
2
3
4
5
private void deleteById() {
// 直接根据id进行删除
moneyDeleteRepository.deleteById(21);
showLeft();
}

执行完毕之后,输出结果如下,对比前面的输出可以知道 id=21 的记录被删除了

1
[MoneyPO(id=20, name=jpa 一灰灰5, money=2323, isDeleted=0, createAt=2019-07-02 08:42:41.0, updateAt=2019-07-02 08:42:41.0), MoneyPO(id=22, name=jpa 一灰灰7, money=6666, isDeleted=0, createAt=2019-07-02 08:42:41.0, updateAt=2019-07-02 08:42:41.0), MoneyPO(id=23, name=jpa 一灰灰8, money=2666, isDeleted=0, createAt=2019-07-02 08:42:41.0, updateAt=2019-07-02 08:42:41.0)]

然后一个疑问自然而然的来了,如果这个id对应的记录不存在,会怎样?

把上面代码再执行一次,发现抛了异常

为什么会这样呢?我们debug进去,调用的实现是默认的 SimpleJpaRepository,其源码如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 类为: org.springframework.data.jpa.repository.support.SimpleJpaRepository
@Transactional
public void deleteById(ID id) {

Assert.notNull(id, ID_MUST_NOT_BE_NULL);

delete(findById(id).orElseThrow(() -> new EmptyResultDataAccessException(
String.format("No %s entity with id %s exists!", entityInformation.getJavaType(), id), 1)));
}

@Transactional
public void delete(T entity) {

Assert.notNull(entity, "The entity must not be null!");
em.remove(em.contains(entity) ? entity : em.merge(entity));
}

从源码可以看出,这个是先通过id进行查询,如果对应的记录不存在时,直接抛异常;当存在时,走remove逻辑;

如果我们希望删除一个不存在的数据时,不要报错,可以怎么办?

  • 自定义实现一个继承SimpleJpaRepository的类,覆盖删除方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Repository
@Transactional(readOnly = true)
public class MoneyDeleteRepositoryV2 extends SimpleJpaRepository<MoneyPO, Integer> {

@Autowired
public MoneyDeleteRepositoryV2(EntityManager em) {
this(JpaEntityInformationSupport.getEntityInformation(MoneyPO.class, em), em);
}

public MoneyDeleteRepositoryV2(JpaEntityInformation<MoneyPO, ?> entityInformation, EntityManager entityManager) {
super(entityInformation, entityManager);
}

public MoneyDeleteRepositoryV2(Class<MoneyPO> domainClass, EntityManager em) {
super(domainClass, em);
}

@Override
public void deleteById(Integer id) {
Optional<MoneyPO> rec = findById(id);
rec.ifPresent(super::delete);
}
}

然后再调用上面的方法就可以了,不演示具体的测试case了,源码可以到项目工程中查看 👉 源码

b. 条件判断删除

虽然根据id进行删除比较稳妥,但也无法避免某些情况下需要根据其他的字段来删除,比如我们希望删除名为 jpa 一灰灰7的数据,这时则需要我们在MoneyDeleteRepository新增一个方法

1
2
3
4
5
6
/**
* 根据name进行删除
*
* @param name
*/
void deleteByName(String name);

这里比较简单的提一下这个方法的命名规则,后面在查询这一篇会更加详细的说明;

  • delete 表示执行的是删除操作
  • By 表示根据某个字段来进行条件限定
  • Name 这个有POJO中的属性匹配

上面这个方法,如果翻译成sql,相当于 delete from money where name=xx

调用方式和前面一样,如下

1
2
3
4
private void deleteByName() {
moneyDeleteRepository.deleteByName("jpa 一灰灰7");
showLeft();
}

然后我们执行上面的测试,发现并不能成功,报错了

通过前面update的学习,知道需要显示加一个事物的注解,我们这里直接加在Repository

1
2
3
4
5
6
7
/**
* 根据name进行删除
*
* @param name
*/
@Transactional
void deleteByName(String name);

然后再次执行输出如下,这里我们把sql的日志也打印了

1
2
3
4
Hibernate: select moneypo0_.id as id1_0_, moneypo0_.create_at as create_a2_0_, moneypo0_.is_deleted as is_delet3_0_, moneypo0_.money as money4_0_, moneypo0_.name as name5_0_, moneypo0_.update_at as update_a6_0_ from money moneypo0_ where moneypo0_.name=?
Hibernate: delete from money where id=?
Hibernate: select moneypo0_.id as id1_0_, moneypo0_.create_at as create_a2_0_, moneypo0_.is_deleted as is_delet3_0_, moneypo0_.money as money4_0_, moneypo0_.name as name5_0_, moneypo0_.update_at as update_a6_0_ from money moneypo0_ where moneypo0_.id>=?
[MoneyPO(id=20, name=jpa 一灰灰5, money=2323, isDeleted=0, createAt=2019-07-02 08:42:41.0, updateAt=2019-07-02 08:42:41.0), MoneyPO(id=23, name=jpa 一灰灰8, money=2666, isDeleted=0, createAt=2019-07-02 08:42:41.0, updateAt=2019-07-02 08:42:41.0)]

从最终剩余的记录来看,name为jpa 一灰灰7的被删除了,再看一下前面删除的sql,会发现一个有意思的地方,deleteByName 这个方法,翻译成sql变成了两条

  • select * from money where name=xxx 先根据name查询记录
  • delete from money where id = xxx 根据前面查询记录的id,删除记录

c. 比较删除

接下来演示一个删除money在[2000,3000]区间的记录,这时我们新增的放入可以是

1
2
3
4
5
6
7
8
/**
* 根据数字比较进行删除
*
* @param low
* @param big
*/
@Transactional
void deleteByMoneyBetween(Long low, Long big);

通过方法命名也可以简单知道上面这个等同于sql delete from money where money between xxx and xxx

测试代码为

1
2
3
4
private void deleteByCompare() {
moneyDeleteRepository.deleteByMoneyBetween(2000L, 3000L);
showLeft();
}

输出日志

1
2
3
4
5
Hibernate: select moneypo0_.id as id1_0_, moneypo0_.create_at as create_a2_0_, moneypo0_.is_deleted as is_delet3_0_, moneypo0_.money as money4_0_, moneypo0_.name as name5_0_, moneypo0_.update_at as update_a6_0_ from money moneypo0_ where moneypo0_.money between ? and ?
Hibernate: delete from money where id=?
Hibernate: delete from money where id=?
Hibernate: select moneypo0_.id as id1_0_, moneypo0_.create_at as create_a2_0_, moneypo0_.is_deleted as is_delet3_0_, moneypo0_.money as money4_0_, moneypo0_.name as name5_0_, moneypo0_.update_at as update_a6_0_ from money moneypo0_ where moneypo0_.id>=?
[]

从拼接的sql可以看出,上面的逻辑等同于,先执行了查询,然后根据id一个一个进行删除….

4. 小结

我们通过声明方法的方式来实现条件删除;需要注意

  • 删除需要显示声明事物 @Transactional
  • 删除一个不存在的记录,会抛异常
  • 声明删除方法时,实际等同于先查询记录,然后根据记录的id进行精准删除

II. 其他

源码

相关博文

1. 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

一灰灰blog


打赏 如果觉得我的文章对您有帮助,请随意打赏。
分享到