声明式查询服务,只需定义,无需实现
创始人
2024-02-01 18:04:51
0

1. 概览

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

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

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

1.1. 背景

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

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

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

  1. 主流程代码如下:
public Page pageByUserId(@Valid @NotNull(message = "查询参数不能为 null") PageByUserId query) {Page 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 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,具体如下:

com.geekhalo.legolego-starter0.1.9-query_proxy-SNAPSHOT

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

org.springframework.bootspring-boot-starter-validation

org.springframework.bootspring-boot-starter-data-jpa

在 application 文件中添加 Datasource 配置:

spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://127.0.0.1:3306/legousername: rootpassword: rootjpa:hibernate:ddl-auto: updateshow-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 pageByUserId(@Valid @NotNull(message = "查询参数不能为 null") PageByUserId query);List getByUserId(@Valid @NotNull(message = "查询参数不能为 null") GetByUserId getByUserId);Long countByUser(@Valid @NotNull(message = "查询参数不能为 null") CountByUserId countByUserId);List getPaidByUserId(Long id);
}

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

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

OrderQueryRepository 继承:

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

2.3. 常见功能

2.3.1. 入参校验

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

接口定义如下:

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

执行如下代码:

this.getQueryService().getByUserId(null);

抛出验证异常

javax.validation.ConstraintViolationException: getByUserId.getByUserId: 查询参数不能为 nullat 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 * @param */
public interface QueryResultConverter {/*** 是否能支持对应类型的转换* @param input*      输入类型* @param output*      输出类型* @return*/boolean support(Class input, Class output);/*** 进行模型转换* @param input* @return*/O convert(I input);
}

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

@Data
public class OrderDetail {private Order order;@JoinItemByOrder(keyFromSourceData = "#{order.id}")private List 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 pageByUserId(@Valid @NotNull(message = "查询参数不能为 null") PageByUserId query);

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

QueryObjectRepository 的 pageOf 定义如下:

 Page pageOf(Q query);

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

2.3.5. 自定义查询

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

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

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

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

@Service
public class CustomOrderQueryServiceImpl implements CustomOrderQueryService{@Autowiredprivate JoinService joinService;@Autowiredprivate OrderQueryRepository orderQueryRepository;@Overridepublic List getPaidByUserId(Long id) {List orders = orderQueryRepository.getByUserIdAndStatus(id, OrderStatus.PAID);List 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 代理对象;

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
一帆风顺二龙腾飞三阳开泰祝福语... 本篇文章极速百科给大家谈谈一帆风顺二龙腾飞三阳开泰祝福语,以及一帆风顺二龙腾飞三阳开泰祝福语结婚对应...
美团联名卡审核成功待激活(美团... 今天百科达人给各位分享美团联名卡审核成功待激活的知识,其中也会对美团联名卡审核未通过进行解释,如果能...