spring boot實現(xiàn)軟刪除的示例代碼
本文開發(fā)環(huán)境:spring-boot:2.0.3.RELEASE + java1.8
WHY TO DO
軟刪除:即不進行真正的刪除操作。由于我們實體間的約束性(外鍵)的存在,刪除某些數(shù)據(jù)后,將導(dǎo)致其它的數(shù)據(jù)不完整。比如,計算機1801班的教師是張三,此時,我們?nèi)绻褟埲齽h除掉,那么在查詢計算機1801班時,由于張三不存了,所以就會報EntityNotFound的錯誤。當然了,在有外鍵約束的數(shù)據(jù)庫中,如果張三是1801班的教師,那么我們直接刪除張三將報一個約束性的異常。也就是說:直接刪除張三這個行為是無法執(zhí)行的。
但有些時候,我們的確有刪除的需求。比如說,有個員工離職了,然后我們想在員工管理中刪除該員工。但是:該員工由于在數(shù)據(jù)表中存在歷史記錄。比如我們記錄了17年第二學(xué)期的數(shù)據(jù)結(jié)構(gòu)是張三教的。那么,由于約束性的存在,刪除張三時就會報約束性錯誤。也就是說:出現(xiàn)了應(yīng)該刪除,但卻刪除不了的尷尬。
這就用到了本文所提到的軟刪除,所謂軟刪除,就是說我并不真正的刪除數(shù)據(jù)表中的數(shù)據(jù),而是在給這條記錄加一個是否刪除的標記。
spring jpa是支持軟刪除的,我們可以找到較多質(zhì)量不錯的文章來解決這個問題。大體步驟為:1. 加入@SqlDelete("update xxxx set deleted = 1 where id = ?")。2.加入@Where(clause = "deleted = false")的注解。但這個解決方案并不完美。具體表現(xiàn)在:
我們還以張三是1801班的教師舉例。
加入注解后,我們的確是可以做到可以成功的刪除張三了,刪除操作后,我們查看數(shù)據(jù)表,張三的記錄的確也還在。但此時,如果我們進行all或是page查詢,將得到一個500 EntiyNotFound錯誤。這是由于在all查詢時,jpa自動加入了@Where中的的查詢參數(shù),由于關(guān)聯(lián)數(shù)據(jù)的deleted = true,進而發(fā)生了未找到關(guān)聯(lián)實體的異常。
但事實是:實體雖然被刪除,但實際在還在,我們想將其應(yīng)用到關(guān)聯(lián)查詢中。并不希望其發(fā)生500 EntiyNotFound異常。
本文的方案實現(xiàn)了:
- 可以成功的實現(xiàn)軟刪除。
- 再進行關(guān)聯(lián)刪除時,不發(fā)生500 EntiyNotFound錯誤。
解決方案
- 即然500是由于注解@Where(clause = "deleted = false")引起的,那么我們棄用該注解。
- 我們需要在查詢時,加入deleted = false的查詢。那么我們新建一個接口,并繼承jpa的CrudRepository,然后重寫其查詢相關(guān)的方法。在重寫過程中,加入deleted = false的查詢條件。
實施

初始化
新建ClazzTest, Clazz, Teacher三個實體,新建BaseEntity抽象類實體。其中ClazzTest用于演示使用@Where(clause = "deleted = false")注解時發(fā)生的異常。
package com.mengyunzhi.springbootsamplecode.softdelete.entity;
import javax.persistence.MappedSuperclass;
@MappedSuperclass
public abstract class BaseEntity {
private Boolean deleted = false;
// setter and getter
}
package com.mengyunzhi.springbootsamplecode.softdelete.entity;
import org.hibernate.annotations.SQLDelete;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
/**
* 班級
*/
@Entity
@SQLDelete(sql = "update `klass` set deleted = 1 where id = ?")
public class Klass extends BaseEntity {
@Id
@GeneratedValue
private Long id;
private String name;
// setter and getter
}
@Entity
@SQLDelete(sql = "update `klass_test` set deleted = 1 where id = ?")
@Where(clause = "deleted = false")
public class KlassTest extends BaseEntity {
@Id @GeneratedValue
private Long id;
private String name;
}
重寫CrudRepository
package com.mengyunzhi.springbootsamplecode.softdelete.core;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.NoRepositoryBean;
import javax.transaction.Transactional;
import java.util.Optional;
/**
* 應(yīng)用軟刪除
* 默認的@Where(clause = "deleted = 0")會導(dǎo)致hibernate內(nèi)部進行關(guān)聯(lián)查詢時,發(fā)生ObjectNotFound的異常
* 在此重新定義接口
* 參考:https://stackoverflow.com/questions/19323557/handling-soft-deletes-with-spring-jpa/22202469
* @author 河北工業(yè)大學(xué) 夢云智軟件開發(fā)團隊
*/
@NoRepositoryBean
public interface SoftDeleteCrudRepository<T, ID> extends CrudRepository<T, ID> {
@Override
@Transactional
@Query("select e from #{#entityName} e where e.id = ?1 and e.deleted = false")
Optional<T> findById(ID id);
@Override
@Transactional
default boolean existsById(ID id) {
return findById(id).isPresent();
}
@Override
@Transactional
@Query("select e from #{#entityName} e where e.deleted = false")
Iterable<T> findAll();
@Override
@Transactional
@Query("select e from #{#entityName} e where e.id in ?1 and e.deleted = false")
Iterable<T> findAllById(Iterable<ID> ids);
@Override
@Transactional
@Query("select count(e) from #{#entityName} e where e.deleted = false")
long count();
}
新建倉庫類
繼承spring的CrudRepository。
/**
* 班級
* @author panjie
*/
public interface KlassRepository extends SoftDeleteCrudRepository<Klass, Long>{
}
public interface KlassTestRepository extends SoftDeleteCrudRepository<KlassTest, Long> {
}
public interface TeacherRepository extends CrudRepository<Teacher, Long> {
}
測試
package com.mengyunzhi.springbootsamplecode.softdelete.repository;
import com.mengyunzhi.springbootsamplecode.softdelete.entity.Klass;
import com.mengyunzhi.springbootsamplecode.softdelete.entity.KlassTest;
import com.mengyunzhi.springbootsamplecode.softdelete.entity.Teacher;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.orm.jpa.JpaObjectRetrievalFailureException;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;
import java.util.Optional;
/**
* @author panjie
*/
@SpringBootTest
@RunWith(SpringRunner.class)
public class TeacherRepositoryTest {
private final static Logger logger = LoggerFactory.getLogger(TeacherRepositoryTest.class);
@Autowired KlassRepository klassRepository;
@Autowired KlassTestRepository klassTestRepository;
@Autowired TeacherRepository teacherRepository;
@Test
public void findById() {
logger.info("新建一個有Klass和KlassTest的教師");
Klass klass = new Klass();
klassRepository.save(klass);
KlassTest klassTest = new KlassTest();
klassTestRepository.save(klassTest);
Teacher teacher = new Teacher();
teacher.setKlass(klass);
teacher.setKlassTest(klassTest);
teacherRepository.save(teacher);
logger.info("查找教師,斷言查找了實體,并且不發(fā)生異常");
Optional<Teacher> teacherOptional = teacherRepository.findById(teacher.getId());
Assertions.assertThat(teacherOptional.get()).isNotNull();
logger.info("刪除關(guān)聯(lián)的Klass, 再查找教師實體,斷言查找到了實體,不發(fā)生異常。斷言教師實體中,仍然存在已經(jīng)刪除的Klass實體");
klassRepository.deleteById(klass.getId());
teacherOptional = teacherRepository.findById(teacher.getId());
Assertions.assertThat(teacherOptional.get()).isNotNull();
Assertions.assertThat(teacherOptional.get().getKlass().getId()).isEqualTo(klass.getId());
logger.info("查找教師列表,不發(fā)生異常。斷言教師實體中,存在已刪除的Klass實體記錄");
List<Teacher> teacherList = (List<Teacher>) teacherRepository.findAll();
for (Teacher teacher1 : teacherList) {
Assertions.assertThat(teacher1.getKlass().getId()).isEqualTo(klass.getId());
}
logger.info("刪除關(guān)聯(lián)的KlassTest,再查找教師實體, 斷言找到了刪除的klassTest");
klassTestRepository.deleteById(klassTest.getId());
teacherOptional = teacherRepository.findById(teacher.getId());
Assertions.assertThat(teacherOptional.get()).isNotNull();
Assertions.assertThat(teacherOptional.get().getKlassTest().getId()).isEqualTo(klassTest.getId());
logger.info("再查找教師列表,斷言將發(fā)生JpaObjectRetrievalFailureException(EntityNotFound 異常被捕獲后,封裝拋出)異常");
Boolean catchException = false;
try {
teacherRepository.findAll();
} catch (JpaObjectRetrievalFailureException e) {
catchException = true;
}
Assertions.assertThat(catchException).isTrue();
}
}
總結(jié)
使用默認的@SqlDelete以及@Where注解時,jpa data能夠很好的處理findById()方法,但卻未能很好的處理findAll()方法。在此,我們通過重寫CrunRepository的方法,實現(xiàn)了,將進行基本的查詢時,使用我們自定義的加入了deleted = true的方法。而當jpa進行關(guān)聯(lián)查詢時,由于我們未設(shè)置@Where注解,所以將查詢出所有的數(shù)據(jù),進而避免了當進行findAll()查詢時,有被刪除的關(guān)聯(lián)數(shù)據(jù)時而發(fā)生的異常。
本文中,我們只給了部分示例代碼。
如果你需要完整的代碼,請點擊:https://github.com/mengyunzhi/springBootSampleCode/tree/master/softDelete。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Java如何實現(xiàn)URL帶請求參數(shù)(get/post)及得到get和post請求url和參數(shù)列表的方法
本文給大家介紹Java如何實現(xiàn)URL帶請求參數(shù)(get/post)及得到get和post請求url和參數(shù)列表的方法,涉及到 java獲取post請求參數(shù)的方法,感興趣的朋友一起看看吧2015-10-10
java GUI實現(xiàn)ATM機系統(tǒng)(3.0版)
這篇文章主要為大家詳細介紹了java GUI實現(xiàn)ATM機系統(tǒng)(3.0版),文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-03-03
mybatis-plus分頁如何接收前端參數(shù)limit和page
這篇文章主要介紹了mybatis-plus分頁如何接收前端參數(shù)limit和page,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-01-01
SpringBoot使用JDBC獲取相關(guān)的數(shù)據(jù)方法
這篇文章主要介紹了SpringBoot使用JDBC獲取相關(guān)的數(shù)據(jù)方法,JDBC與數(shù)據(jù)庫建立連接、發(fā)送 操作數(shù)據(jù)庫的語句并處理結(jié)果,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-03-03

