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

背景

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

  • 使用Query注解编写SQL/HQL时,参数不能动态设置
    MyBatis 有<c:if>这种标签来动态控制参数,而jpa没有,事实上很多业务场景都需要动态控制参数,jpa 遇到这种场景,一般有这几种方式实现:

    1. 使用 EntityManager.createNativeQuery,动态拼接SQL,如下案例:
      image.png
    2. 使用 QueryByExampleExecutor(QBE)方式,类似如下:
      image.pngimage.png
    3. 使用 JpaSpecificationExecutor 方式,类似如下:
      image.png
    4. @Query 动态参数方式,如下:
    @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对象,再通过构造方法,接收查询结果(或者用接口去接收)如下图:
    image.png

针对以上的两点,我就想自己扩展jpa框架,对jpa做一个增强。

增强的功能

动态参数支持以下写法:

1. 占位符position解析方式
image.png
如上图,对于需要动态传参的参数使用占位符 ?{} 包裹起来,参数可以使用 ?1 等占位符;

2. 占位符name参数名解析方式
image.png
如上图所示,对于需要动态传参的参数仍然使用占位符 ?{} 包裹起来,但是参数可以使用 :name 方式占位,不过注意的是,参数名需要使用 @RequestParam 标注;

3. 非占位符position解析方式
image.png
如果你不想写 ?{} 占位符,也可以去掉,但是这种只支持如上图一样简单的条件,目前支持是类似如下条件:
and/or name =/>/</in/like ?1,不支持 between and,或者复杂的条件组合形式(and (name = ?1 or age = ?2))

4. 非占位符name参数名解析方式
image.png
如果你不想写 ?{} 占位符,也可以去掉,但是这种只支持如上图一样简单的条件,目前支持是类似如下条件:
and/or name =/>/</in/like :name,不支持 between and,或者复杂的条件组合形式(and (name = :name or age = :age))

5. 不需要动态参数的
这种是你需要解析动态参数的,但是你希望返回值能直接使用VO对象的情况,如下图:

image.png

注意,需要指定 expressionQuery = false,表示不需要解析动态参数;

使用Query注解返回VO对象

image.png
例如这种SQL,直接返回VO就行,字段也会自动使用驼峰映射的,可以作用于 nativeQuery 和 hql;

@Query注解分页/排序的使用

image.png
如上图,对于分页使用,和@Query使用分页一样,分页参数(PageRequest)需要放在最后一个参数位置,返回值使用 Page接收;如果是 sort ,就把 sort 放在最后一个参数;

无论是 nativeQuery 还是 hql,分页时候,都支持不需要写 countSQL(如果你写了优先使用你写的);

扩展思路

jpa 扩展点

Jpa 创建 Repository Bean 的流程具体可以看:JpaRepositoryBean创建流程分析

总的来说,这里有个很重要的类 RepositoryFactoryBeanSupport,它初始化Bean的后置方法 afterPropertiesSet 会创建 RepositoryFactory,并获取 Repository 的代理对象,而代理对象会增加一个拦截器 QueryExecutorMethodInterceptor,创建这个拦截器的时候会对 Repository的方法绑定一个 RepositoryQuery,这个对象就是执行SQL的关键;

image.png

具体调用链如下

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 对象

那么我们的扩展点就如下图:
image.png

初步想法

由上面jpa扩展点,可知获取 Repository 代理对象的时候会增加一个拦截器 QueryExecutorMethodInterceptor:

image.png

由上图可知,创建 QueryExecutorMethodInterceptor 时,会通过getQueryLookupStrategy方法会获取 Repository 方法的执行策略,然后会通过策略的 resolveQuery 方法绑定 Repository 方法对应执行的 RepositoryQuery;

来瞧一眼 RepositoryQuery 实现:
image.png

  • NativeJpaQuery: @Query(nativeQuery = true)底层实现类
  • SimpleJpaQuery:@Query(nativeQuery = false)底层实现类
  • PartTreeJpaQuery:方法名上没注解时的方法实现类。直接解析方法名

那我懂了,我可以自己实现一个 RepositoryQuery,通过代理设计模式/装饰者设计模式/委派设计模式, 我把需要把动态参数那部分处理好,然后再调用jpa自己的 RepositoryQuery 去处理SQL不就行了。

这看似没毛病,但是在实现时发现这样不好做。

分析一下为啥这样不行:

首先创建 QueryExecutorMethodInterceptor 时,getQueryLookupStrategy方法的默认实现:JpaRepositoryFactory.getQueryLookupStrategy
image.png
最终会调用 JpaQueryLookupStrategy.create方法:
image.png

假设我的扩展点放在 JpaQueryLookupStrategy 上:

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

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

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

实现的思路

既然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 );

实现细节

image.png
jpa扩展点我选择在继承 JpaRepositoryFactory 上,而 JpaRepositoryFactoryJpaRepositoryFactoryBean创建,所以我们需要扩展 JpaRepositoryFactoryBean

所以,在使用时,第一步就是在 EnableJpaRepositories 注解中指定jpa扩展的 factoryBean,如下图:
image.png

整个启动流程:

image.png
从上面的流程就把 repository 中的方法和 RepositoryQuery 绑定在一起了。

例如:

下面这个方法就绑定为 SimpleJpaExtendQuery 方式执行SQL;
image.png

下面这个方法就绑定为 NativeJpaExtendQuery 方式执行SQL;
image.png

那就是分析 SimpleJpaExtendQuery 和 NativeJpaExtendQuery 如何实现了。

如何确定使用 SimpleJpaExtendQuery 还是 NativeJpaExtendQuery 呢?

如上面介绍,我们需要自定义 JpaRepositoryFactoryBean,指定创建的 RepositoryFactory:
image.png

JpaExtendRepositoryFactory 重写 getQueryLookupStrategy方法,实现自己的策略:
image.png

创建 JpaQueryLookupStrategy,如果Repository方法的注解不是用 MyQuery,那么走jpa执行SQL的逻辑,如果是 MyQuery,判断是否是 nativeQuery,来确实使用 SimpleJpaExtendQuery 还是 NativeJpaExtendQuery。
image.png

AbstractJpaExtendQuery 实现细节

image.png
如上图,SimpleJpaExtendQuery 和 NativeJpaExtendQuery都是继承 AbstractJpaExtendQuery,它的核心方法是 doCreateQuery、doCreateCountQuery,那这两个方法什么时候调用呢?

可以看到它是 RepositoryQuery 的实现类,有个方法是 execute(),这个方法是RepositoryFactorySupport 的内部类 QueryExecutorMethodInterceptor调用的;
image.png

AbstractJpaQuery 是 RepositoryQuery 的抽象实现类, 然后会调用 AbstractJpaQuery.doExecute方法,这个方法会获取 JpaQueryExecution,调用JpaQueryExecution的 execute方法:
image.png

JpaQueryExecution最终会调用 AbstractJpaQuery 的 createQuery方法:
image.pngimage.png

那现在就清楚了,我们只要重写 doCreateQuery、doCreateCountQuery即可;

image.png

image.png

所以这里面最重要的是:
image.png

来看一下 ExpressionQueryResolverStrategy 的结构:
image.png
ExpressionQueryResolverStrategy 有个方法resolve,这个方法会遍历 ExpressionQueryResolverEnum 枚举,获取合适的解析策略;ExpressionQueryResolverEnum 实现 ExpressionQueryResolver 接口;

这个枚举代码:

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)作为参数占位符时,注意解析后的参数索引需要改变,举个例子:
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 语法错误,举个例子:
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
    image.pngimage.png

至此,解析结束,因为代码比较简单,所以简单说了一下思路,具体代码讲解的简单一点,有兴趣的可以看一下源码;

最重要的

代码仓库:
gitee: gitee.com/listen_w/sp…
github: github.com/jettwangcj/…

注意:spring-data-jpa-extend 分为 springboot 2.x版本和 springboot 3.x 版本,springboot 3.x版本在项目中未使用,请谨慎使用;

另外,这个项目我已经推送 Maven 中央仓库了,可以通过如下坐标使用:

<dependency>
    <groupId>cn.org.wangchangjiu</groupId>
    <artifactId>spring-data-jpa-extend</artifactId>
    <version>2.0.1-RELEASE</version>
</dependency>

2.x 表示使用 springboot 2.x版本;

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYEckvJc' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片