在日常开发中,数据查询是最为常见的需求,也是占比最大的一部分。为了降低成本提升开发效率,已经封装了两个组件:
两个组件,基本都能做到只“声明能力”,不“编写代码”,提升开发效率的同时,降低了bug概率。但,在两者结合使用时,就需要编写实现代码,将能力粘合起来。
在日常开发中,一个查询请求主要由以下几部分组成:
在lego框架中,三个步骤都提供了相应的组件进行支持,以一个订单分页查询为例:
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;
}
@Data
public class PageByUserId {@NotNull(message = "user id 不能为 null")@FieldEqualTo("userId")private Long userId;private Pageable pageable;
}
@Data
public class OrderDetail {private Order order;@JoinItemByOrder(keyFromSourceData = "#{order.id}")private List orderItems;public OrderDetail(Order order){this.order = order;}
}
查询三大步骤均基于“声明式注解”通过描述的方式进行实现,然后通过 编码 的方式完成主流程。
仔细观察主流程,会发现这是一套标准的“模板代码”,重复枯燥、没有业务价值,像这样有规律的“重复”代码,就应交由框架实现。
构建声明式 QueryService,只需定义方法,无需编写实现代码,便能完成大多数场景的数据查询。
组件应具有如下特性:
设计目标与 Spring Data 的设计理念高度相似,QueryService 组件在实现上进行了借鉴,在使用上也与 Spring Data 保存一致,以降低使用门槛。
首先,在项目中引入 lego-starter,具体如下:
com.geekhalo.lego lego-starter 0.1.9-query_proxy-SNAPSHOT
然后,依次引入 validation 和 spring data jpa 支持
org.springframework.boot spring-boot-starter-validation
org.springframework.boot spring-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);}
}
其中:
@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 接口,添加相关注解:
public interface OrderQueryRepositoryextends JpaRepository,QueryRepository {Order getById(Long id);List getByUserIdAndStatus(Long id, OrderStatus paid);
}
OrderQueryRepository 继承:
框架对 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)
通常情况下,repository 查询方法只会返回实体对象,框架会自动将其转化为结果对象,并使用 JoinService 完成数据填充。
框架自动查找模型转换方案,具体如下:
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;}
}
QueryService 可以直接使用 QueryRepository 中的方法进行实体查询。
OrderQueryRepository 存在一个 getById 方法,具体如下:
Order getById(Long id);
OrderQueryServiceProxy 也存在一个 getById 方法,具体如下:
OrderDetail getById(@Valid @NotNull(message = "订单号不能为null") Long id);
两者,除返回值不同,方法名和入参均相同,框架会根据 方法名+入参 选择合适的查询方法。
除了直接使用 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 将忽略方法名,基于入参和返回结果的兼容性对方法进行筛选。
当业务非常复杂,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 进行调整。
为 QueryService 自动实现的 Proxy 结构如下:
image
Proxy 实现 自定义的QueryService 接口,并将方法调用分发给不同的实现,核心拦截器包括:
以下是整个框架的初始化流程:
image
通过 @EnableQueryService 注解开启 QueryService 支持后,将向 Spring 容器完成
QueryServiceBeanDefinitionRegistrar 的注册。