spring-data-jpa 是如何渲染查詢的

spring-data-jpa 是如何渲染查詢的

1、 spring-data-jpa是什麼, 它和jpa有什麼關係?

jpa: Java Persistence API, 是一種規範, 定義了一系列的接口, 用於將對象映射到表上.

Hibernate: 是jpa的一種實現

spring-data-jpa spring全家通成員之一, 也是jpa的一種實現, 並且是hibernate的封裝, 底層調用的是hibernate

三者的關係可以參考下圖(圖片源自網絡)
在這裏插入圖片描述

從上圖中可以看出, spring-data-jpa 也是jpa規範的實現之一,並且底層封裝的是hibernate

2、一次查詢的渲染過程

在我實際工作中, 我們是這麼使用spring-data-jpa的:

@Repository
public interface UserRepository extends AbstractRepository<UserDO, UUID> {
 
    List<UserDO> findByUserIdAndValidTrueOrderByUserOrder(UUID id);
}

上面的代碼定義了一個UserRepository接口,接口中定義了方法:findByUserIdAndValidTrueOrderByUserOrder, 從左到右通讀這個方法名,彷彿看到了下面這條sql:

select * from t_user where user_id=:id and valid=1 order by user_order asc;

那麼,spring-data-jpa是怎如何把這一大串方法(findByUserIdAndValidTrueOrderByUserOrder),“ 翻譯”成可執行的sql的,其中spring-data-jpa爲我們做了哪些事情呢?

要理解這個問題, 首先可以明確一點思路:首先把需要“翻譯”的方法定位出來,即找到一系列findXXXByXXX方法,然後按照方法名一步一步“渲染”成sql

爲此,spring-data-jpa實現了一個"查詢方法攔截器" QueryExecutorMethodInterceptor (cglib動態代理 ), 部分代碼如下:

public class QueryExecutorMethodInterceptor implements MethodInterceptor {
 
    // queries是一個複數名詞, 一個Map類型, key是Method, value是RepositoryQuery
    private final Map<Method, RepositoryQuery> queries;
 
    // 在項目啓動時, 會走到這個構造方法裏
    public QueryExecutorMethodInterceptor(RepositoryInformation repositoryInformation,
            ProjectionFactory projectionFactory) {
        // ...
        this.queries = lookupStrategy //optional
                .map(it -> mapMethodsToQuery(repositoryInformation, it, projectionFactory)) //
                .orElse(Collections.emptyMap());
    }
 
    // 把method映射成Query, 返回一個Map<Method, RepositoryQuery>
    private Map<Method, RepositoryQuery> mapMethodsToQuery(RepositoryInformation repositoryInformation,
            QueryLookupStrategy lookupStrategy, ProjectionFactory projectionFactory) {
 
        return repositoryInformation.getQueryMethods().stream() //
                .map(method -> lookupQuery(method, repositoryInformation, lookupStrategy, projectionFactory)) //
                // ...
                .collect(Pair.toMap());
    }
 
    // 核心是strategy.resolveQuery方法
    private Pair<Method, RepositoryQuery> lookupQuery(Method method, RepositoryInformation information,
            QueryLookupStrategy strategy, ProjectionFactory projectionFactory) {
        return Pair.of(method, strategy.resolveQuery(method, information, projectionFactory, namedQueries));
    }
 
    // ...
}

QueryExecutorMethodInterceptor類中, 包含一個重要的成員:queries,顯然queries是一個複數名詞, 它是一個Map類型, key是Method, value是RepositoryQuery,用來存儲多個Method -> RepositoryQuery映射關係對

在項目啓動時, 程序會走到QueryExecutorMethodInterceptor的構造方法中,爲queries賦值。其中,RepositoryInformation包含待映射的Method,在mapMethodsToQuery方法中,將Method映射成了RepositoryQuery。
那麼是如何映射的呢?關鍵就在lookupQuery方法中,將映射的實現委託給了strategy.resolveQuery方法

strategy是啥呢,它是一個QueryLookupStrategy 對象, QueryLookupStrategy 有三種實現策略:

  • CREATE: 直接根據方法名創建Query
  • USE_DECLARE_QUERY: 使用@Query聲明的方式創建 Query
  • CREATE_IF_NOT_FOUND : 1, 2的結合, 先找有沒有@Query註解, 沒有則直接用方法名創建 Query

在XXApplication類中,我們可以使用@EnableJpaRepositories(queryLookupStrategy= … )配置QueryLookupStrategy,

默認的配置是CREATE_IF_NOT_FOUND, 所以看下 CREATE_IF_NOT_FOUND這種策略是 如何實現resolveQuery方法的

@Override
protected RepositoryQuery resolveQuery(JpaQueryMethod method, EntityManager em, NamedQueries namedQueries) {
 
    try {
        return lookupStrategy.resolveQuery(method, em, namedQueries); // 先查詢@Query註解, 查不到則拋出異常
    } catch (IllegalStateException e) {
        return createStrategy.resolveQuery(method, em, namedQueries); // 捕捉到異常後, 執行創建策略
    }
}

這裏使用了try catch做流程控制,先查詢有沒有@Query註解, 如果查不到則拋出異常;在捕捉到異常後, 使用方法名創建。用try-catch做流程控制,可能並不太友好。

接下來看看 createStrategy.resolveQuery方法爲我們做了哪些事

@Override
protected RepositoryQuery resolveQuery(JpaQueryMethod method, EntityManager em, NamedQueries namedQueries) {
    return new PartTreeJpaQuery(method, em, persistenceProvider, escape);
}
 
/**
 * Creates a new {@link PartTreeJpaQuery}.
 *
 */
PartTreeJpaQuery(JpaQueryMethod method, EntityManager em, PersistenceProvider persistenceProvider,
        EscapeCharacter escape) {
 
    // ...
    this.tree = new PartTree(method.getName(), domainClass);
    // ... 
}

resolveQuery方法都在圍繞PartTree搞事情,放眼望去,主要就是new了一個PartTree對象, PartTree這個類是做什麼的呢?構造方法如下:

public PartTree(String source, Class<?> domainClass) {
 
    Matcher matcher = PREFIX_TEMPLATE.matcher(source);
 
    if (!matcher.find()) {
        this.subject = new Subject(Optional.empty());
        this.predicate = new Predicate(source, domainClass);
    } else {
        this.subject = new Subject(Optional.of(matcher.group(0)));
        this.predicate = new Predicate(source.substring(matcher.group().length()), domainClass);
    }
}

從上面的構造方法中, 看到PartTree利用正則表達式, 解析出了方法名中的主語和謂語。

關於正則表達式, PartTree中定義了下面幾個正則:

  • QUERY_PATTERN = “find|read|get|query|stream”;
  • COUNT_PATTERN = “count”;
  • EXISTS_PATTERN = “exists”;
  • DELETE_PATTERN = “delete|remove”;
  • Pattern PREFIX_TEMPLATE = Pattern.compile( “^(” + QUERY_PATTERN + “|” + COUNT_PATTERN + “|” + EXISTS_PATTERN + “|” + DELETE_PATTERN + ");

關於主語和謂語, 下面的註釋還是值得一看的

/**
 * The subject, for example "findDistinctUserByNameOrderByAge" would have the subject "DistinctUser".
 */
private final Subject subject;
 
/**
 * The subject, for example "findDistinctUserByNameOrderByAge" would have the predicate "NameOrderByAge".
 */
private final Predicate predicate;

findXXXByYYY, 可以把XXX理解成subject,把YYY理解爲predicate

以"findByUserIdAndValidTrueOrderByUserOrder"爲例, 將被解析成以下類似樹狀的結構(可能這也是PartTree名字的來源)

findByUserIdAndValidTrueOrderByUserOrder
    -> UserIdAndValidTrueOrderByUserOrder
        -> UserIdAndValidTrue
            -> UserId
            And
            -> ValidTrue
        -> OrderByUserOrder
            -> UserOrder
            -> ASC

然後,把得到PartTree對象封裝成RepositoryQuery 並返回,這樣就將Method映射成了RepositoryQuery,

在啓動時, 就創建出了 Map<Method, RepositoryQuery>映射關係, 即QueryExecutorMethodInterceptor中的queries。

下面是創建Map<Method, RepositoryQuery>映射關係 過程的調用棧:

org.springframework.data.repository.core.support.RepositoryFactorySupport.QueryExecutorMethodInterceptor#QueryExecutorMethodInterceptor
org.springframework.data.repository.core.support.RepositoryFactorySupport.QueryExecutorMethodInterceptor#mapMethodsToQuery
org.springframework.data.repository.core.support.RepositoryFactorySupport.QueryExecutorMethodInterceptor#lookupQuery
org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy.AbstractQueryLookupStrategy#resolveQuery(java.lang.reflect.Method, org.springframework.data.repository.core.RepositoryMetadata, org.springframework.data.projection.ProjectionFactory, org.springframework.data.repository.core.NamedQueries)
org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy.CreateIfNotFoundQueryLookupStrategy#resolveQuery
org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy.CreateQueryLookupStrategy#resolveQuery
org.springframework.data.jpa.repository.query.PartTreeJpaQuery#PartTreeJpaQuery(org.springframework.data.jpa.repository.query.JpaQueryMethod, javax.persistence.EntityManager, org.springframework.data.jpa.provider.PersistenceProvider, org.springframework.data.jpa.repository.query.EscapeCharacter)
org.springframework.data.repository.query.parser.PartTree

得到Map<Method, RepositoryQuery>映射關係後, jpa是怎麼使用這層關係的呢?

答案是, 在執行查詢時, 根據method 直接找到對應的RepositoryQuery, 利用到了上面的queries

Method method = invocation.getMethod();
if (hasQueryFor(method)) {
    return queries.get(method).execute(invocation.getArguments());
}

拿到RepositoryQuery後, 對Query進行渲染, 最終會進入hibernate中,下面是調用棧

org.springframework.data.repository.core.support.RepositoryFactorySupport.QueryExecutorMethodInterceptor#invoke
org.springframework.data.repository.core.support.RepositoryFactorySupport.QueryExecutorMethodInterceptor#doInvoke
org.springframework.data.jpa.repository.query.AbstractJpaQuery#execute
org.springframework.data.jpa.repository.query.AbstractJpaQuery#doExecute
org.springframework.data.jpa.repository.query.JpaQueryExecution#execute
org.springframework.data.jpa.repository.query.JpaQueryExecution.SingleEntityExecution#doExecute
org.springframework.data.jpa.repository.query.AbstractJpaQuery#createQuery
org.springframework.data.jpa.repository.query.PartTreeJpaQuery#doCreateQuery
org.springframework.data.jpa.repository.query.PartTreeJpaQuery.QueryPreparer#createQuery(org.springframework.data.jpa.repository.query.JpaParametersParameterAccessor)
org.springframework.data.jpa.repository.query.PartTreeJpaQuery.QueryPreparer#createQuery(javax.persistence.criteria.CriteriaQuery<?>)
 
// 從這裏開始, 進入hibernate
org.hibernate.engine.spi.SessionDelegatorBaseImpl#createQuery(javax.persistence.criteria.CriteriaQuery<T>)
org.hibernate.internal.AbstractSharedSessionContract#createQuery(javax.persistence.criteria.CriteriaQuery<T>)
org.hibernate.query.criteria.internal.compile.CriteriaCompiler#compile
org.hibernate.query.criteria.internal.CriteriaQueryImpl#interpret
下面渲染部分
org.hibernate.query.criteria.internal.QueryStructure#render
org.hibernate.query.criteria.internal.QueryStructure#renderSelectClause

最終走到QueryStructure的render方法,通過renderXXX方法,依次渲染select,from,where,group by

public void render(StringBuilder jpaqlQuery, RenderingContext renderingContext) {
    renderSelectClause( jpaqlQuery, renderingContext );
    renderFromClause( jpaqlQuery, renderingContext );
    renderWhereClause( jpaqlQuery, renderingContext );
    renderGroupByClause( jpaqlQuery, renderingContext );
}

renderXXX方法裏, 主要是jpaqlQuery查詢字符串的拼接, 這裏以renderSelectClause爲例,看看它都做了哪些事:

protected void renderSelectClause(StringBuilder jpaqlQuery, RenderingContext renderingContext) {
    renderingContext.getClauseStack().push( Clause.SELECT );
 
    try {
        jpaqlQuery.append( "select " );
 
        if ( isDistinct() ) {
            jpaqlQuery.append( "distinct " );
        }
 
        if ( getSelection() == null ) {
            jpaqlQuery.append( locateImplicitSelection().render( renderingContext ) );
        }
        else {
            jpaqlQuery.append( ( (Renderable) getSelection() ).render( renderingContext ) );
        }
    }
    finally {
        renderingContext.getClauseStack().pop();
    }
}

renderSelectClause主要做了StringBuilder的拼接

到了這裏, 對於如何把方法名“翻譯”成sql, 能有個大概的認識了。當然這裏的sql,也並不是mysql可已直接執行的原生sql,按我的理解應該是hibernate定義的一種“類似sql”的形式,裏面有些佔位符等。

總結:

上面主要分析了select查詢是如何被渲染成sql的,當然, 除了將方法名映射成sql以外, jpa也支持@Query註解, 自定義查詢, 支持原生sql或HQL

  @Query("select * from tb_task t where t.task_name = ?1", nativeQuery = true)
  Task findByTaskName(String taskName);


  @Query("select t from Task t where t.taskName = ?1")
  Task findByTaskName(String taskName);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章