分类
Spring Data Spring Persistence

spring data jpa优雅的实现软删除

软删除,即通过在标识数据表中的某个字段来达到看似删除的目的,由于软删除对数据库的侵入比较小,所以在删除时可以完美的规避数据约束的问题。

我曾在4年前写过一篇spring boot实现软删除,在该文章中给出了实现软删除的一种方式。本文将使用一种更优雅的方式来实现它。

项目源码

本文项目源码https://github.com/codedemo-club/spring-data-jpa-soft-delete

接口

首先我们需要建立一个接口,来标识哪些实体在查询时是需要软删除的:

public interface SoftDelete {
  Boolean getDeleted();
}

然后实体实现这一接口:

@Entity
public class Foo implements SoftDelete {
  private Boolean deleted = false;

  @Override
  public Boolean getDeleted() {
    return deleted;
  }

  // 设置为私有
  private void setDeleted(Boolean deleted) {
    this.deleted = deleted;
  }

注意:在此省略了其它的属性,其它的属性请参考源代码

@Entity
public class Bar implements SoftDelete {
  private Boolean deleted = false;

  @Override
  public Boolean getDeleted() {
    return deleted;
  }

  // 设置为私有
  private void setDeleted(Boolean deleted) {
    this.deleted = deleted;
  }

设置标识

接下来,使用@SQLDelete注解来设置删除语句,虽然此操作我们也可以通过相应的方法统一设置,但使用@SQLDelete会使软删除更加的灵活,特别是当我们解决一些数据唯一性的时候。

@Entity
@SQLDelete(sql = "update `bar` set deleted = 1 where id = ?")
public class Bar implements SoftDelete {
@Entity
@SQLDelete(sql = "update `foo` set deleted = 1 where id = ?")
public class Foo implements SoftDelete {

自定义软删除实现类

通过自定义的软删除实现类来达到在查询时加入软删除的目的。

package club.codedemo.springdatajpasoftdelete;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.JpaEntityInformationSupport;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.lang.Nullable;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * 软删除实现类
 * https://www.codedemo.club/spring-data-jpa-soft-delete/
 * https://developer.aliyun.com/article/465404
 * https://stackoverflow.com/questions/36721601/spring-boot-how-to-declare-a-custom-repository-factory-bean
 */
@Transactional(
    readOnly = true
)
public class SoftDeleteRepositoryImpl<T, ID> extends SimpleJpaRepository<T, ID>  implements PagingAndSortingRepository<T, ID>,
    CrudRepository<T, ID>,
    JpaSpecificationExecutor<T> {
  private final Logger logger = LoggerFactory.getLogger(this.getClass());

  public SoftDeleteRepositoryImpl(Class<T> domainClass, EntityManager em) {
    this(JpaEntityInformationSupport.getEntityInformation(domainClass, em), em);
  }

  public SoftDeleteRepositoryImpl(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {
    super(entityInformation, entityManager);
  }

  @Override
  public boolean existsById(ID id) {
    return this.findById(id).isPresent();
  }

  @Override
  public List<T> findAll() {
    return this.findAll(this.andDeleteFalseSpecification(null));
  }

  @Override
  public Page<T> findAll(Pageable pageable) {
    return this.findAll(this.andDeleteFalseSpecification(null), pageable);
  }

  @Override
  public List<T> findAll(@Nullable Specification<T> specification) {
    return super.findAll(this.andDeleteFalseSpecification(specification));
  }

  @Override
  public Page<T> findAll(@Nullable Specification<T> specification, Pageable pageable) {
    return super.findAll(this.andDeleteFalseSpecification(specification), pageable);
  }

  @Override
  public List<T> findAll(@Nullable Specification<T> specification, Sort sort) {
    return super.findAll(this.andDeleteFalseSpecification(specification), sort);
  }

  @Override
  public Optional<T> findById(ID id) {
    Optional<T> entityOptional = super.findById(id);
    if (entityOptional.isPresent()) {
      T entity = entityOptional.get();
      if (entity instanceof SoftDelete) {
        if (!((SoftDelete) entity).getDeleted())
          return entityOptional;
      } else {
        this.logger.warn("未实现SoftDeleteEntity的实体" + entity.getClass() + "使用了软删除功能。请检查JpaRepositoryFactoryBean配置");
      }
    }
    return Optional.empty();
  }

  @Override
  public List<T> findAllById(Iterable<ID> ids) {
    return super.findAllById(ids).stream().filter(entity -> {
      if (entity instanceof SoftDelete) {
        return !((SoftDelete) entity).getDeleted();
      } else {
        this.logger.warn("未实现SoftDeleteEntity的实体" + entity.getClass() + "使用了软删除功能。请检查JpaRepositoryFactoryBean配置");
      }
      return false;
    }).collect(Collectors.toList());
  }

  @Override
  public long count() {
    return this.count(this.andDeleteFalseSpecification(null));
  }

  @Override
  public long count(@Nullable Specification<T> specification) {
    return super.count(this.andDeleteFalseSpecification(specification));
  }

  /**
   * 添加软查询条件
   *
   * @param specification 综合查询条件
   */
  private Specification<T> andDeleteFalseSpecification(Specification<T> specification) {
    Specification<T> deleteIsFalse = (root, criteriaQuery, criteriaBuilder) -> criteriaBuilder.equal(root.get("deleted").as(Boolean.class), false);
    if (specification == null) {
      specification = deleteIsFalse;
    } else {
      specification = specification.and(deleteIsFalse);
    }
    return specification;
  }
}

自定义JpaRepositoryFactoryBean

通过自定义JpaRepositoryFactoryBean达到:实现了SoftDelete的实体在查询时使用SoftDeleteRepositoryImpl,而未实现SoftDelete的实体在查询时使用原SimpleJpaRepository


/**
 * 自定义软件删除工厂
 * @param <R> 仓库
 * @param <T> 实体
 */
public class SoftDeleteRepositoryFactoryBean <R extends JpaRepository<T, Serializable>, T> extends JpaRepositoryFactoryBean<R, T, Serializable> {
  public SoftDeleteRepositoryFactoryBean(Class<? extends R> repositoryInterface) {
    super(repositoryInterface);
  }

  @Override
  protected RepositoryFactorySupport createRepositoryFactory(final EntityManager entityManager) {
    return new JpaRepositoryFactory(entityManager) {
      protected SimpleJpaRepository<T, Serializable> getTargetRepository(
          RepositoryInformation information, EntityManager entityManager) {
        Class<T> domainClass = (Class<T>) information.getDomainType();
        if(SoftDelete.class.isAssignableFrom(domainClass)) {
          return new SoftDeleteRepositoryImpl(domainClass, entityManager);
        } else {
          return new SimpleJpaRepository(domainClass, entityManager);
        }
      }

      @Override
      protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
        return metadata.getDomainType().isAssignableFrom(SoftDelete.class) ? SoftDeleteRepositoryImpl.class : SimpleJpaRepository.class;
      }
    };
  }
}

注册Bean

最后我们注册刚刚创建的工厂Bean,以使其生效:

@EnableJpaRepositories(value = "club.codedemo.springdatajpasoftdelete",
        repositoryFactoryBeanClass = SoftDeleteRepositoryFactoryBean.class)
public class SpringDataJpaSoftDeleteApplication {

此时,将我们在进行查询操作时,如果实体实现了SoftDelete,则会使用我们自定义的SoftDeleteRepositoryImpl,而如果没有实现SoftDelete,则仍然使用原查询。

SoftDeleteRepositoryImpl方法中,我们重写了所有的查询方法,并在查询中加入了deletedfasle的查询条件。

测试

此时,重新启动系统,点击删除操作后。数据仍然存在于数据中,而且关联查询也不会发生任何错误。如果我们希望在@OneToMany注解中也使用软删除生效(即不返回已删除的关联实体),则仅仅需要在相应的字段加入@Where(clause = "deleted = false")注解即可。