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));
}