基于SpringEL表达式 + 声明式注解的 数据库内存Join组件

背景

 

java

复制代码

@Data public class OrderDetailVO { private OrderVO order; private UserVO user; private AddressVO address; private ProductVO product; }

现在需要根据 userId 查询某个用户下面所有的订单 (orders)。通过 order 对象的 userId 查询 User (转化为 UserVo),通过 order 对象的 productId 获得 Product (转化为 ProductVO), 通过 addressId 获得 Address (转化为 AddressVo)。最后封装为上述的 OrderDetailVO 返回。

方案

1. 数据库层面Join

在数据库层面进行Join操作,给出如下伪代码

 

mysql

复制代码

SELECT u.*, a.*, p.*, o.* from user u, address a, product p, order o WHERE u.user_id = o.user_id AND a.address_id = o.address_id AND p.product_id = o.product_id AND o.user_id = #{user.userId}

坏处:

  1. 这样对数据库的CPU占用特别高,如果数据量很大那么不太现实。
  2. 对于分片的数据库来说,不支持跨库Join操作。

最终方案不太合适,我们应该将Join操作放到Java内存里面进行。

2. 内存里面进行foreach查询,然后封装

下面给出伪代码

 

java

复制代码

private String testForEachVersion() { // 1, 查询 userId = 1 的所有订单 List<Order> orders = orderRepository.getOrderByUserId(1L); // 2, 遍历每条订单 生成OrderDetailVO List<OrderDetailVO> orderDetailVOS = orders.stream().map(order -> { // 获得userId Long userId = order.getUserId(); // 获得user User user = userRepository.findUserById(userId); // 获得addressId Long addressId = order.getAddressId(); // 获得address Address address = addressRepository.findAddressById(addressId); // 获得productId Long productId = order.getProductId(); // 获得product Product product = productRepository.findProductById(productId); // 封装成OrderVO OrderDetailVO orderDetailVO = new OrderDetailVO(OrderVO.apply(order)); orderDetailVO.setUser(UserVO.apply(user)); orderDetailVO.setAddress(AddressVO.apply(address)); orderDetailVO.setProduct(ProductVO.apply(product)); return orderDetailVO; }).collect(Collectors.toList()); return JSON.toJSONString(orderDetailVOS); }

坏处:

  1. 每次都需要 for 循环去查询 user, address, product. 数据库的压力十分大

3. 内存里面进行批量查询,然后封装

改进的思路是: 收集 userIds, addressIds, productIds。然后根据 userIds, addressIds, productIds 进行批量查询 users, addresss, products 减小数据库请求次数。

伪代码如下:

 

java

复制代码

private String testBatchQueryVersion() { // 1, 查询 userId = 1 的所有订单 List<Order> orders = orderRepository.getOrderByUserId(1L); // 2, 收集 userIds List<Long> userIds = orders.stream() .map(Order::getUserId) .distinct() .collect(Collectors.toList()); // 3, 批量查询User List<User> users = userRepository.findUserById(userIds); // 根据 userId 进行分组 转化成 Map<UserId, List<UserVO>> Map<Long, List<UserVO>> userId2UserVO = users.stream() .map(UserVO::apply) .collect(Collectors.groupingBy(UserVO::getId)); // 4, 收集 productId List<Long> productIds = orders.stream() .map(Order::getProductId) .distinct() .collect(Collectors.toList()); // 5, 批量查询Product List<Product> products = productRepository.findProductById(productIds); // 根据 productId 进行分组 转化成 Map<productId, List<ProductVO>> Map<Long, List<ProductVO>> productId2ProductVO = products.stream() .map(ProductVO::apply) .collect(Collectors.groupingBy(ProductVO::getId)); // 6, 收集 addressId List<Long> addressIds = orders.stream() .map(Order::getAddressId) .distinct() .collect(Collectors.toList()); // 7, 批量查询Addresses List<Address> addresses = addressRepository.findAddressById(addressIds); // 根据 addressId 进行分组 转化成 Map<addressId, List<AddressVO>> Map<Long, List<AddressVO>> addressid2AddressVO = addresses.stream() .map(AddressVO::apply) .collect(Collectors.groupingBy(AddressVO::getId)); // 8, 遍历每条订单 生成OrderDetailVO List<OrderDetailVO> orderDetailVOS = orders.stream().map(order -> { OrderDetailVO orderDetailVO = new OrderDetailVO(OrderVO.apply(order)); Long userId = order.getUserId(); Long productId = order.getProductId(); Long addressId = order.getAddressId(); orderDetailVO.setUser(userId2UserVO.get(userId).get(0)); orderDetailVO.setProduct(productId2ProductVO.get(productId).get(0)); orderDetailVO.setAddress(addressid2AddressVO.get(addressId).get(0)); return orderDetailVO; }).collect(Collectors.toList()); return JSON.toJSONString(orderDetailVOS); }

此时这种方案就是最好的。但是它存在的问题就是 编码太复杂,而且有很多逻辑都是相似的,那么我们是否有办法封装一套组件来处理此种问题?

引入 Kdk-JoinInMemory-SpringBoot-Starter

1. 引入依赖

 

xml

复制代码

<dependency> <groupId>com.hdu</groupId> <artifactId>kdk-joinInMemory-spring-boot-starter</artifactId> <version>1.0-SNAPSHOT</version> </dependency>

2. 为 OrderDetailVO 添加上一些注解

 

java

复制代码

@Data @JoinInMemoryConfig(executorType = JoinInMemeoryExecutorType.PARALLEL) public class OrderDetailVO { private final OrderVO order; @JoinInMemory(keyFromSourceData = "#{order.userId}", loader = "#{@userRepository.findUserById(#root)}", keyFromJoinData = "#{id}", joinDataConverter = "#{T(com.hdu.joinInMemory.entity.VO.UserVO).apply(#root)}" ) private UserVO user; @JoinInMemory(keyFromSourceData = "#{order.addressId}", loader = "#{@addressRepository.findAddressById(#root)}", keyFromJoinData = "#{id}", joinDataConverter = "#{T(com.hdu.joinInMemory.entity.VO.AddressVO).apply(#root)}" ) private AddressVO address; @JoinInMemory(keyFromSourceData = "#{order.productId}", loader = "#{@productRepository.findProductById(#root)}", keyFromJoinData = "#{id}", joinDataConverter = "#{T(com.hdu.joinInMemory.entity.VO.ProductVO).apply(#root)}" ) private ProductVO product; }

下面解释一下这些注解的含义:

便于理解这些注解的含义,你需要 去认真看看 上述方案3 - 内存里面进行批量查询。这些注解的作用便是封装上面的代码逻辑然后封装

  1. @JoinInMemoryConfig(executorType = JoinInMemeoryExecutorType.PARALLEL)

表示希望多线程去查询 Users, Addresses, Products。然后进行封装。 如果不添加 @JoinInMemoryConfig 注解,或者 指定 executorType = JoinInMemeoryExecutorType.SERIAL。那么会串行查询 Users, Addresses, Products。然后进行封装。

  1. @JoinInMemory

它定义了一种行为。定义了怎么提取 userIds, 怎么查询 users, 怎么 将 users 根据 id 分组, 怎么将 user -> userVO

举个例子:

 

java

复制代码

@JoinInMemory(keyFromSourceData = "#{order.userId}", loader = "#{@userRepository.findUserById(#root)}", keyFromJoinData = "#{id}", joinDataConverter = "#{T(com.hdu.joinInMemory.entity.VO.UserVO).apply(#root)}" )

 

java

复制代码

// 1, 收集 userIds List<Long> userIds = orders.stream() .map(Order::getUserId) .distinct() .collect(Collectors.toList()); // 2, 批量查询User List<User> users = userRepository.findUserById(userIds); // 3, 根据 userId 进行分组 转化成 Map<UserId, List<UserVO>> Map<Long, List<UserVO>> userId2UserVO = users.stream() .map(UserVO::apply) .collect(Collectors.groupingBy(UserVO::getId)); .......... //


keyFromSourceData = "#{order.userId}" 定义了 怎么收集 userId 即通过 OrderDetailVO.order.userId 即可收集到所有 userIds 即替代代码

图片.png


loader = "#{@productRepository.findProductById(#root)}" 定义了 怎么通过 userIds 查询 users. 即替代代码

图片.png


 

java

复制代码

keyFromJoinData = "#{id}", joinDataConverter = "#{T(com.hdu.joinInMemory.entity.VO.UserVO).apply(#root)}"

定义了取 user.id 进行分组, 使用 UserVO.apply方法将 user -> userVO。即替代代码

图片.png

3. 最终使用

 

java

复制代码

private String testJoinInMemory() { // 1, 查询 userId = 1 的所有订单 List<Order> orders = orderRepository.getOrderByUserId(1L); // 2, order -> orderDetailVO List<OrderDetailVO> orderDetailVOS = orders.stream() .map(order -> new OrderDetailVO(OrderVO.apply(order))) .collect(Collectors.toList()); // 3, 内存Join joinService.joinInMemory(orderDetailVOS); return JSON.toJSONString(orderDetailVOS); }

我们可以看到,最终的使用十分简单!

4. 提取注解 方便复用

 

java

复制代码

@Data @JoinInMemoryConfig(executorType = JoinInMemeoryExecutorType.PARALLEL) public class OrderDetailVO { private final OrderVO order; @JoinInMemory(keyFromSourceData = "#{order.userId}", loader = "#{@userRepository.findUserById(#root)}", keyFromJoinData = "#{id}", joinDataConverter = "#{T(com.hdu.joinInMemory.entity.VO.UserVO).apply(#root)}" ) private UserVO user; @JoinInMemory(keyFromSourceData = "#{order.addressId}", loader = "#{@addressRepository.findAddressById(#root)}", keyFromJoinData = "#{id}", joinDataConverter = "#{T(com.hdu.joinInMemory.entity.VO.AddressVO).apply(#root)}" ) private AddressVO address; @JoinInMemory(keyFromSourceData = "#{order.productId}", loader = "#{@productRepository.findProductById(#root)}", keyFromJoinData = "#{id}", joinDataConverter = "#{T(com.hdu.joinInMemory.entity.VO.ProductVO).apply(#root)}" ) private ProductVO product; }

我们可以注意到,假如以后 UserVO 还可能封装到其他的 xxxVOs 中,那么注解书写起来就会比较麻烦。观察变与不变,我们会发现 UserVO 封装到 其他的 xxxVOs 里面的话,变化的部分只有 keyFromSourceData , 其他属性都是不需要改变的,那么我们可以将注解封装。如下:

 

java

复制代码

@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @JoinInMemory(keyFromSourceData = "", keyFromJoinData = "#{id}", loader = "#{@userRepository.findUserById(#root)}", joinDataConverter = "#{T(com.hdu.joinInMemory.entity.VO.UserVO).apply(#root)}" ) public @interface JoinUserVOOnId { // @AliasFor 表示将会把 属性 keyFromSourceData 桥接到 @JoinInMemory 的 keyFromSourceData 属性。 //其实 @SpingBootApplication 也有相同的处理套路。 @AliasFor( annotation = JoinInMemory.class ) String keyFromSourceData(); } ... productVO, addressVO 同理......

最终我们可以得到如下的注解搭配

 

java

复制代码

@Data @JoinInMemoryConfig(executorType = JoinInMemeoryExecutorType.SERIAL) public class OrderDetailVO { private final OrderVO order; @JoinUserVOOnId(keyFromSourceData = "#{order.userId}") private UserVO user; @JoinAddressVOOnId(keyFromSourceData = "#{order.addressId}") private AddressVO address; @JoinProductVOOnId(keyFromSourceData = "#{order.productId}") private ProductVO product; }

最终我们可以看到是如此的清爽,丝滑。并且 如果 UserVo 还被其他 XXXVo 引入,那么注解也可以得到复用。只需要修改 keyFromSourceData 即可

源码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值