spring-data-jpa源码阅读笔记:Repository方法名查询推导(Query Derivation From Method Names)的实现原理 2
Repository方法名查询推导(Query Derivation From Method Names)的实现原理 2
从魔法到现实
上次的文章我们讲到QueryExecutorMethodInterceptor
这个类。
阅读这个类的源代码,我们发现,这个类实现了MethodInterceptor
接口。也就是说它是一个方法调用的拦截器,
当一个Repository
上的查询方法,譬如说findByEmailAndLastname
方法被调用,Advice
拦截器会在
方法真正的实现调用前,先执行这个MethodInterceptor
的invoke
方法。这样我们就有机会在真正方法实现
执行前执行其他的代码了。
然而对于QueryExecutorMethodInterceptor
来说,最重要的代码并不在invoke
方法中,而是在它的
构造器QueryExecutorMethodInterceptor(RepositoryInformation r, Object customImplementation, Object target)
中。
最重要的一段代码是这段:
这段代码的主要工作是,通过lookupStrategy
,针对Repository
接口上所定义的方法来查询RepositoryQuery
,
并在查询到的query上执行监听器(我们先忽略监听器),然后以method
方法对象作为key,放入queries
缓存中。
那么问题是lookupStrategy
是哪里来的?往上翻我们发现
}
从这段代码我们可以看到,lookupStrategy
是从外部类RepositoryFactorySupport
上的getQueryLookupStrategy(...)
方法来获取的。
阅读RepositoryFactorySupport
的源代码,我们发现,无论是一个参数还是两个参数的getQueryLookupStrategy
方法都是直接返回null,
所以是不可能通过这个两个方法来获取真正的实现的,那么lookupStrategy
到底哪里来的呢。
答案在RepositoryFactorySupport
的子类JpaRepositoryFactory
中。在JpaRepositoryFactory
中我们发现了
getQueryLookupStrategy
(两个参数)的真正实现,它调用了JpaQueryLookupStrategy
的静态create
方法
可以看到,这个方法通过外部传入的key来返回不同实现的QueryLookupStrategy
。如果外部key没有定义(为null
)的话,
会返回CreateIfNotFoundQueryLookupStrategy
的实现。通过名字可以知道这个实现其实只是一个代理,它会将真正的调用
根据情况转发到CreateQueryLookupStrategy
以及DeclaredQueryLookupStrategy
。
根据名称,我们就能大致猜测到这些实现类的作用以及区别了。CreateQueryLookupStrategy
会根据方法名
创建查询;DeclaredQueryLookupStrategy
则会尝试使用方法上的@Query
注解来查找named query;
而CreateIfNotFoundQueryLookupStrategy
则是会先尝试DeclaredQueryLookupStrategy
,
如果没有找到则再去调用CreateQueryLookupStrategy
。
另外说一下关于这个作为参数传入的key
,如果你仔细看一下就会发现,这个key
的来源其实是定义在RepositoryFactorySupport
中的
域queryLookupStrategyKey
,可以作为外部配置选项使用,并且默认并没有赋值,所以默认是null
。因此该create
方法其实默认会
返回CreateIfNotFoundQueryLookupStrategy
的实现。
因为我们要找的重点是Repository
方法名查询推导,所以我们先忽略DeclaredQueryLookupStrategy
和
CreateIfNotFoundQueryLookupStrategy
实现。让我们回到CreateQueryLookupStrategy
实现中来。
在CreateQueryLookupStrategy
实现中,我们可以看到它继承了AbstractQueryLookupStrategy
抽象类,
并且覆盖了resolveQuery
方法,返回了一个叫做PartTreeJpaQuery
的RepositoryQuery
实现,源码如下:
其中PartTreeJpaQuery
是重点。这个PartTreeJpaQuery
就是前文所说的lookupStrategy
返回的、被放入缓存的最终Query的实现。
所以需要重点阅读这个类的源代码。
PartTree
关于PartTreeJpaQuery
,我们先来看一下它的继承结构。
PartTreeJpaQuery
扩展自AbstractJpaQuery
抽象类,并且AbstractJpaQuery
实现了RepositoryQuery
接口。
查询AbstractJpaQuery
抽象类的子类我们看到,除PartTreeJpaQuery
的实现外还有好多其他的实现,但这些实现并不是我们关注的重点,所以暂时忽略。
回到PartTreeJpaQuery
实现,从该类的注释中我们获知,该类是基于PartTree
的一个AbstractJpaQuery
的实现。
AbstractJpaQuery implementation based on a PartTree.
打开PartTree
类的源代码我们终于发现了奥秘所在之处:(代码注释)
Class to parse a String into a tree or PartTree.OrParts consisting of simple Part instances in turn. Takes a domain class as well to validate that each of the Parts are referring to a property of the domain class. The PartTree can then be used to build queries based on its API instead of parsing the method name for each query execution.
根据注释我们知道,该类通过将一个字符串(其实是Repository
里面定义的方法名)分解成树状数据结构,或者说是分解成一系列
包含简单Part
实例的OrParts
来构建查询的。并且通过查询传入的domain class类型来验证每一个Part
所对应的字段是否有效。
通过阅读代码,我们知道该类是通过正则表达式来分解类方法名的。
主语对象(Subject)和谓语对象(Predicate)分别表示了方法名中的不同部分,譬如给定方法名
findDistinctUserByNameOrderByAge
通过上述正则表达式解析,则分成主语部分DistinctUserBy
和谓语部分NameOrderByAge
。
在Subject
类(主语类)中,还将继续通过正则表达式进行分解,提取法语中distinct
、count
、delete
和maxResults
几种属性。
在Predicate
类(谓语类)中,如果方法名中有AllIgnoreCase
或者AllIgnoringCase
,则首先从改方法名中剥除该字段,
并且标示alwaysIgnoreCase
无视大小写flag为true。然后,如果方法名中有OrderBy
则首先对该方法名使用OrderBy
进行分割,
再然后针对分割后不包含OrderBy
的部分,通过关键字Or
进行分割。譬如给定宾语
NameAndAgeOrGenderAndLocationOrderByDistanceAllIgnoringCase
通过分割后变成(AllIgnoringCase被剥离)
(NameAndAge)Or(GenderAndLocation)OrderBy(Distance)
其中(NameAndAge)
和(GenderAndLocation)
被分别包装成OrPart
对象作为节点;(Distance)
则被包装成OrderBySource
对象另行对待。另外,其中OrderBySource
对象只允许有1个。
在OrPart
中我们可以看到,它再次使用正则表达式,使用And
关键字对已分割的OrPart
进行分割,最后包装成Part
对象作为子节点。
这样来说的话,其实在Predicate
类中,实际上是已经构建了一颗语法树。还是拿之前的例子来说的话,就是
Predicate
根节点OrPart
子节点(NameAndAge
和GenderAndLocation
)Part
叶节点(Name
和Age
、Gender
和Location
,分别属于不同的上一级子节点,可以看到每一个叶节点均为实体类的属性)
在Part
类中我们看到,除了针对该叶节点是否无视大小写的处理外,还分别解析了改叶节点的类型(由内部枚举类型Type
定义)和
针对实例类型的属性路径(使用PropertyPath#from
方法构建的PropertyPath
类型属性)。
其中叶节点类型Type
分别定义了该字段类型对应的关键字(Between
、Exists
、Like
、NotNull
等)和参数个数
(譬如Between
需要2个参数才能确定,而NotNull
则不需要参数)信息。
而PropertyPath
则定义了访问某一属性所需要的属性路径,譬如FindByLocation_Nation_Address
可能对应访问一个实体类的
entity.location.nation.address
的属性所需要的路径。
到此为止,针对Repository
中定义的方法名称创建查询的解析就已经做完了。完成后的解析会以PartTreeJpaQuery
对象的方式放置
在内存中(RepositoryFactorySupport#QueryExecutorMethodInterceptor.queries
,注意是个ConcurrentHashMap
)。
待到相应的查询执行时,就会从queries
中取出并执行相应的查询。
需要特别注意的是,这些放置在内存中的PartTreeJpaQuery
会在初始化阶段(构造器中)去创建相应的JPA CriteriaQuery
,
所以如果我们在Repository
接口中不小心定义了一个错误的方法名,在Spring容器启动时就应该能看到报错了。
到此为止,Repository
方法名查询推导(Query Derivation From Method Names)的实现原理就算理解完成了,你看懂了么?
- 完 -