1. 程式人生 > >Spring data jpa的高階查詢的應用和底層原理分析

Spring data jpa的高階查詢的應用和底層原理分析

spring data jpa的查詢

目前比較簡單的查詢:

三種查詢方案的寫法

  1. 固定引數查詢
interface XxxRepo implements JpaRepository<T,Long>{
	EntityXxx findByNameAndSex(String name,String sex);
}

這種方式是簡單的,方法名錶達自己想查詢的方法。支援and, or ,like, top, betweent,order等等。

  1. 不定引數個數查詢
PageRequest pageable = new PageRequest(query.getPindex(), query.
getPcount(), query.getSortObj());//分頁 Example<EntityXXX> example = Example.of(entity, ExampleMatcher.matchingAll());//封裝物件,matchingAll表明滿足所有條件 Page<EntityXXX> findAll = entityRepo.findAll(example, pageable);//開始查詢並返回物件

像以上這種程式碼,是可以多條件and查詢的。比如name=abc and sex=male。這種組合滿足我們一般的需求。

但是如果,我們想要查詢name in (zhangsan,lisi,wanger,mazi)等,那麼這種方式就不再適用。我們可以採用高階一點的查詢。

  1. 高階組合查詢:
public Page<EntityXxx> findAll(EntityXxx entity, PageRequest pageable) {
	Specification<EntityXxx> condition = whereCondition(entity);
	Page<EntityXxx> page = entityRepo.findAll(condition, pageable);
	return page;
}


private Specification<EntityXxx> whereCondition
(EntityXxx entity) { return new Specification<EntityXxx>() { @Override public Predicate toPredicate(Root<EntityXxx> root, CriteriaQuery<?> query, CriteriaBuilder cb) { List<Predicate> predicates = new ArrayList<Predicate>(); List<String> names = new ArrayList<>(); names.add("zhangsan"); names.add("lisi"); names.add("wanger"); Expression<Long> parentExpression = root.get("name"); Predicate parentPredicate = parentExpression.in(names); predicates.add(parentPredicate); if (!StringUtils.isEmpty(entity.getSex())) { predicates.add(cb.like(root.<String>get("sex"), "%" + entity.getSex() + "%"));//like查詢 } if (null != entity.getIdnum()) { predicates.add(cb.equal(root.<Integer>get("innum"), entity.getIdnum()));//精準匹配 } return query.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction(); } }; }

這種就比較高階了,判斷一下前端傳過來的物件裡某某屬性是否有值,有的話就新增到條件裡。

這樣一來,我們就可以簡單的寫個方法滿足很大部分的要求。

當然,如果我們只停留在會用,而不清楚原理的話,對個人發展是不利的。

我們可以來看看,每一種情況下,spring是怎樣實現的。

三種情況的Spring底層實現:

其實實現都是類似

類似這種,定義一個介面EntityRepo implements JpaRepository,然後在裡邊寫各種簡單的查詢。這種執行時Spring是通過方法攔截器實現的。在Spring Data(commons包)裡的實現:
a. 在應用啟動時收集所有repo裡的所有方法

//RepositoryFactorySupport$QueryExecutorMethodInterceptor 
public class QueryExecutorMethodInterceptor implements MethodInterceptor {
	//表明該方法屬於哪種查詢:
	//例如:對於方法findByNameAndSex
	// 1.SimpleJpaQuery
	// 方法頭上@Query註解的nativeQuery屬性預設值為false,也就是使用JPQL,此時會建立SimpleJpaQuery例項,並通過兩個StringQuery類例項分別持有query jpql語句和根據query jpql計算拼接出來的countQuery jpql語句;
	// 2.NativeJpaQuery
	// 方法頭上@Query註解的nativeQuery屬性如果顯式的設定為nativeQuery=true,也就是使用原生SQL,此時就會建立NativeJpaQuery例項;
	// 3.PartTreeJpaQuery
	// 方法頭上未進行@Query註解,將使用spring-data-jpa獨創的方法名識別的方式進行sql語句拼接,此時在spring-data-jpa內部就會建立一個PartTreeJpaQuery例項;
	// 4.NamedQuery
	// 使用javax.persistence.NamedQuery註解訪問資料庫的形式,此時在spring-data-jpa內部就會根據此註解選擇建立一個NamedQuery例項;
	// 5.StoredProcedureJpaQuery
	// 顧名思義,在Repository介面的方法頭上使用org.springframework.data.jpa.repository.query.Procedure註解,也就是呼叫儲存過程的方式訪問資料庫,此時在spring-data-jpa內部就會根據@Procedure註解而選擇建立一個StoredProcedureJpaQuery例項。
    private final Map < Method, RepositoryQuery > queries;
    ...
    public QueryExecutorMethodInterceptor(RepositoryInformation repositoryInformation,
        ProjectionFactory projectionFactory) {
    	...

        this.queries = lookupStrategy //
            .map(it - > mapMethodsToQuery(repositoryInformation, it, projectionFactory)) //
            .orElse(Collections.emptyMap());
    }

    private Map < Method, RepositoryQuery > mapMethodsToQuery(RepositoryInformation repositoryInformation,
            QueryLookupStrategy lookupStrategy, ProjectionFactory projectionFactory) {

            return repositoryInformation.getQueryMethods().stream() //
                .map(method - > lookupQuery(method, repositoryInformation, lookupStrategy, projectionFactory)) //
                .peek(pair - > invokeListeners(pair.getSecond())) //
                .collect(Pair.toMap());
        }
        ...
}
...
}

查詢repository裡有哪些方法:
這個方法裡對每個repo裡的方法進行收集。

class DefaultRepositoryInformation...
		public Streamable<Method> getQueryMethods() {

		Set<Method> result = new HashSet<>();

		for (Method method : getRepositoryInterface().getMethods()) {
			method = ClassUtils.getMostSpecificMethod(method, getRepositoryInterface());
			if (isQueryMethodCandidate(method)) {
				result.add(method);
			}
		}

		return Streamable.of(Collections.unmodifiableSet(result));
	}...

b. 當程式執行時遇到呼叫repo中的某個方法時:

同樣檢視那個攔截器裡的程式碼。

 //RepositoryFactorySupport$QueryExecutorMethodInterceptor implements MethodInterceptor
@Nullable
private Object doInvoke(MethodInvocation invocation) throws Throwable {

	Method method = invocation.getMethod();
	Object[] arguments = invocation.getArguments();

	if (hasQueryFor(method)) {
		return queries.get(method).execute(arguments);//execute執行的是AbstractJpaQuery中的方法
	}

	return invocation.proceed();
}

找到AbstractJpaQuery

@Nullable
@Override
public Object execute(Object[] parameters) {
	return doExecute(getExecution(), parameters);
}

找到PartTreeJpaQuery extends AbstractJpaQuery

為什麼是這個,這是因為我們在使用自定義的查詢方法(如:EntityXxx findByNameAndSex(String name,String sex)),使用的是這個Query。這個在上面的程式碼裡註釋有所介紹。

@Override
protected JpaQueryExecution getExecution() {

	if (this.tree.isDelete()) {
		return new DeleteExecution(em);
	} else if (this.tree.isExistsProjection()) {
		return new ExistsExecution();
	}

	return super.getExecution();
}

在AbstractJpaQuery找到對應的執行器。

protected JpaQueryExecution getExecution() {

	if (method.isStreamQuery()) {
		return new StreamExecution();
	} else if (method.isProcedureQuery()) {
		return new ProcedureExecution();//儲存過程執行器
	} else if (method.isCollectionQuery()) {
		return new CollectionExecution();//結果為集合
	} else if (method.isSliceQuery()) {
		return new SlicedExecution(method.getParameters());
	} else if (method.isPageQuery()) {
		return new PagedExecution(method.getParameters());//分頁查詢
	} else if (method.isModifyingQuery()) {
		return new ModifyingExecution(method, em);//方法為修改資料庫
	} else {
		return new SingleEntityExecution();//結果為單個
	}
}

然後找到對應的執行器類,並在裡邊找對應的方法。

關於裡邊的內容盤根錯節,無法用簡單文字詳細說明。這裡只是提供一個實實在在的線頭,你根據這個線頭就能剝開了。_

--------------------2018.9.20更新----------------------------------------------------

對於第二種或第三種查詢,Spring原始碼分析:

//第二種
Page<EntityXXX> findAll = exdapRepo.findAll(example, pageable)
//呼叫SimpleJpaRepository
public <S extends T> Page<S> findAll(Example<S> example, Pageable pageable) {

  ExampleSpecification<S> spec = new ExampleSpecification<S>(example);
  Class<S> probeType = example.getProbeType();
  TypedQuery<S> query = getQuery(new ExampleSpecification<S>(example), probeType, pageable);

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

protected <S extends T> TypedQuery<S> getQuery(Specification<S> spec, Class<S> domainClass, Pageable pageable) {

  Sort sort = pageable == null ? null : pageable.getSort();
  return getQuery(spec, domainClass, sort);
}
//第三種
Page<EntityXxx> page = entityRepo.findAll(condition, pageable);//condition:Specification型別
//呼叫SimpleJpaRepository
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, getDomainClass(), pageable, spec);
}

protected TypedQuery<T> getQuery(Specification<T> spec, Pageable pageable) {

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

看到沒有,兩者殊途同歸,最終呼叫的都是getQuery:

protected <S extends T> TypedQuery<S> getQuery(Specification<S> spec, Class<S> domainClass, Sort sort) {

	CriteriaBuilder builder = em.getCriteriaBuilder();
	CriteriaQuery<S> query = builder.createQuery(domainClass);

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

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

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

然後,我們順著getQuery往下擼,重點關注applySpecificationToCriteria:

private <S, U extends T> Root<U> applySpecificationToCriteria(Specification<U> spec, Class<U> domainClass,
		CriteriaQuery<S> query) {

	Assert.notNull(domainClass, "Domain class must not be null!");
	Assert.notNull(query, "CriteriaQuery must not be null!");

	Root<U> root = query.from(domainClass);

	if (spec == null) {
		return root;
	}

	CriteriaBuilder builder = em.getCriteriaBuilder();
	Predicate predicate = spec.toPredicate(root, query, builder);

	if (predicate != null) {
		query.where(predicate);
	}

		return root;
	}

如果我們在查詢的時候new一個匿名Specification的實現,那麼這裡會直接返回。否則,這裡會例項一個ExampleSpecification。

其toPredicate的Spring實現:

public static <T> Predicate getPredicate(Root<T> root, CriteriaBuilder cb, Example<T> example) {

	Assert.notNull(root, "Root must not be null!");
	Assert.notNull(cb, "CriteriaBuilder must not be null!");
	Assert.notNull(example, "Example must not be null!");

	ExampleMatcher matcher = example.getMatcher();

	List<Predicate> predicates = getPredicates("", cb, root, root.getModel(), example.getProbe(),
			example.getProbeType(), new ExampleMatcherAccessor(matcher), new PathNode("root", null, example.getProbe()));

	if (predicates.isEmpty()) {
		return cb.isTrue(cb.literal(true));
	}

	if (predicates.size() == 1) {
		return predicates.iterator().next();
	}

	Predicate[] array = predicates.toArray(new Predicate[predicates.size()]);

	return matcher.isAllMatching() ? cb.and(array) : cb.or(array);
}

就介紹到這裡了吧。感覺仔細看下,應該可以看懂。如果有不明白的地方,或者覺得我寫的不好,可以留言,我盡力更正改進。