背景
jpa 是使用非常广泛的orm框架,开发也非常方便,但是我觉得jpa的Query注解有两个不友好的地方:
-
使用Query注解编写SQL/HQL时,参数不能动态设置
MyBatis 有<c:if>这种标签来动态控制参数,而jpa没有,事实上很多业务场景都需要动态控制参数,jpa 遇到这种场景,一般有这几种方式实现:
- 使用 EntityManager.createNativeQuery,动态拼接SQL,如下案例:
- 使用 QueryByExampleExecutor(QBE)方式,类似如下:
- 使用 JpaSpecificationExecutor 方式,类似如下:
- @Query 动态参数方式,如下:
java@Query("select u.name as name,u.email as email from User u where (:name is null or u.name =:name) and (:email is null or u.email =:email)") UserOnlyName findByUser(@Param("name") String name,@Param("email") String email);
第一种方式需要自己拼接原生SQL,如果是分页情况,还需要写获取总数等相关SQL,使用比较麻烦;
第二种和第三种都是自己拼接出需要的参数,分页可以不需要特别处理,但是只能单表操作,不能做join;
第四种虽然解决了动态SQL问题,但是写的SQL不够友好,而且也会影响性能(可能索引会失效);
-
使用Query注解返回VO对象,需要利用 JPQL,new 了一个 利用 JPQL,new 了一个 VO对象,再通过构造方法,接收查询结果(或者用接口去接收)如下图:
针对以上的两点,我就想自己扩展jpa框架,对jpa做一个增强。
增强的功能
动态参数支持以下写法:
1. 占位符position解析方式 如上图,对于需要动态传参的参数使用占位符 ?{}
包裹起来,参数可以使用 ?1 等占位符;
2. 占位符name参数名解析方式 如上图所示,对于需要动态传参的参数仍然使用占位符 ?{}
包裹起来,但是参数可以使用 :name 方式占位,不过注意的是,参数名需要使用 @RequestParam 标注;
3. 非占位符position解析方式 如果你不想写 ?{}
占位符,也可以去掉,但是这种只支持如上图一样简单的条件,目前支持是类似如下条件: and/or name =/>/</in/like ?1
,不支持 between and,或者复杂的条件组合形式(and (name = ?1 or age = ?2))
4. 非占位符name参数名解析方式 如果你不想写 ?{}
占位符,也可以去掉,但是这种只支持如上图一样简单的条件,目前支持是类似如下条件: and/or name =/>/</in/like :name
,不支持 between and,或者复杂的条件组合形式(and (name = :name or age = :age))
使用Query注解返回VO对象
例如这种SQL,直接返回VO就行,字段也会自动使用驼峰映射的,可以作用于 nativeQuery 和 hql;
注意,需要指定 expressionQuery = false
,表示不需要解析动态参数;
@Query注解分页/排序的使用
如上图,对于分页使用,和@Query使用分页一样,分页参数(PageRequest)需要放在最后一个参数位置,返回值使用 Page接收;如果是 sort ,就把 sort 放在最后一个参数;
无论是 nativeQuery 还是 hql,分页时候,都支持不需要写 countSQL(如果你写了优先使用你写的);
扩展思路
jpa 扩展点
Jpa 创建 Repository Bean 的流程具体可以看:JpaRepositoryBean创建流程分析
总的来说,这里有个很重要的类 RepositoryFactoryBeanSupport,它初始化Bean的后置方法 afterPropertiesSet 会创建 RepositoryFactory,并获取 Repository 的代理对象,而代理对象会增加一个拦截器 QueryExecutorMethodInterceptor,创建这个拦截器的时候会对 Repository的方法绑定一个 RepositoryQuery,这个对象就是执行SQL的关键;
具体调用链如下:
kotlin
org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.afterPropertiesSet()
org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.createRepositoryFactory()
org.springframework.data.repository.core.support.RepositoryFactorySupport.getRepository(Class<T>, RepositoryFragments) // 获取代理目标对象
org.springframework.data.repository.core.support.RepositoryFactorySupport.getTargetRepository(RepositoryInformation)
org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.QueryExecutorMethodInterceptor(RepositoryInformation, ProjectionFactory, Optional<QueryLookupStrategy>, NamedQueries, List<QueryCreationListener<?>>)
org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy.DeclaredQueryLookupStrategy.resolveQuery(JpaQueryMethod, EntityManager, NamedQueries) --在这里校验,并决策出RepositoryQuery 对象
那么我们的扩展点就如下图:
初步想法
由上面jpa扩展点,可知获取 Repository 代理对象的时候会增加一个拦截器 QueryExecutorMethodInterceptor:
由上图可知,创建 QueryExecutorMethodInterceptor 时,会通过getQueryLookupStrategy方法会获取 Repository 方法的执行策略,然后会通过策略的 resolveQuery 方法绑定 Repository 方法对应执行的 RepositoryQuery;
来瞧一眼 RepositoryQuery 实现:
- NativeJpaQuery: @Query(nativeQuery = true)底层实现类
- SimpleJpaQuery:@Query(nativeQuery = false)底层实现类
- PartTreeJpaQuery:方法名上没注解时的方法实现类。直接解析方法名
那我懂了,我可以自己实现一个 RepositoryQuery,通过代理设计模式/装饰者设计模式/委派设计模式, 我把需要把动态参数那部分处理好,然后再调用jpa自己的 RepositoryQuery 去处理SQL不就行了。
这看似没毛病,但是在实现时发现这样不好做。
分析一下为啥这样不行:
首先创建 QueryExecutorMethodInterceptor 时,getQueryLookupStrategy方法的默认实现:JpaRepositoryFactory.getQueryLookupStrategy 最终会调用 JpaQueryLookupStrategy.create方法:
假设我的扩展点放在 JpaQueryLookupStrategy 上:
-
利用继承: 那么我就自己实现一个策略:MyJpaQueryLookupStrategy 继承 JpaQueryLookupStrategy,重写它的create方法,目的是创建自己的 QueryLookupStrategy,先处理动态参数,然后再调用 jpa 的QueryLookupStrategy实现类去执行。但是 JpaQueryLookupStrategy 是final的,所以不能继承;
-
利用组合(代理/委派/装饰者): 组合就需要持有 CreateQueryLookupStrategy/DeclaredQueryLookupStrategy,然后发现它们是私有的。
那既然这样我,我就不用 jpa 的 JpaQueryLookupStrategy 了,我只用 jpa 的NativeJpaQuery/SimpleJpaQuery,要用这两个,就需要持有它们,但一看,心都凉了,这两个类是包访问权限,压根访问不了;
实现的思路
既然jpa 的很多类都不能持有,也不可以继承,那就不用jpa那套了,自己写一个注解去解析SQL的动态参数,然后通过EntityManager.createNativeQuery/EntityManager.createQuery去执行SQL;
- 那么就是如果使用jpa 的 @Query 注解,就使用jpa的JpaQueryLookupStrategy去创建QueryLookupStrategy,使用我自定义的@MyQuery,就自己处理,所以需要实现一个RepositoryFactory,重写JpaRepositoryFactory.getQueryLookupStrategy方法;
- 需要分开考虑 nativeSQL 和 hql,还要考虑分页/排序等;
- 使用正则表达式匹配需要处理的动态参数;
- 支持 position类型参数(where条件使用 ?1)和 name类型的参数(where 条件使用 :name );
实现细节
jpa扩展点我选择在继承 JpaRepositoryFactory
上,而 JpaRepositoryFactory
由 JpaRepositoryFactoryBean
创建,所以我们需要扩展 JpaRepositoryFactoryBean
;
所以,在使用时,第一步就是在 EnableJpaRepositories
注解中指定jpa扩展的 factoryBean
,如下图:
整个启动流程:
从上面的流程就把 repository 中的方法和 RepositoryQuery 绑定在一起了。
例如:
下面这个方法就绑定为 SimpleJpaExtendQuery 方式执行SQL;
下面这个方法就绑定为 NativeJpaExtendQuery 方式执行SQL;
那就是分析 SimpleJpaExtendQuery 和 NativeJpaExtendQuery 如何实现了。
如何确定使用 SimpleJpaExtendQuery 还是 NativeJpaExtendQuery 呢?
如上面介绍,我们需要自定义 JpaRepositoryFactoryBean,指定创建的 RepositoryFactory:
JpaExtendRepositoryFactory 重写 getQueryLookupStrategy方法,实现自己的策略:
创建 JpaQueryLookupStrategy,如果Repository方法的注解不是用 MyQuery,那么走jpa执行SQL的逻辑,如果是 MyQuery,判断是否是 nativeQuery,来确实使用 SimpleJpaExtendQuery 还是 NativeJpaExtendQuery。
AbstractJpaExtendQuery 实现细节
如上图,SimpleJpaExtendQuery 和 NativeJpaExtendQuery都是继承 AbstractJpaExtendQuery,它的核心方法是 doCreateQuery、doCreateCountQuery,那这两个方法什么时候调用呢?
可以看到它是 RepositoryQuery 的实现类,有个方法是 execute(),这个方法是RepositoryFactorySupport 的内部类 QueryExecutorMethodInterceptor调用的;
AbstractJpaQuery 是 RepositoryQuery 的抽象实现类, 然后会调用 AbstractJpaQuery.doExecute方法,这个方法会获取 JpaQueryExecution,调用JpaQueryExecution的 execute方法:
JpaQueryExecution最终会调用 AbstractJpaQuery 的 createQuery方法:
那现在就清楚了,我们只要重写 doCreateQuery、doCreateCountQuery即可;
所以这里面最重要的是:
来看一下 ExpressionQueryResolverStrategy 的结构: ExpressionQueryResolverStrategy 有个方法resolve,这个方法会遍历 ExpressionQueryResolverEnum 枚举,获取合适的解析策略;ExpressionQueryResolverEnum 实现 ExpressionQueryResolver 接口;
这个枚举代码:
ini
enum ExpressionQueryResolverEnum implements ExpressionQueryResolver {
EmptyExpressionQueryResolver(){
@Override
public boolean match(String queryString, boolean expressionQuery) {
return !expressionQuery;
}
@Override
public QueryResolveResult resolve(String queryString, JpaParameters parameters, Object[] values) {
Matcher positionExpressionParameter = POSITION_EXPRESSION_PARAMETER.matcher(queryString);
boolean positionParam = false;
if(positionExpressionParameter.find()){
positionParam = true;
}
return new QueryResolveResult.EmptyQueryResolveResult(queryString, positionParam, parameters, values);
}
},
/**
* 占位符 Position 表达式 查询处理器
*
*/
PlaceholderPositionExpressionQueryResolver() {
@Override
public boolean match(String queryString, boolean expressionQuery) {
if(!expressionQuery){
return false;
}
Matcher expressionParameter = PLACEHOLDER_EXPRESSION_PARAMETER.matcher(queryString);
Matcher positionExpressionParameter = POSITION_EXPRESSION_PARAMETER.matcher(queryString);
return expressionParameter.find() && positionExpressionParameter.find();
}
@Override
public QueryResolveResult resolve(String queryString, JpaParameters parameters, Object[] values) {
// 是否包含 ?1
Matcher expressionParameter = PLACEHOLDER_EXPRESSION_PARAMETER.matcher(queryString);
// 使用 ? 注入参数的
List<Integer> removeParamIndex = new ArrayList<>();
while (expressionParameter.find()) {
// and t.name = ?1
String parameter = expressionParameter.group(1);
queryString = super.positionParameterProcessor(queryString, values, removeParamIndex, parameter);
}
String afterParseSQL = queryString.replace(PLACEHOLDER_PREFIX, BLANK_STR).replace(PLACEHOLDER_SUFFIX, BLANK_STR);
afterParseSQL = super.whereKeywordSyntaxErrorProcessor(afterParseSQL, removeParamIndex.size() == values.length);
return new QueryResolveResult.PositionExpressionQueryResolveResult(afterParseSQL, removeParamIndex, JpaExtendQueryUtils.toPositionMap(values));
}
},
/**
* 占位符 Name 表达式 查询处理器
*
*/
PlaceholderNameExpressionQueryResolver(){
@Override
public boolean match(String queryString, boolean expressionQuery) {
if(!expressionQuery){
return false;
}
Matcher expressionParameter = PLACEHOLDER_EXPRESSION_PARAMETER.matcher(queryString);
Matcher nameExpressionParameter = NAME_EXPRESSION_PARAMETER.matcher(queryString);
return expressionParameter.find() && nameExpressionParameter.find();
}
@Override
public QueryResolveResult resolve(String queryString, JpaParameters parameters, Object[] values) {
// 解析参数 所有参数 包括 null的
Map<String, Object> allQueryParams = JpaExtendQueryUtils.getParams(parameters, values);
List<String> removeParams = new ArrayList<>();
Matcher expressionParameter = PLACEHOLDER_EXPRESSION_PARAMETER.matcher(queryString);
while (expressionParameter.find()) {
// and t.name = :name
String matchExpression = expressionParameter.group();
queryString = super.nameParameterProcessor(queryString, allQueryParams, removeParams, matchExpression);
}
String afterParseSQL = queryString.replace(PLACEHOLDER_PREFIX, BLANK_STR).replace(PLACEHOLDER_SUFFIX, BLANK_STR);
afterParseSQL = this.whereKeywordSyntaxErrorProcessor(afterParseSQL, removeParams.size() == allQueryParams.size());
return new QueryResolveResult.NameExpressionQueryResolveResult(afterParseSQL, removeParams, allQueryParams);
}
},
PositionExpressionQueryResolver() {
@Override
public boolean match(String queryString, boolean expressionQuery) {
if(!expressionQuery){
return false;
}
return !PLACEHOLDER_EXPRESSION_PARAMETER.matcher(queryString).find() && NO_PLACEHOLDER_POSITION_EXPRESSION_PARAMETER.matcher(queryString).find();
}
@Override
public QueryResolveResult resolve(String queryString, JpaParameters parameters, Object[] values) {
Matcher expressionParameter = NO_PLACEHOLDER_POSITION_EXPRESSION_PARAMETER.matcher(queryString);
// 使用 ? 注入参数的
List<Integer> removeParamIndex = new ArrayList<>();
while (expressionParameter.find()) {
String parameter = expressionParameter.group();
queryString = super.positionParameterProcessor(queryString, values, removeParamIndex, parameter);
}
queryString = super.whereKeywordSyntaxErrorProcessor(queryString, removeParamIndex.size() == values.length);
return new QueryResolveResult.PositionExpressionQueryResolveResult(queryString, removeParamIndex, JpaExtendQueryUtils.toPositionMap(values));
}
},
NameExpressionQueryResolver() {
@Override
public boolean match(String queryString, boolean expressionQuery) {
if(!expressionQuery){
return false;
}
return !PLACEHOLDER_EXPRESSION_PARAMETER.matcher(queryString).find() && NO_PLACEHOLDER_NAME_EXPRESSION_PARAMETER.matcher(queryString).find();
}
@Override
public QueryResolveResult resolve(String queryString, JpaParameters parameters, Object[] values) {
Map<String, Object> allQueryParams = JpaExtendQueryUtils.getParams(parameters, values);
List<String> removeParams = new ArrayList<>();
Matcher expressionParameter = NO_PLACEHOLDER_NAME_EXPRESSION_PARAMETER.matcher(queryString);
while (expressionParameter.find()) {
// and t.name = :name
String matchExpression = expressionParameter.group();
queryString = super.nameParameterProcessor(queryString, allQueryParams, removeParams, matchExpression);
}
queryString = this.whereKeywordSyntaxErrorProcessor(queryString, removeParams.size() == allQueryParams.size());
return new QueryResolveResult.NameExpressionQueryResolveResult(queryString, removeParams, allQueryParams);
}
}
}
以上就是动态参数解析的代码,代码还是很简单,原理就是使用正则匹配,然后动态判断参数值是否为空,为空就去掉where条件;
上面值得注意的是:
- 使用 position 表达式(?1)作为参数占位符时,注意解析后的参数索引需要改变,举个例子:
ruby
select d from Dress d where ?{ classify = ?1 } ?{ and enable = ?2 } ?{ and (name like concat('%', ?3, '%') or id like concat('%', ?3, '%')) }
这段SQL中,如果 enable 是null, 需要将 ?3改成?2;
- 解析后出现where 语法错误,举个例子:
ruby
select d from Dress d where ?{ classify = ?1 } ?{ and enable = ?2 } ?{ and (name like concat('%', ?3, '%') or id like concat('%', ?3, '%')) }
上面这段SQL classify 为 null ,就会解析成 where and enable = ?1 ,所以要注意去掉 where 后面直接的and;
- 返回值可以直接使用VO封装,因为底层使用的是 EntityManager.createNativeQuery/EntityManager.createQuery
至此,解析结束,因为代码比较简单,所以简单说了一下思路,具体代码讲解的简单一点,有兴趣的可以看一下源码;
最重要的
代码仓库: gitee: gitee.com/listen_w/sp... github: github.com/jettwangcj/...
注意:spring-data-jpa-extend 分为 springboot 2.x版本和 springboot 3.x 版本,springboot 3.x版本在项目中未使用,请谨慎使用;
另外,这个项目我已经推送 Maven 中央仓库了,可以通过如下坐标使用:
xml
<dependency>
<groupId>cn.org.wangchangjiu</groupId>
<artifactId>spring-data-jpa-extend</artifactId>
<version>2.0.1-RELEASE</version>
</dependency>
2.x 表示使用 springboot 2.x版本;