SpringBoot系列教程JPA之delete使用姿势详解

原文: 190702-SpringBoot系列教程JPA之delete使用姿势详解

image

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

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

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

I. 环境准备

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

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

1. 表准备

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

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. 项目配置

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

## 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. 数据准备

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

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');
image

II. Delete使用教程

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

1. 表关联POJO

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

@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来与数据库打交道

public interface MoneyDeleteRepository extends CrudRepository<MoneyPO, Integer> {
    /**
     * 查询测试
     * @param id
     * @return
     */
    List<MoneyPO> queryByIdGreaterThanEqual(int id);
}

3. 使用姿势

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

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

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

[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这个接口已经提供了对应的方法,所以我们可以直接使用

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

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

[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对应的记录不存在,会怎样?

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

image

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

// 类为: 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的类,覆盖删除方法
@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新增一个方法

/**
 * 根据name进行删除
 *
 * @param name
 */
void deleteByName(String name);

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

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

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

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

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

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

image

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

/**
 * 根据name进行删除
 *
 * @param name
 */
@Transactional
void deleteByName(String name);

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

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]区间的记录,这时我们新增的放入可以是

/**
 * 根据数字比较进行删除
 *
 * @param low
 * @param big
 */
@Transactional
void deleteByMoneyBetween(Long low, Long big);

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

测试代码为

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

输出日志

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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,719评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,337评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,887评论 0 324
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,488评论 1 266
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,313评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,284评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,672评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,346评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,644评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,700评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,457评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,316评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,706评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,261评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,648评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,859评论 2 335

推荐阅读更多精彩内容