本文中,咱們只給了部分示例代碼。
若是你須要完整的代碼,請點擊: https://github.com/mengyunzhi/springBootSampleCode/tree/master/softDelete。
本文開發環境:spring-boot:2.0.3.RELEASE
+ java1.8
java
軟刪除:即不進行真正的刪除操做。因爲咱們實體間的約束性(外鍵)的存在,刪除某些數據後,將致使其它的數據不完整。好比,計算機1801班的教師是張三,此時,咱們若是把張三刪除掉,那麼在查詢計算機1801班時,因爲張三不存了,因此就會報EntityNotFound的錯誤。固然了,在有外鍵約束的數據庫中,若是張三是1801班的教師,那麼咱們直接刪除張三將報一個約束性的異常。也就是說:直接刪除張三這個行爲是沒法執行的。git
但有些時候,咱們的確有刪除的需求。好比說,有個員工離職了,而後咱們想在員工管理中刪除該員工。可是:該員工因爲在數據表中存在歷史記錄。好比咱們記錄了17年第二學期的數據結構是張三教的。那麼,因爲約束性的存在,刪除張三時就會報約束性錯誤。也就是說:出現了應該刪除,但卻刪除不了的尷尬。github
這就用到了本文所提到的軟刪除,所謂軟刪除,就是說我並不真正的刪除數據表中的數據,而是在給這條記錄加一個是否刪除的標記。spring
spring jpa
是支持軟刪除的,咱們能夠找到較多質量不錯的文章來解決這個問題。大致步驟爲:1. 加入@SqlDelete("update xxxx set deleted = 1 where id = ?")
。2.加入@Where(clause = "deleted = false")
的註解。但這個解決方案並不完美。具體表如今:
咱們還以張三是1801班的教師舉例。
加入註解後,咱們的確是能夠作到能夠成功的刪除張三了,刪除操做後,咱們查看數據表,張三的記錄的確也還在。但此時,若是咱們進行all
或是page
查詢,將獲得一個500 EntiyNotFound
錯誤。這是因爲在all
查詢時,jpa
自動加入了@Where
中的的查詢參數,因爲關聯數據的deleted = true
,進而發生了未找到關聯實體的異常。sql
但事實是:實體雖然被刪除,但實際在還在,咱們想將其應用到關聯查詢中。並不但願其發生500 EntiyNotFound
異常。數據庫
本文的方案實現了:api
500 EntiyNotFound
錯誤。@Where(clause = "deleted = false")
引發的,那麼咱們棄用該註解。deleted = false
的查詢。那麼咱們新建一個接口,並繼承jpa
的CrudRepository
,而後重寫其查詢相關的方法。在重寫過程當中,加入deleted = false
的查詢條件。
新建KlassTest
, Klass
, Teacher
三個實體,新建BaseEntity
抽象類實體。其中KlassTest
用於演示使用@Where(clause = "deleted = false")
註解時發生的異常。springboot
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; /** * 應用軟刪除 * 默認的@Where(clause = "deleted = 0")會致使hibernate內部進行關聯查詢時,發生ObjectNotFound的異常 * 在此從新定義接口 * 參考:https://stackoverflow.com/questions/19323557/handling-soft-deletes-with-spring-jpa/22202469 * @author 河北工業大學 夢雲智軟件開發團隊 */ @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("查找教師,斷言查找了實體,而且不發生異常"); Optional<Teacher> teacherOptional = teacherRepository.findById(teacher.getId()); Assertions.assertThat(teacherOptional.get()).isNotNull(); logger.info("刪除關聯的Klass, 再查找教師實體,斷言查找到了實體,不發生異常。斷言教師實體中,仍然存在已經刪除的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("查找教師列表,不發生異常。斷言教師實體中,存在已刪除的Klass實體記錄"); List<Teacher> teacherList = (List<Teacher>) teacherRepository.findAll(); for (Teacher teacher1 : teacherList) { Assertions.assertThat(teacher1.getKlass().getId()).isEqualTo(klass.getId()); } logger.info("刪除關聯的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("再查找教師列表,斷言將發生JpaObjectRetrievalFailureException(EntityNotFound 異常被捕獲後,封裝拋出)異常"); Boolean catchException = false; try { teacherRepository.findAll(); } catch (JpaObjectRetrievalFailureException e) { catchException = true; } Assertions.assertThat(catchException).isTrue(); } }
使用默認的@SqlDelete
以及@Where
註解時,jpa data
可以很好的處理findById()
方法,但卻未能很好的處理findAll()
方法。在此,咱們經過重寫CrunRepository
的方法,實現了,將進行基本的查詢時,使用咱們自定義的加入了deleted = true
的方法。而當jpa
進行關聯查詢時,因爲咱們未設置@Where
註解,因此將查詢出全部的數據,進而避免了當進行findAll()
查詢時,有被刪除的關聯數據時而發生的異常。app