jpa动态参数以及结果集映射增强方案

背景

jpa 是使用非常广泛的orm框架,开发也非常方便,但是我觉得jpa的Query注解有两个不友好的地方:

  • 使用Query注解编写SQL/HQL时,参数不能动态设置

    MyBatis 有<c:if>这种标签来动态控制参数,而jpa没有,事实上很多业务场景都需要动态控制参数,jpa 遇到这种场景,一般有这几种方式实现:

    1. 使用 EntityManager.createNativeQuery,动态拼接SQL,如下案例:
    2. 使用 QueryByExampleExecutor(QBE)方式,类似如下:
    3. 使用 JpaSpecificationExecutor 方式,类似如下:
    4. @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 上:

  1. 利用继承: 那么我就自己实现一个策略:MyJpaQueryLookupStrategy 继承 JpaQueryLookupStrategy,重写它的create方法,目的是创建自己的 QueryLookupStrategy,先处理动态参数,然后再调用 jpa 的QueryLookupStrategy实现类去执行。但是 JpaQueryLookupStrategy 是final的,所以不能继承;

  2. 利用组合(代理/委派/装饰者): 组合就需要持有 CreateQueryLookupStrategy/DeclaredQueryLookupStrategy,然后发现它们是私有的。

那既然这样我,我就不用 jpa 的 JpaQueryLookupStrategy 了,我只用 jpa 的NativeJpaQuery/SimpleJpaQuery,要用这两个,就需要持有它们,但一看,心都凉了,这两个类是包访问权限,压根访问不了;

实现的思路

既然jpa 的很多类都不能持有,也不可以继承,那就不用jpa那套了,自己写一个注解去解析SQL的动态参数,然后通过EntityManager.createNativeQuery/EntityManager.createQuery去执行SQL;

  1. 那么就是如果使用jpa 的 @Query 注解,就使用jpa的JpaQueryLookupStrategy去创建QueryLookupStrategy,使用我自定义的@MyQuery,就自己处理,所以需要实现一个RepositoryFactory,重写JpaRepositoryFactory.getQueryLookupStrategy方法;
  2. 需要分开考虑 nativeSQL 和 hql,还要考虑分页/排序等;
  3. 使用正则表达式匹配需要处理的动态参数;
  4. 支持 position类型参数(where条件使用 ?1)和 name类型的参数(where 条件使用 :name );

实现细节

jpa扩展点我选择在继承 JpaRepositoryFactory 上,而 JpaRepositoryFactoryJpaRepositoryFactoryBean创建,所以我们需要扩展 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条件;

上面值得注意的是:

  1. 使用 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;

  1. 解析后出现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;

  1. 返回值可以直接使用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版本;

相关推荐
V+zmm101346 分钟前
基于微信小程序的乡村政务服务系统springboot+论文源码调试讲解
java·微信小程序·小程序·毕业设计·ssm
Oneforlove_twoforjob32 分钟前
【Java基础面试题025】什么是Java的Integer缓存池?
java·开发语言·缓存
xmh-sxh-131434 分钟前
常用的缓存技术都有哪些
java
搬码后生仔1 小时前
asp.net core webapi项目中 在生产环境中 进不去swagger
chrome·后端·asp.net
凡人的AI工具箱1 小时前
每天40分玩转Django:Django国际化
数据库·人工智能·后端·python·django·sqlite
AiFlutter1 小时前
Flutter-底部分享弹窗(showModalBottomSheet)
java·前端·flutter
J不A秃V头A2 小时前
IntelliJ IDEA中设置激活的profile
java·intellij-idea
DARLING Zero two♡2 小时前
【优选算法】Pointer-Slice:双指针的算法切片(下)
java·数据结构·c++·算法·leetcode
Lx3522 小时前
Pandas数据重命名:列名与索引为标题
后端·python·pandas
小池先生2 小时前
springboot启动不了 因一个spring-boot-starter-web底下的tomcat-embed-core依赖丢失
java·spring boot·后端