声明式查询服务,只需定义,无需实现

news2025/1/16 3:53:52

1. 概览

在日常开发中,数据查询是最为常见的需求,也是占比最大的一部分。为了降低成本提升开发效率,已经封装了两个组件:

  1. 将 QueryObject 与 Spring Data Jpa 进行集成,无需编写实现代码,只需通过注解定义查询对象,并能完成单表的普通查询、列表查询、分页查询等;
  2. 内存 Join 组件,通过注解对关联对象进行标记,框架自动完成数据的抓取,也无需编写实现代码;

两个组件,基本都能做到只“声明能力”,不“编写代码”,提升开发效率的同时,降低了bug概率。但,在两者结合使用时,就需要编写实现代码,将能力粘合起来。

1.1. 背景

在日常开发中,一个查询请求主要由以下几部分组成:

  1. 验证入参的有效性;
  2. 查询数据库获得主实体数据;
  3. 查询关联数据并完成结果的组装;

在lego框架中,三个步骤都提供了相应的组件进行支持,以一个订单分页查询为例:

  1. 主流程代码如下:
public Page<OrderDetail> pageByUserId(@Valid @NotNull(message = "查询参数不能为 null") PageByUserId query) {
    Page<OrderDetail> orderDetailPage = this.orderQueryRepository.pageOf(query, OrderDetail::new);
    if (orderDetailPage.hasContent()){
        this.joinService.joinInMemory(orderDetailPage.getContent());
    }
    return orderDetailPage;
}
  1. 查询参数定义如下:
@Data
public class PageByUserId {
    @NotNull(message = "user id 不能为 null")
    @FieldEqualTo("userId")
    private Long userId;
    private Pageable pageable;
}
  1. 返回结果如下:
@Data
public class OrderDetail {
    private Order order;
    @JoinItemByOrder(keyFromSourceData = "#{order.id}")
    private List<OrderItem> orderItems;
    public OrderDetail(Order order){
        this.order = order;
    }
}

查询三大步骤均基于“声明式注解”通过描述的方式进行实现,然后通过 编码 的方式完成主流程。

仔细观察主流程,会发现这是一套标准的“模板代码”,重复枯燥、没有业务价值,像这样有规律的“重复”代码,就应交由框架实现。

1.2. 目标

构建声明式 QueryService,只需定义方法,无需编写实现代码,便能完成大多数场景的数据查询。

组件应具有如下特性:

  1. 只定义接口,由框架负责具体实现;
  2. 保留参数校验、单表查询和内存join等全套能力;
  3. 对于个性化需求,提供扩展点,可通过 coding 方式实现;
  4. 启动时进行有效性校验,避免运行时异常;

2. 快速入门

设计目标与 Spring Data 的设计理念高度相似,QueryService 组件在实现上进行了借鉴,在使用上也与 Spring Data 保存一致,以降低使用门槛。

2.1. 环境搭建

首先,在项目中引入 lego-starter,具体如下:

<dependency>
    <groupId>com.geekhalo.lego</groupId>
    <artifactId>lego-starter</artifactId>
    <version>0.1.9-query_proxy-SNAPSHOT</version>
</dependency>

然后,依次引入 validation 和 spring data jpa 支持

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

在 application 文件中添加 Datasource 配置:

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/lego
    username: root
    password: root
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true

在启动类上通过注解开启 JpaRepository 和 QueryService 支持

@SpringBootApplication
@EnableJpaRepositories(basePackages = {
        "com.geekhalo.lego.query"
}, repositoryFactoryBeanClass = JpaBasedQueryObjectRepositoryFactoryBean.class)
@EnableQueryService(basePackages = "com.geekhalo.lego.query")
public class DemoApplication {
    public static void main(String[] args){
        SpringApplication.run(DemoApplication.class, args);
    }
}

其中:

  1. @EnableJpaRepositories 开启 JpaRepository 的支持,并通过设置 JpaBasedQueryObjectRepositoryFactoryBean 完成与 QueryObject 模型的集成;basePackages 指定自动扫描的包路径;
  2. @EnableQueryService 开启 QueryService 的支持,并通过basePackages指定自动扫描的包路径;

2.2. 定义 OrderQueryService

@QueryServiceDefinition(domainClass = Order.class,
        repositoryClass = OrderQueryRepository.class)
@Validated
public interface OrderQueryServiceProxy extends OrderQueryService {
    OrderDetail getById(@Valid @NotNull(message = "订单号不能为null") Long id);
    Page<OrderDetail> pageByUserId(@Valid @NotNull(message = "查询参数不能为 null") PageByUserId query);
    List<OrderDetail> getByUserId(@Valid @NotNull(message = "查询参数不能为 null") GetByUserId getByUserId);
    Long countByUser(@Valid @NotNull(message = "查询参数不能为 null") CountByUserId countByUserId);
    List<OrderDetail> getPaidByUserId(Long id);
}

定义 OrderQueryService 接口,添加相关注解:

  1. @QueryServiceDefinition 标记该接口为查询接口,将自动为其生成代理,其中
  2. domainClass 为查询实体的类型
  3. repositoryClass 为查询服务所使用的底层仓库
  4. @Validated 注解启动验证框架,对验证注解进行处理;
  5. OrderQueryRepository 也只有定义没有实现,具体定义如下:
public interface OrderQueryRepository
        extends JpaRepository<Order, Long>,
        QueryRepository<Order, Long> {
    Order getById(Long id);
    List<Order> getByUserIdAndStatus(Long id, OrderStatus paid);
}

OrderQueryRepository 继承:

  1. JpaRepository, 拥有 JpaRepository 中基本的查询功能
  2. QueryRepository,拥有 QueryObject 查询功能

2.3. 常见功能

2.3.1. 入参校验

框架对 validation 验证体系进行集成,只需在接口和方法上增加注解,该接口便拥有参数验证能力。

接口定义如下:

List<OrderDetail> getByUserId(@Valid @NotNull(message = "查询参数不能为 null") GetByUserId getByUserId);

执行如下代码:

this.getQueryService().getByUserId(null);

抛出验证异常

javax.validation.ConstraintViolationException: getByUserId.getByUserId: 查询参数不能为 null
    at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:120)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
    at com.sun.proxy.$Proxy142.getByUserId(Unknown Source)

2.3.2. 模型转换和数据填充

通常情况下,repository 查询方法只会返回实体对象,框架会自动将其转化为结果对象,并使用 JoinService 完成数据填充。

框架自动查找模型转换方案,具体如下:

  1. 查找结果对象的静态方法,入参为实体对象,返回值为结果对象;
  2. 查找结果对象的构造函数,入参为实体对象;
  3. 查找 Spring 中的 QueryResultConverter 实现;

QueryResultConverter 的定义如下:

/**
 * 结构转化器,对查询结果进行封装
 * @param <I>
 * @param <O>
 */
public interface QueryResultConverter<I, O> {
    /**
     * 是否能支持对应类型的转换
     * @param input
     *      输入类型
     * @param output
     *      输出类型
     * @return
     */
    boolean support(Class<I> input, Class<O> output);
    /**
     * 进行模型转换
     * @param input
     * @return
     */
    O convert(I input);
}

其中,OrderDetail 通过构造函数进行模型转换,代码如下:

@Data
public class OrderDetail {
    private Order order;
    @JoinItemByOrder(keyFromSourceData = "#{order.id}")
    private List<OrderItem> orderItems;
    public OrderDetail(Order order){
        this.order = order;
    }
}

2.3.3. 使用 Repositry 方法

QueryService 可以直接使用 QueryRepository 中的方法进行实体查询。

OrderQueryRepository 存在一个 getById 方法,具体如下:

Order getById(Long id);

OrderQueryServiceProxy 也存在一个 getById 方法,具体如下:

OrderDetail getById(@Valid @NotNull(message = "订单号不能为null") Long id);

两者,除返回值不同,方法名和入参均相同,框架会根据 方法名+入参 选择合适的查询方法。

2.3.4. 使用通用方法

除了直接使用 QueryRepository 方法外,QueryService 也会使用 QueryObject 完成查询。

OrderQueryServiceProxy 存在一个分页查询 pageByUserId,具体定义如下:

Page<OrderDetail> pageByUserId(@Valid @NotNull(message = "查询参数不能为 null") PageByUserId query);

而在 OrderQueryRepository 中并未定义 pageByUserId 方法,此时 QueryService 会直接使用 QueryObjectRepository 中的 pageOf 完成数据查询。

QueryObjectRepository 的 pageOf 定义如下:

<Q> Page<E> pageOf(Q query);

QueryService 将忽略方法名,基于入参和返回结果的兼容性对方法进行筛选。

2.3.5. 自定义查询

当业务非常复杂,QueryService 默认实现无法满足时,可以通过自定义方式对实现进行扩展。

首先,需要定义一个 自定义接口,如:

public interface CustomOrderQueryService {
    List<OrderDetail> getPaidByUserId(Long id);
}

其次,根据业务逻辑实现自定义接口,如:

@Service
public class CustomOrderQueryServiceImpl implements CustomOrderQueryService{
    @Autowired
    private JoinService joinService;
    @Autowired
    private OrderQueryRepository orderQueryRepository;
    @Override
    public List<OrderDetail> getPaidByUserId(Long id) {
        List<Order> orders = orderQueryRepository.getByUserIdAndStatus(id, OrderStatus.PAID);
        List<OrderDetail> orderDetails = orders.stream()
                .map(OrderDetail::new)
                .collect(Collectors.toList());
        this.joinService.joinInMemory(orderDetails);
        return orderDetails;
    }
}

最后,让 QueryService 继承自定义接口即可:

public interface OrderQueryServiceProxy extends CustomOrderQueryService{
}

在调用 getPaidByUserId 方法时,会将请求转发给
CustomOrderQueryServiceImpl 的 getPaidByUserId 实现。

对于自定义接口的实现类,默认使用 Impl 作为后置,如有必要,可通过 @EnableQueryService 的
queryImplementationPostfix 进行调整。

3. 核心设计

3.1. Proxy 结构

为 QueryService 自动实现的 Proxy 结构如下:

 

image

Proxy 实现 自定义的QueryService 接口,并将方法调用分发给不同的实现,核心拦截器包括:

  1. DefaultMethodInvokingMethodInterceptor。拦截对默认方法的调用,将请求转发给代理对象;
  2. 基于自定义实现的 QueryServiceMethodDispatcherInterceptor,将请求转发给自定义实现类;
  3. 基于自动创建 QueryServiceMethod 的 QueryServiceMethodDispatcherInterceptor,根据方法签名自动实现查询逻辑,并将请求转发给 QueryServiceMethod;

3.2. 初始化流程

以下是整个框架的初始化流程:

 

image

通过 @EnableQueryService 注解开启 QueryService 支持后,将向 Spring 容器完成
QueryServiceBeanDefinitionRegistrar 的注册。

  1. QueryServiceBeanDefinitionScanner 根据 basePackages 设置,自动对带有@QueryServiceDefinition的接口进行扫描;
  2. 扫描到带有@QueryServiceDefinition注解的接口后,将其封装为 QueryServiceProxyFactoryBean,并将其注册到 Spring 容器;
  3. Spring 实例化 QueryServiceProxyFactoryBean 生成对应的 QueryService 代理对象;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/19081.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

大厂永恒敲门砖——Android 系统启动流程详解

正文 AMS 是 Android 中最核心的服务之一&#xff0c;主要负责系统中四大组件的启动、切换、调度及应用进程的管理和调度等工作&#xff0c;其职责与操作系统中的进程管理和调度模块相类似&#xff0c;它本身也是一个 Binder 的实现类&#xff0c;应用进程能通过 Binder 机制调…

【Java开发】 Spring 04:云服务器 Docker 环境下安装 Redis 并连接 Spring 项目实现简单 CRUD

Redis是目前使用最多的缓存&#xff0c;包括Spring Boot 中我们也是会用Redis做很多事情。它是完全开源免费的&#xff0c;遵守BSD协议&#xff0c;是一个高性能的key-value数据库&#xff0c;具备数据持久化、多数据结构存储及数据备份等特点。Redis 和 Mongo 同属于文档型数据…

C++17新特性的使用场景总结

一、简单特性 1. namespace 嵌套 C17使我们可以更加简洁使用命名空间&#xff1a; 2. std::variant 升级版的C语言Union 在C17之前&#xff0c;通常使用Union来定义一个可以存储不同类型的变量&#xff0c;现在可以通过std::variant<T1,T2,...> 来定义一个可以存储不同…

Day2.

文章目录一、回文日期1、题目描述2、输入描述3、输出描述4、参考代码二、杨辉三角形1、题目描述2、输入描述3、输出描述4、参考代码一、回文日期 1、题目描述 2020 年春节期间&#xff0c;有一个特殊的日期引起了大家的注意&#xff1a;2020 年 2 月 2 日。因为如果将这个日期…

【2022年11月19日提高A组】消圈策略【DP】

思路&#xff1a; dis i 代表1 到 i 的最短路 因为它要求没有负环 无负环和可以求出最短路是充要条件 然后dis i 和 dis (i1)相差最多不超过1 因为有0边的存在 然后就对边的大小进行分类讨论 如果i<j即e i,j 为-1 那么dis (i) - 1 > dis j 然后移项 dis i - dis j > 1…

云原生|kubernetes|部署MySQL一主多从复制集群(基于GTID的复制)

前言&#xff1a; 一&#xff0c; MySQL的主从复制优点如下&#xff1a; 数据更安全&#xff1a;做了数据冗余&#xff0c;不会因为单台服务器的宕机而丢失数据 性能大大提升&#xff1a;一主多从&#xff0c;不同用户从不同数据库读取&#xff0c;性能提升 扩展性更优&…

如何使用Jekyll在GitHub Pages上搭建网站(个人博客)

本文很长&#xff0c;建议使用侧边栏进行跳转。 前言 Jekyll 是一个基于 Ruby 语言的&#xff0c;用于搭建静态网站的生成器&#xff0c;主要用于搭建博客网站&#xff08;官方自己的介绍为&#xff1a;Jekyll is a blog-aware, static site generator in Ruby&#xff09;。…

html制作网页案例代码----(故宫博物馆9页)特效很多

⛵ 源码获取 文末联系 ✈ Web前端开发技术 描述 网页设计题材&#xff0c;DIVCSS 布局制作,HTMLCSS网页设计期末课程大作业 茶文化网站 | 中华传统文化题材 | 京剧文化水墨风书画 | 中国民间年画文化艺术网站 | 等网站的设计与制作 | HTML期末大学生网页设计作业&#xff0c;We…

Tauri+Vite+Vue3创建项目步骤

1、按照官网文档创建tauri项目 npm create tauri-app 然后按顺序填&#xff1a; 1、项目名称&#xff1a; 2、选择你的package manager 3、选择你的前端框架 然后再一步一步执行下面命令 cd 刚才创建的项目名称 npm install npm run tauri dev 然后出现 第一次可能没有反…

基于java+ssm的理论课_考勤,作业,签到管理系统

本理论课管理系统采用目前最流行的ssm框架和eclipse编辑器、mysql数据库设计并实现的 。主要包括登录模块、课程信息管理模块、考勤信息管理模块、成绩管理模块、和退出模块等多个模块 本系统基于SSM(SpringSpringMVCMyBatis)框架,适用于毕业设计&#xff0c;采用javaweb,基于B…

Android 驾车出行路线规划

好久没有写了&#xff0c;今天又是加班的一天&#xff0c;先随便写点东西吧。 最近在搞一款自驾游的项目&#xff0c;需要用到地图&#xff0c;扒了点高德上的数据&#xff0c;便集成了高德地图SDK。之前的项目大部分都只是用了定位&#xff0c;其中有一款也用了地图poi的搜索…

js的promise的究竟是同步还是异步的问题和promise.all可以同时请求多个接口是错误的回答的原因

个人理解 前景-代码输出结果是什么 我们都知道,循环队列的时候,同步任务大于异步任务(异步任务里面的微任务又大于宏任务),那么你猜猜这个代码输出结果是 什么呢 <script>setTimeout(() > {console.log(1); }, 0);new Promise(function(resolve,reject){console.lo…

各省市ZF工作BG环境规制环境词汇词频分析(2009-2019年)

各省市ZF工作报告及环境词汇词频分析 1、时间&#xff1a;2009-2019年 2、说明&#xff1a;环保词频汇总分析&#xff1a;通过手工搜集整理出2009-2019年全国31个省ZF工作报告中环境规制相关的环保词汇及环保词频。 3、词汇包括&#xff1a; 环境保护、环保、污染、能耗、减…

NeurIPS 22|四分钟内就能训练目标检测器!( AGVM)

文章目录引言方法介绍实验过程结果分析引言 来自商汤的基模型团队和香港大学等机构的研究人员提出了一种大批量训练算法 AGVM&#xff0c;该研究已被NeurIPS 2022接收。 本文提出了一种大批量训练算法 AGVM (Adaptive Gradient Variance Modulator)&#xff0c;不仅可以适配于…

Shell脚本

文章目录Shell脚本学习1. Shell概念1.1Shell脚本的好处1.2 Shell脚本的入门1.2.1 Linux环境中默认Shell版本1.2.2 Shell脚本1.2.3 编写简单的hello,world 脚本1.2.4 Shell 脚本的多种执行方法1.2.4.1 第一种 bash 或 sh 加文件的路径1.2.4.2 第二种 文件的路径直接执行1.2.4.2.…

Day07--wxs的概念以及其基本的用法

一.概念 1.啥子是wxs呢&#xff1f; *****************************************************************************************************************************************************************************************************************************…

【附源码】Python计算机毕业设计特大城市地铁站卫生防疫系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

数字电路和模拟电路-10时序逻辑电路的分析和设计

前言&#xff1a;学习同步时序逻辑电路的分析、设计 一、同步时序逻辑电路的分析 1、时序逻辑电路的分析步骤 步骤一 逻辑图 同步or异步 计数器or状态机 一条总线同步&#xff0c;多条总线是异步 计数器无输入&#xff0c;状态机有输入 状态机还分摩尔型和米里型 步骤二 驱动…

力扣(LeetCode)17. 电话号码的字母组合(C++)

回溯 将 222——999 和字母对应起来&#xff0c;用字符串数组保存。 递归遍历 digitsdigitsdigits 每一个数字&#xff0c;每一个数字对应的字母&#xff0c;又可以递归遍历&#xff0c;和下一个数字的字母组成排列。当排列长度等于 digitsdigitsdigits 的长度&#xff0c;就…

详解MySQL非常重要的日志—bin log

前言 bin log想必大家多多少少都有听过&#xff0c;它是MySQL中一个非常重要的日志&#xff0c;所以各位架构师们&#xff0c;如果有不了解的&#xff0c;一定要好好学习了&#xff0c;因为它涉及到数据库层面的主从复制、高可用等设计。 bin log是什么&#xff1f; bin log…