spring boot實現軟刪除

本文中,咱們只給了部分示例代碼。
若是你須要完整的代碼,請點擊: https://github.com/mengyunzhi/springBootSampleCode/tree/master/softDelete

本文開發環境:spring-boot:2.0.3.RELEASE + java1.8java

WHY TO DO

軟刪除:即不進行真正的刪除操做。因爲咱們實體間的約束性(外鍵)的存在,刪除某些數據後,將致使其它的數據不完整。好比,計算機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

  1. 能夠成功的實現軟刪除。
  2. 再進行關聯刪除時,不發生500 EntiyNotFound錯誤。

解決方案

  1. 即然500是因爲註解@Where(clause = "deleted = false")引發的,那麼咱們棄用該註解。
  2. 咱們須要在查詢時,加入deleted = false的查詢。那麼咱們新建一個接口,並繼承jpaCrudRepository,而後重寫其查詢相關的方法。在重寫過程當中,加入deleted = false的查詢條件。

實施

clipboard.png

初始化

新建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();
}

新建倉庫類

繼承springCrudRepository數據結構

/**
 * 班級
 * @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

相關文章
相關標籤/搜索