參考:
【本文內(nèi)容】

1. 在Repository中進(jìn)行刪除
Spring Data Repository提供了兩個(gè)方法進(jìn)行數(shù)據(jù)的刪除:
- delete(entity)
- deleteById(entityId)
2. 使用CascadeType.ALL或CascadeType.REMOVE
上述的delete方法,100%適用于如果entity中沒(méi)有關(guān)聯(lián)的時(shí)候。但如果entity中存在某些JPA的關(guān)聯(lián)(在數(shù)據(jù)庫(kù)中有外鍵存在),并且當(dāng)我們沒(méi)有添加CascadeType.ALL或CascadeType.REMOVE時(shí),會(huì)報(bào)錯(cuò)。
書店(bookStore)中有很多書(book),屬于一對(duì)多關(guān)系。例子數(shù)據(jù)模式參考:http://www.itdecent.cn/p/1c279b221527
@Entity
@Table(name = "book_store")
public class BookStore {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
@OneToMany(mappedBy = "bookStore")
private Set<Book> books = new HashSet<>();
}
@Entity
@Table(name = "book")
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "book_store_id")
private BookStore bookStore;
}
在BookStoreRepository中想要?jiǎng)h除,遇到錯(cuò)誤:
@Test
public void deleteTest() {
bookStoreRepository.deleteById(1);
}
報(bào)錯(cuò),原因是bookStore如果刪除了,但是它的id作為book.book_store_id的外鍵存在于book表中,所以遇到了數(shù)據(jù)庫(kù)級(jí)別的錯(cuò)誤:
2023-01-15 15:47:02.777 WARN 83246 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 1451, SQLState: 23000
2023-01-15 15:47:02.777 ERROR 83246 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : Cannot delete or update a parent row: a foreign key constraint fails...
想要fix這個(gè)問(wèn)題,可以在@OneToMany上加CascadeType.ALL或CascadeType.REMOVE,這樣在刪除bookStore.id=1的數(shù)據(jù)時(shí),也會(huì)同時(shí)刪除book表中所有bookStoreId=1的數(shù)據(jù):
@OneToMany(mappedBy = "bookStore", cascade = CascadeType.ALL)
private Set<Book> books = new HashSet<>();
cascade在英語(yǔ)單詞中的意思是串聯(lián)、級(jí)聯(lián)。CascadeType有很多值,如ALL,PERSIST,MERGE,REMOVE,REFRESH以及2.0后加進(jìn)來(lái)的DETACH。
其中ALL包含了另外5個(gè)值,即cascade=ALL等價(jià)于cascade={PERSIST, MERGE, REMOVE, REFRESH, DETACH}
CascadeType.REMOVE表示當(dāng)做刪除操作的時(shí)候,需要自動(dòng)刪除它下面關(guān)聯(lián)的entity list。在我們的例子中,即在刪除bookStore的時(shí)候,如果配置了CascadeType=ALL/REMOVE,則會(huì)自動(dòng)刪除它下面通過(guò)@OneToMany關(guān)聯(lián)的book的數(shù)據(jù)。
上述的測(cè)試,除了查詢的sql外,刪除相關(guān)的sql如下,因?yàn)橄嚓P(guān)的book有兩條,所以有三個(gè)刪除語(yǔ)句,可以看到使用CascadeType.ALL或CascadeType.REMOVE來(lái)刪除數(shù)據(jù)會(huì)導(dǎo)致N+1的問(wèn)題:
delete from book where id=?
delete from book where id=?
delete from book_store where id=?
3. 使用orphanRemoval
CascadeType.REMOVE vs orphanRemoval:
- https://www.baeldung.com/jpa-cascade-remove-vs-orphanremoval
- https://stackoverflow.com/questions/18813341/what-is-the-difference-between-cascadetype-remove-and-orphanremoval-in-jpa
CascadeType.REMOVE強(qiáng)調(diào)的是當(dāng)父entity刪除的時(shí)候,連帶著刪除子entity。發(fā)生在父entity 刪除的時(shí)候。
orphanRemoval = true,是JPA 2.0引入的,主要是提供了一種可以刪除orphan entity的方式。強(qiáng)調(diào)的是當(dāng)子entity脫離了父entity的關(guān)聯(lián)的時(shí)候,則刪除子entity。這里的脫離父entity,可以是父entity在save的時(shí)候發(fā)現(xiàn)子entity的list改變了(比如移掉了一個(gè)子entity),這時(shí)候這個(gè)子entity就會(huì)被刪除。發(fā)現(xiàn)了父entity 保存的時(shí)候。
@Entity
@Table(name = "book_store")
public class BookStore {
...
@OneToMany(mappedBy = "bookStore", cascade = CascadeType.PERSIST, orphanRemoval = true)
private Set<Book> books = new HashSet<>();
}
在數(shù)據(jù)庫(kù)中的數(shù)據(jù):bookStoreId = 1, 有2本書:
[Book{id=2, name='book-2'}, Book{id=1, name='book-1'}]
測(cè)試:
@Test
public void orphanRemovalTest() {
BookStore bookStore = bookStoreRepository.findById(1).get();
bookStore.setBooks(new HashSet<>());
bookStoreRepository.save(bookStore);
}
sql:
select
bookstore0_.id as id1_2_0_,
bookstore0_.name as name2_2_0_
from
book_store bookstore0_
where
bookstore0_.id=?
以下是save產(chǎn)生的sql,首先會(huì)先查詢一次:
select
bookstore0_.id as id1_2_0_,
bookstore0_.name as name2_2_0_
from
book_store bookstore0_
where
bookstore0_.id=?
如果有setBooks的調(diào)用,所以會(huì)查詢book表:
select
books0_.book_store_id as book_sto3_1_0_,
books0_.id as id1_1_0_,
books0_.id as id1_1_1_,
books0_.book_store_id as book_sto3_1_1_,
books0_.name as name2_1_1_
from
book books0_
where
books0_.book_store_id=?
因?yàn)槲覀儼裝ooks給清空了,那么原先兩個(gè)books(id=1,2)就變成了orphan entity了,因?yàn)槲覀兣渲昧?code>orphanRemoval=true,所以會(huì)自動(dòng)清除這兩個(gè)orphan books:
delete from book where id=?
delete from book where id=?
注:orphanRemoval=true,需要在CascadeType.PERSIST或是CascadeType.ALL下使用(因?yàn)?code>CascadeType.ALL包含了所有的的級(jí)聯(lián))。
4. 使用JPQL來(lái)批量刪除數(shù)據(jù)
在#2中的sql可以看到,使用CascadeType.ALL或CascadeType.REMOVE來(lái)刪除數(shù)據(jù)會(huì)導(dǎo)致N+1的問(wèn)題。
也就是說(shuō)JPA在刪除的時(shí)候,是逐條刪除的。在數(shù)據(jù)量多的情況下這樣比較影響效率,理想狀態(tài)下,我們希望使用一條sql來(lái)刪除相關(guān)聯(lián)的數(shù)據(jù)(比如:delete from book where book_store_id = ?)。
這時(shí)候可以用JPQL來(lái)刪除數(shù)據(jù),如果repository中不用@Transactional進(jìn)行標(biāo)記,可以在service調(diào)用的時(shí)候,在事務(wù)context中進(jìn)行調(diào)用:
public interface BookStoreRepository extends JpaRepository<BookStore, Integer> {
@Modifying
@Transactional
@Query("DELETE FROM BookStore b WHERE b.id = :id")
void deleteByIdWithJPQL(int id);
}
public interface BookRepository extends JpaRepository<Book, Integer> {
@Modifying
@Transactional
@Query("DELETE FROM Book b WHERE b.bookStore.id = :bookStoreId")
void deleteInBulkByBookStoreId(int bookStoreId);
}
測(cè)試:
@Test
public void deleteWithJPQLTest() {
bookRepository.deleteInBulkByBookStoreId(1);
bookStoreRepository.deleteByIdWithJPQL(1);
}
相關(guān)sql:
delete from book where book_store_id=?
delete from book_store where id=?
需要先刪除子entity(即book),如果先刪除bookStore,那么因?yàn)橥怄I還留在book表中,導(dǎo)致報(bào)錯(cuò)。
5. 邏輯刪除
很多時(shí)候,我們的系統(tǒng)除了直接刪除數(shù)據(jù)外,一般會(huì)選擇一列來(lái)記錄數(shù)據(jù)的狀態(tài),比如DELETED或是STATUS。另外可能還需要兩列來(lái)記錄創(chuàng)建時(shí)間以及最近一次的修改時(shí)間,即:CREATED_TIME,UPDATED_TIME。
@Entity
@Table(name = "book_store")
public class BookStore {
...
@OneToMany(mappedBy = "bookStore", cascade = CascadeType.ALL)
private Set<Book> books = new HashSet<>();
private boolean deleted;
@CreationTimestamp
private LocalDateTime createdTime;
@UpdateTimestamp
private LocalDateTime updatedTime;
}
@Entity
@Table(name = "book")
public class Book {
...
@ManyToOne
@JoinColumn(name = "book_store_id")
private BookStore bookStore;
private boolean deleted;
@CreationTimestamp
private LocalDateTime createdTime;
@UpdateTimestamp
private LocalDateTime updatedTime;
}
在BookStoreServiceImpl類是加入方法:
@Service
public class BookStoreServiceImpl implements BookStoreService {
@Transactional
public void softDeleteManually(int id) {
BookStore bookStore = bookStoreRepository.findById(id).get();
bookStore.setDeleted(true);
bookStore.getBooks().forEach(book -> book.setDeleted(true));
bookStoreRepository.save(bookStore);
}
}
測(cè)試:
@Test
public void test() {
bookStoreService.softDeleteManually(1);
}
bookStore id=1,在數(shù)據(jù)庫(kù)中有2條book數(shù)據(jù),除了select相關(guān)的sql,會(huì)產(chǎn)生3條update的操作:
update book_store
set
created_time=?,
deleted=?,
name=?,
updated_time=?
where id=?
update book
set
book_store_id=?,
created_time=?,
deleted=?,
name=?,
updated_time=?
where id=?
update book
set // 同上,略
where id=?
@CreationTimestamp和@UpdateTimestamp從Hibernate 4.3版本后被加入。在查詢的時(shí)候,我們需要按deleted=false的條件取數(shù)據(jù):
public interface BookStoreRepository extends JpaRepository<BookStore, Integer> {
List<BookStore> findByNameContainingAndDeletedFalse(String name);
}
6. @Where和@SQLDelete邏輯刪除
@Where和@SQLDelete可以被定義在entity類上,從而進(jìn)行邏輯刪除。
可以使用@Where(clause = ...)來(lái)排除所有已經(jīng)被“刪除”的數(shù)據(jù)。使用@SQLDelete(sql = ...)來(lái)定義刪除的時(shí)候需要執(zhí)行的語(yǔ)句。
具體來(lái)看:
@SQLDelete(sql = "UPDATE book_store SET deleted = 1 WHERE id = ?")
@Where(clause = "deleted = 0")
@Entity
@Table(name = "book_store")
public class BookStore {
...
}
@SQLDelete(sql = "UPDATE book SET deleted = 1 WHERE id = ?")
@Where(clause = "deleted = 0")
@Entity
@Table(name = "book")
public class Book {
...
}
測(cè)試:
@Test
public void deleteTest() {
bookStoreRepository.deleteById(1);
}
相關(guān)的sql:
先查詢bookStore的數(shù)據(jù),按id和deleted=0查詢:
select // 略
from book_store bookstore0_
where
bookstore0_.id=?
and (
bookstore0_.deleted = 0
)
再查詢book的數(shù)據(jù),也會(huì)自動(dòng)加上deleted=0的條件:
select
books0_.book_store_id as book_sto6_1_0_,
books0_.id as id1_1_0_,
books0_.id as id1_1_1_,
books0_.book_store_id as book_sto6_1_1_,
books0_.created_time as created_2_1_1_,
books0_.deleted as deleted3_1_1_,
books0_.name as name4_1_1_,
books0_.updated_time as updated_5_1_1_
from book books0_
where
(
books0_.deleted = 0
)
and books0_.book_store_id=?
開(kāi)始做“刪除”操作,其實(shí)是邏輯刪除,所以是update語(yǔ)句,因?yàn)閎ookStore下有兩個(gè)book數(shù)據(jù),所以會(huì)有3條update語(yǔ)句(逐個(gè)更新):
UPDATE book SET deleted = 1 WHERE id = ?
UPDATE book SET deleted = 1 WHERE id = ?
UPDATE book_store SET deleted = 1 WHERE id = ?