欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

在 Spring Data Jpa 中使用逻辑删除需做的工作

程序员文章站 2022-05-05 11:06:38
...

Spring Data 是个好东西,极大简化了后端dao的操作,只需要在 dao 接口写个 findByXXX 的方法就能自动实现按条件查询这个简直太爽了。

 

不过问题也出现了,我的应用对于数据的操作没有物理删除,全是逻辑删除,也就是每个表都有个字段 deleted,1表示此记录已删除,默认值为 0 。这就与 spring data 提供的模式有冲突了,那剩下的就是:改之。

 

CRUD 操作

对于基础的CRUD 操作搞起来比较简单,按照其官方文档重新实现个 factory-class 就ok了,具体的 repository 类可以继承 org.springframework.data.jpa.repository.support.SimpleJpaRepository 进行修改,不过我为了省事,直接把这个类复制过来然后下手:

 

/*
 * $Id$
 */
package com.someok.common.base.spring.data;

import static org.springframework.data.jpa.repository.query.QueryUtils.DELETE_ALL_QUERY_STRING;
import static org.springframework.data.jpa.repository.query.QueryUtils.applyAndBind;
import static org.springframework.data.jpa.repository.query.QueryUtils.getQueryString;
import static org.springframework.data.jpa.repository.query.QueryUtils.toOrders;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
import javax.persistence.NoResultException;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
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.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.JpaEntityInformationSupport;
import org.springframework.data.jpa.repository.support.LockMetadataProvider;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import com.someok.common.base.mvc.BaseDao;
import com.someok.common.base.mvc.BaseDefaultModel;
import com.someok.common.utils.StringUtil;

/**
 * 修改自
 * {@link org.springframework.data.jpa.repository.support.SimpleJpaRepository},
 * 提供逻辑删除功能(logicDelete),为适应这个要求,相关的查询也都做了修改。
 * 
 * 需要注意的是那些 delete 方法仍然是物理删除,而新增的那些 logic 开头的方法才是逻辑删除, 用时候需要注意这点.
 * 
 * @author wangjxe
 * 
 */
@org.springframework.stereotype.Repository
@Transactional(readOnly = true)
public class CustomSimpleJpaRepository<T, ID extends Serializable> implements
		BaseDao<T, ID> {

	/**
	 * 逻辑删除字段名.
	 */
	public final static String DELETEED_FIELD = "deleted";

	public static final String COUNT_QUERY_STRING = "select count(%s) from %s x where x.deleted = false";
	public static final String EXISTS_QUERY_STRING = "select count(%s) from %s x where x.%s = :id and x.deleted = false";

	private final JpaEntityInformation<T, ?> entityInformation;
	private final EntityManager em;
	// private final PersistenceProvider provider;

	private LockMetadataProvider lockMetadataProvider;

	/**
	 * Creates a new {@link SimpleJpaRepository} to manage objects of the given
	 * {@link JpaEntityInformation}.
	 * 
	 * @param entityInformation
	 *            must not be {@literal null}.
	 * @param entityManager
	 *            must not be {@literal null}.
	 */
	public CustomSimpleJpaRepository(
			JpaEntityInformation<T, ?> entityInformation,
			EntityManager entityManager) {

		Assert.notNull(entityInformation);
		Assert.notNull(entityManager);

		this.entityInformation = entityInformation;
		this.em = entityManager;
		// this.provider = PersistenceProvider.fromEntityManager(entityManager);
	}

	/**
	 * Creates a new {@link SimpleJpaRepository} to manage objects of the given
	 * domain type.
	 * 
	 * @param domainClass
	 *            must not be {@literal null}.
	 * @param em
	 *            must not be {@literal null}.
	 */
	public CustomSimpleJpaRepository(Class<T> domainClass, EntityManager em) {
		this(JpaEntityInformationSupport.getMetadata(domainClass, em), em);
	}

	/**
	 * Configures a custom {@link LockMetadataProvider} to be used to detect
	 * {@link LockModeType}s to be applied to queries.
	 * 
	 * @param lockMetadataProvider
	 */
	public void setLockMetadataProvider(
			LockMetadataProvider lockMetadataProvider) {
		this.lockMetadataProvider = lockMetadataProvider;
	}

	private Class<T> getDomainClass() {
		return entityInformation.getJavaType();
	}

	private String getDeleteAllQueryString() {
		return getQueryString(DELETE_ALL_QUERY_STRING,
				entityInformation.getEntityName());
	}

	private String getCountQueryString() {

		String countQuery = String.format(COUNT_QUERY_STRING,
				getCountQueryPlaceholder(), "%s");
		return getQueryString(countQuery, entityInformation.getEntityName());
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.springframework.data.repository.CrudRepository#delete(java.io.
	 * Serializable)
	 */
	@Transactional
	public void delete(ID id) {

		Assert.notNull(id, "The given id must not be null!");

		if (!exists(id)) {
			throw new EmptyResultDataAccessException(String.format(
					"No %s entity with id %s exists!",
					entityInformation.getJavaType(), id), 1);
		}

		delete(findOne(id));
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.data.repository.CrudRepository#delete(java.lang.Object
	 * )
	 */
	@Transactional
	public void delete(T entity) {

		Assert.notNull(entity, "The entity must not be null!");
		em.remove(em.contains(entity) ? entity : em.merge(entity));
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.data.repository.CrudRepository#delete(java.lang.Iterable
	 * )
	 */
	@Transactional
	public void delete(Iterable<? extends T> entities) {

		Assert.notNull(entities, "The given Iterable of entities not be null!");

		for (T entity : entities) {
			delete(entity);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.data.jpa.repository.JpaRepository#deleteInBatch(java
	 * .lang.Iterable)
	 */
	@Transactional
	public void deleteInBatch(Iterable<T> entities) {

		Assert.notNull(entities, "The given Iterable of entities not be null!");

		if (!entities.iterator().hasNext()) {
			return;
		}

		applyAndBind(
				getQueryString(DELETE_ALL_QUERY_STRING,
						entityInformation.getEntityName()), entities, em)
				.executeUpdate();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.springframework.data.repository.Repository#deleteAll()
	 */
	@Transactional
	public void deleteAll() {

		for (T element : findAll()) {
			delete(element);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.data.jpa.repository.JpaRepository#deleteAllInBatch()
	 */
	@Transactional
	public void deleteAllInBatch() {
		em.createQuery(getDeleteAllQueryString()).executeUpdate();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.data.repository.Repository#readById(java.io.Serializable
	 * )
	 */
	public T findOne(ID id) {

		Assert.notNull(id, "The given id must not be null!");
		return em.find(getDomainClass(), id);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.springframework.data.repository.CrudRepository#exists(java.io.
	 * Serializable)
	 */
	public boolean exists(ID id) {

		Assert.notNull(id, "The given id must not be null!");

		if (entityInformation.getIdAttribute() != null) {

			String placeholder = getCountQueryPlaceholder();
			String entityName = entityInformation.getEntityName();
			String idAttributeName = entityInformation.getIdAttribute()
					.getName();
			String existsQuery = String.format(EXISTS_QUERY_STRING,
					placeholder, entityName, idAttributeName);

			TypedQuery<Long> query = em.createQuery(existsQuery, Long.class);
			query.setParameter("id", id);

			return query.getSingleResult() == 1;
		} else {
			return findOne(id) != null;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.springframework.data.jpa.repository.JpaRepository#findAll()
	 */
	public List<T> findAll() {
		return getQuery(null, (Sort) null).getResultList();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.springframework.data.repository.CrudRepository#findAll(ID[])
	 */
	public List<T> findAll(Iterable<ID> ids) {

		return getQuery(new Specification<T>() {
			public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
					CriteriaBuilder cb) {
				Path<?> path = root.get(entityInformation.getIdAttribute());
				return path.in(cb.parameter(List.class, "ids"));
			}
		}, (Sort) null).setParameter("ids", ids).getResultList();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.springframework.data.jpa.repository.JpaRepository#findAll(org.
	 * springframework.data.domain.Sort)
	 */
	public List<T> findAll(Sort sort) {
		return getQuery(null, sort).getResultList();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.data.repository.PagingAndSortingRepository#findAll
	 * (org.springframework.data.domain.Pageable)
	 */
	public Page<T> findAll(Pageable pageable) {

		if (null == pageable) {
			return new PageImpl<T>(findAll());
		}

		return findAll(null, pageable);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.data.jpa.repository.JpaSpecificationExecutor#findOne
	 * (org.springframework.data.jpa.domain.Specification)
	 */
	public T findOne(Specification<T> spec) {

		try {
			return getQuery(spec, (Sort) null).getSingleResult();
		} catch (NoResultException e) {
			return null;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.data.jpa.repository.JpaSpecificationExecutor#findAll
	 * (org.springframework.data.jpa.domain.Specification)
	 */
	public List<T> findAll(Specification<T> spec) {
		return getQuery(spec, (Sort) null).getResultList();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.data.jpa.repository.JpaSpecificationExecutor#findAll
	 * (org.springframework.data.jpa.domain.Specification,
	 * org.springframework.data.domain.Pageable)
	 */
	public Page<T> findAll(Specification<T> spec, Pageable pageable) {

		TypedQuery<T> query = getQuery(spec, pageable);
		return pageable == null ? new PageImpl<T>(query.getResultList())
				: readPage(query, pageable, spec);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.data.jpa.repository.JpaSpecificationExecutor#findAll
	 * (org.springframework.data.jpa.domain.Specification,
	 * org.springframework.data.domain.Sort)
	 */
	public List<T> findAll(Specification<T> spec, Sort sort) {

		return getQuery(spec, sort).getResultList();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.springframework.data.repository.CrudRepository#count()
	 */
	public long count() {
		return em.createQuery(getCountQueryString(), Long.class)
				.getSingleResult();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.data.jpa.repository.JpaSpecificationExecutor#count
	 * (org.springframework.data.jpa.domain.Specification)
	 */
	public long count(Specification<T> spec) {

		return getCountQuery(spec).getSingleResult();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.data.repository.CrudRepository#save(java.lang.Object)
	 */
	@Transactional
	public <S extends T> S save(S entity) {

		if (entityInformation.isNew(entity)) {
			em.persist(entity);
			return entity;
		} else {
			return em.merge(entity);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.data.jpa.repository.JpaRepository#saveAndFlush(java
	 * .lang.Object)
	 */
	@Transactional
	public T saveAndFlush(T entity) {

		T result = save(entity);
		flush();

		return result;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.data.jpa.repository.JpaRepository#save(java.lang.
	 * Iterable)
	 */
	@Transactional
	public <S extends T> List<S> save(Iterable<S> entities) {

		List<S> result = new ArrayList<S>();

		if (entities == null) {
			return result;
		}

		for (S entity : entities) {
			result.add(save(entity));
		}

		return result;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.springframework.data.jpa.repository.JpaRepository#flush()
	 */
	@Transactional
	public void flush() {

		em.flush();
	}

	/**
	 * Reads the given {@link TypedQuery} into a {@link Page} applying the given
	 * {@link Pageable} and {@link Specification}.
	 * 
	 * @param query
	 *            must not be {@literal null}.
	 * @param spec
	 *            can be {@literal null}.
	 * @param pageable
	 *            can be {@literal null}.
	 * @return
	 */
	private Page<T> readPage(TypedQuery<T> query, Pageable pageable,
			Specification<T> spec) {

		query.setFirstResult(pageable.getOffset());
		query.setMaxResults(pageable.getPageSize());

		Long total = getCountQuery(spec).getSingleResult();
		List<T> content = total > pageable.getOffset() ? query.getResultList()
				: Collections.<T> emptyList();

		return new PageImpl<T>(content, pageable, total);
	}

	/**
	 * Creates a new {@link TypedQuery} from the given {@link Specification}.
	 * 
	 * @param spec
	 *            can be {@literal null}.
	 * @param pageable
	 *            can be {@literal null}.
	 * @return
	 */
	private TypedQuery<T> getQuery(Specification<T> spec, Pageable pageable) {

		Sort sort = pageable == null ? null : pageable.getSort();
		return getQuery(spec, sort);
	}

	/**
	 * Creates a {@link TypedQuery} for the given {@link Specification} and
	 * {@link Sort}.
	 * 
	 * @param spec
	 *            can be {@literal null}.
	 * @param sort
	 *            can be {@literal null}.
	 * @return
	 */
	private TypedQuery<T> getQuery(Specification<T> spec, Sort sort) {

		CriteriaBuilder builder = em.getCriteriaBuilder();
		CriteriaQuery<T> query = builder.createQuery(getDomainClass());

		Root<T> root = applySpecificationToCriteria(spec, query);
		query.select(root);

		if (sort != null) {
			query.orderBy(toOrders(sort, root, builder));
		}

		return applyLockMode(em.createQuery(query));
	}

	/**
	 * Creates a new count query for the given {@link Specification}.
	 * 
	 * @param spec
	 *            can be {@literal null}.
	 * @return
	 */
	private TypedQuery<Long> getCountQuery(Specification<T> spec) {

		CriteriaBuilder builder = em.getCriteriaBuilder();
		CriteriaQuery<Long> query = builder.createQuery(Long.class);

		Root<T> root = applySpecificationToCriteria(spec, query);
		query.select(builder.count(root));

		return em.createQuery(query);
	}

	/**
	 * Applies the given {@link Specification} to the given
	 * {@link CriteriaQuery}.
	 * 
	 * @param spec
	 *            can be {@literal null}.
	 * @param query
	 *            must not be {@literal null}.
	 * @return
	 */
	private <S> Root<T> applySpecificationToCriteria(Specification<T> spec,
			CriteriaQuery<S> query) {

		Assert.notNull(query);
		Root<T> root = query.from(getDomainClass());

		CriteriaBuilder builder = em.getCriteriaBuilder();

		// 增加了删除条件判断,从而将被逻辑删除的数据过滤掉
		Predicate deletedPredicate = null;
		if (BaseDefaultModel.class.isAssignableFrom(getDomainClass())) {
			Path<Boolean> deletedPath = root.<Boolean> get(DELETEED_FIELD);
			deletedPredicate = builder.isFalse(deletedPath);
		}
		
		if (spec == null) {
			// 没有其它条件的时候只判断deleted字段
			query.where(deletedPredicate);
			
			return root;
		}
		
		Predicate predicate = spec.toPredicate(root, query, builder);

		if (predicate != null) {
			// 存在其它条件的时候还需要组合一下 deleted 条件
			if (null != deletedPredicate) {
				predicate = builder.and(predicate, deletedPredicate);
			}
			query.where(predicate);
		}

		return root;
	}

	private TypedQuery<T> applyLockMode(TypedQuery<T> query) {

		LockModeType type = lockMetadataProvider == null ? null
				: lockMetadataProvider.getLockModeType();
		return type == null ? query : query.setLockMode(type);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.someok.common.base.mvc.BaseDao#logicDelete(java.io.Serializable)
	 */
	@Override
	public void logicDelete(ID id) {
		T entity = findOne(id);
		if (null == entity || !(entity instanceof BaseDefaultModel)) {
			return;
		}
		BaseDefaultModel model = (BaseDefaultModel) entity;
		model.setDeleted(true);

		this.em.merge(model);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.someok.common.base.mvc.BaseDao#logicDelete(java.lang.Object)
	 */
	@Override
	public void logicDelete(T entity) {
		if (null == entity || !(entity instanceof BaseDefaultModel)) {
			return;
		}

		BaseDefaultModel model = (BaseDefaultModel) entity;
		model.setDeleted(true);

		if (StringUtil.isBlank(model.getId())) {
			em.persist(model);
		} else {
			em.merge(model);
		}

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.someok.common.base.mvc.BaseDao#logicDelete(java.lang.Iterable)
	 */
	@Override
	public void logicDelete(Iterable<? extends T> entities) {
		if (null == entities) {
			return;
		}

		for (T entity : entities) {
			logicDelete(entity);
		}
	}

	protected String getCountQueryPlaceholder() {

		return "x";
	}
}

 

 主要的改动是 applySpecificationToCriteria 方法,与 SimpleJpaRepository 比对下就知道改了啥了。

 

findByXXX 操作

CURD 的修改还是比较简单的,不过那些根据接口方法自动实现查询修改起来就比较麻烦了。当然,不做任何修改也可以用,只需要在dao接口的方法上面加个 @Query 就行了,但是这样就需要写大量的jpql了,与采用 spring data的原意不符,咱用这玩意目的不就是为了个简单嘛。

 

本来的想法是继承某些类来对需要调整的方法重新实现就ok了,可惜spring data 这块的实现有点太封闭了,多个类没有 public,只能包内可见,更多的需要的方法只提供了 private 属性。没办法,只好把 org.springframework.data.jpa.repository.query 包内的代码都拷贝过来,其实真正需要改动的地方只有一处:

 

com.someok.common.base.spring.data.query.JpaQueryCreator.complete(Predicate, Sort, CriteriaQuery<Object>, CriteriaBuilder, Root<?>)

 

具体修改方法如下:

 

	protected CriteriaQuery<Object> complete(Predicate predicate, Sort sort,
			CriteriaQuery<Object> query, CriteriaBuilder builder, Root<?> root) {
		
		// 增加了删除条件判断,从而将被逻辑删除的数据过滤掉
		Predicate deletedPredicate = null;
		if (BaseDefaultModel.class.isAssignableFrom(this.domainClass)) {
			Path<Boolean> deletedPath = root.<Boolean> get(CustomSimpleJpaRepository.DELETEED_FIELD);
			deletedPredicate = builder.isFalse(deletedPath);
		}
		// 在原有条件基础上组合 deleted 条件
		if (null != deletedPredicate) {
			predicate = builder.and(predicate, deletedPredicate);
		}
		
		return this.query.select(root).where(predicate)
				.orderBy(QueryUtils.toOrders(sort, root, builder));
	}
 

 

收工走人。。。
相关标签: spring data