
常見db中的四個(gè)操作curd,前面的幾篇博文分別介紹了insert,update,接下來我們看下delete的使用姿勢(shì),通過JPA可以怎樣刪除數(shù)據(jù)
一般來講是不建議物理刪除(直接從表中刪除記錄)數(shù)據(jù)的,在如今數(shù)據(jù)就是錢的時(shí)代,更常見的做法是在表中添加一個(gè)表示狀態(tài)的字段,然后通過修改這個(gè)字段來表示記錄是否有效,從而實(shí)現(xiàn)邏輯刪除;這么做的原因如下
- 物理刪除,如果出問題恢復(fù)比較麻煩
- 無法保證代碼一定準(zhǔn)確,在出問題的時(shí)候,刪錯(cuò)了數(shù)據(jù),那就gg了
- 刪除數(shù)據(jù),會(huì)導(dǎo)致重建索引
- Innodb數(shù)據(jù)庫對(duì)于已經(jīng)刪除的數(shù)據(jù)只是標(biāo)記為刪除,并不真正釋放所占用的磁盤空間,這就導(dǎo)致InnoDB數(shù)據(jù)庫文件不斷增長,也會(huì)導(dǎo)致表碎片
- 邏輯刪除,保留數(shù)據(jù),方便后續(xù)針對(duì)數(shù)據(jù)的挖掘或者分析
I. 環(huán)境準(zhǔn)備
在開始之前,當(dāng)然得先準(zhǔn)備好基礎(chǔ)環(huán)境,如安裝測(cè)試使用mysql,創(chuàng)建SpringBoot項(xiàng)目工程,設(shè)置好配置信息等,關(guān)于搭建項(xiàng)目的詳情可以參考前一篇文章
下面簡單的看一下演示添加記錄的過程中,需要的配置
1. 表準(zhǔn)備
沿用前一篇的表,結(jié)構(gòu)如下
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 '創(chuàng)建時(shí)間',
`update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時(shí)間',
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
2. 項(xiàng)目配置
配置信息,與之前有一點(diǎn)點(diǎn)區(qū)別,我們新增了更詳細(xì)的日志打?。槐酒饕繕?biāo)集中在添加記錄的使用姿勢(shì),對(duì)于配置說明,后面單獨(dú)進(jìn)行說明
## 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相關(guān)配置
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. 數(shù)據(jù)準(zhǔn)備
數(shù)據(jù)修改嘛,所以我們先向表里面插入兩條數(shù)據(jù),用于后面的操作
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使用教程
下面談及到的刪除,都是物理刪除,可以理解為直接將某些記錄從表中抹除掉(并不是說刪了就完全沒有辦法恢復(fù))針對(duì)CURD四種操作而言,除了read之外,另外三個(gè)insert,update,delete都會(huì)加寫鎖(一般來將會(huì)涉及到行鎖和gap鎖,從后面也會(huì)看到,這三個(gè)操作要求顯示聲明事物)
1. 表關(guān)聯(lián)POJO
前面插入篇已經(jīng)介紹了POJO的逐步創(chuàng)建過程,已經(jīng)對(duì)應(yīng)的注解含義,下面直接貼出成果
@Data
@DynamicUpdate
@DynamicInsert
@Entity
@Table(name = "money")
public class MoneyPO {
@Id
// 如果是auto,則會(huì)報(bào)異常 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;
}
上面類中的幾個(gè)注解,說明如下
-
@Data屬于lombok注解,與jpa無關(guān),自動(dòng)生成getter/setter/equals/hashcode/tostring等方法 -
@Entity,@Tablejpa注解,表示這個(gè)類與db的表關(guān)聯(lián),具體匹配的是表money -
@Id@GeneratedValue作用與自增主鍵 -
@Column表明這個(gè)屬性與表中的某列對(duì)應(yīng) -
@CreateDate根據(jù)當(dāng)前時(shí)間來生成默認(rèn)的時(shí)間戳
2. Repository API聲明
接下來我們新建一個(gè)api繼承自CurdRepository,然后通過這個(gè)api來與數(shù)據(jù)庫打交道
public interface MoneyDeleteRepository extends CrudRepository<MoneyPO, Integer> {
/**
* 查詢測(cè)試
* @param id
* @return
*/
List<MoneyPO> queryByIdGreaterThanEqual(int id);
}
3. 使用姿勢(shì)
先寫一個(gè)用于查詢數(shù)據(jù)的方法,用于校驗(yàn)我們執(zhí)行刪除之后,是否確實(shí)被刪除了
private void showLeft() {
List<MoneyPO> records = moneyDeleteRepository.queryByIdGreaterThanEqual(20);
System.out.println(records);
}
在執(zhí)行下面操作之前,先調(diào)用上面的,輸出結(jié)果如
[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. 根據(jù)主鍵id進(jìn)行刪除
這種應(yīng)該屬于最常見的刪除方式了,為了避免誤刪,通過精確的主鍵id來刪除記錄,是一個(gè)非常好的使用姿勢(shì),CrudRepository這個(gè)接口已經(jīng)提供了對(duì)應(yīng)的方法,所以我們可以直接使用
private void deleteById() {
// 直接根據(jù)id進(jìn)行刪除
moneyDeleteRepository.deleteById(21);
showLeft();
}
執(zhí)行完畢之后,輸出結(jié)果如下,對(duì)比前面的輸出可以知道 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)]
然后一個(gè)疑問自然而然的來了,如果這個(gè)id對(duì)應(yīng)的記錄不存在,會(huì)怎樣?
把上面代碼再執(zhí)行一次,發(fā)現(xiàn)拋了異常

為什么會(huì)這樣呢?我們debug進(jìn)去,調(diào)用的實(shí)現(xiàn)是默認(rèn)的 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));
}
從源碼可以看出,這個(gè)是先通過id進(jìn)行查詢,如果對(duì)應(yīng)的記錄不存在時(shí),直接拋異常;當(dāng)存在時(shí),走remove邏輯;
如果我們希望刪除一個(gè)不存在的數(shù)據(jù)時(shí),不要報(bào)錯(cuò),可以怎么辦?
- 自定義實(shí)現(xiàn)一個(gè)繼承
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);
}
}
然后再調(diào)用上面的方法就可以了,不演示具體的測(cè)試case了,源碼可以到項(xiàng)目工程中查看 ?? 源碼
b. 條件判斷刪除
雖然根據(jù)id進(jìn)行刪除比較穩(wěn)妥,但也無法避免某些情況下需要根據(jù)其他的字段來刪除,比如我們希望刪除名為 jpa 一灰灰7的數(shù)據(jù),這時(shí)則需要我們?cè)?code>MoneyDeleteRepository新增一個(gè)方法
/**
* 根據(jù)name進(jìn)行刪除
*
* @param name
*/
void deleteByName(String name);
這里比較簡單的提一下這個(gè)方法的命名規(guī)則,后面在查詢這一篇會(huì)更加詳細(xì)的說明;
-
delete表示執(zhí)行的是刪除操作 -
By表示根據(jù)某個(gè)字段來進(jìn)行條件限定 -
Name這個(gè)有POJO中的屬性匹配
上面這個(gè)方法,如果翻譯成sql,相當(dāng)于 delete from money where name=xx
調(diào)用方式和前面一樣,如下
private void deleteByName() {
moneyDeleteRepository.deleteByName("jpa 一灰灰7");
showLeft();
}
然后我們執(zhí)行上面的測(cè)試,發(fā)現(xiàn)并不能成功,報(bào)錯(cuò)了

通過前面update的學(xué)習(xí),知道需要顯示加一個(gè)事物的注解,我們這里直接加在Repository中
/**
* 根據(jù)name進(jìn)行刪除
*
* @param name
*/
@Transactional
void deleteByName(String name);
然后再次執(zhí)行輸出如下,這里我們把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,會(huì)發(fā)現(xiàn)一個(gè)有意思的地方,deleteByName 這個(gè)方法,翻譯成sql變成了兩條
-
select * from money where name=xxx先根據(jù)name查詢記錄 -
delete from money where id = xxx根據(jù)前面查詢記錄的id,刪除記錄
c. 比較刪除
接下來演示一個(gè)刪除money在[2000,3000]區(qū)間的記錄,這時(shí)我們新增的放入可以是
/**
* 根據(jù)數(shù)字比較進(jìn)行刪除
*
* @param low
* @param big
*/
@Transactional
void deleteByMoneyBetween(Long low, Long big);
通過方法命名也可以簡單知道上面這個(gè)等同于sql delete from money where money between xxx and xxx
測(cè)試代碼為
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可以看出,上面的邏輯等同于,先執(zhí)行了查詢,然后根據(jù)id一個(gè)一個(gè)進(jìn)行刪除....
4. 小結(jié)
我們通過聲明方法的方式來實(shí)現(xiàn)條件刪除;需要注意
- 刪除需要顯示聲明事物
@Transactional - 刪除一個(gè)不存在的記錄,會(huì)拋異常
- 聲明刪除方法時(shí),實(shí)際等同于先查詢記錄,然后根據(jù)記錄的id進(jìn)行精準(zhǔn)刪除
II. 其他
源碼
- 工程:https://github.com/liuyueyi/spring-boot-demo
- module: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-boot/102-jpa
相關(guān)博文
- mysql之鎖與事務(wù)詳解
- Spring學(xué)習(xí)之事務(wù)的使用姿勢(shì)
- Spring學(xué)習(xí)之事務(wù)管理與傳播屬性
- 190612-SpringBoot系列教程JPA之基礎(chǔ)環(huán)境搭建
- 190614-SpringBoot系列教程JPA之新增記錄使用姿勢(shì)
- 190623-SpringBoot系列教程JPA之update使用姿勢(shì)
1. 一灰灰Blog
盡信書則不如,以上內(nèi)容,純屬一家之言,因個(gè)人能力有限,難免有疏漏和錯(cuò)誤之處,如發(fā)現(xiàn)bug或者有更好的建議,歡迎批評(píng)指正,不吝感激
下面一灰灰的個(gè)人博客,記錄所有學(xué)習(xí)和工作中的博文,歡迎大家前去逛逛
- 一灰灰Blog個(gè)人博客 https://blog.hhui.top
- 一灰灰Blog-Spring專題博客 http://spring.hhui.top
