起因
在日常服务巡检中,系统出现了大量的警告日志。
经过排查,发现原因在于JPA执行数据库查询时,WHERE子句中的IN条件带有过多参数(超过300个),超出了系统设定的预警限制(128个),从而触发了警告。
为了解决这一问题,需要对现有代码进行优化:
interface JpaTestRepository extends JpaRepository<TestPo, Long> {
@Query(value = "select * from TestPo test where TestPo.deleted is false and TestPo.id in ?1")
List<TestPo> findTestInId(List<Long> idList);
}
@Service
public class JpaTestDao {
@Autowired
private JpaTestRepository jpaTestRepository;
// 当 idList 数量过多时就会引发 warn 告警
public List<TestPo> batchTest(List<Long> idList){
return jpaTestRepository.findTestInId(idList)
}
}
如果在每个方法内部实现对idList的拆分和方法的遍历调用,代码将显得冗长且不优雅,并可能产生大量重复代码。
注意到使用IN查询的方法结构大致一致,入参为List<Long> id,返回值为List<Po>。
基于这一特点,计划利用函数式接口和泛型来编写一个通用方法,以便在不同类中调用具有类似签名(尽管方法名不同)的方法,从而优化代码结构,提高代码的可复用性和可维护性。
实现
首先定义一个函数式接口 Executable ,这是 Java 8 引入的一个特性,允许将方法作为参数进行传递。
这个接口定义了一个泛型方法 execute ,接收一个类对象和一个目标方法,并返回目标方法的执行结果。通过使用 @FunctionalInterface 注解,确保这个接口中只有一个抽象方法,从而可以使用 lambda 表达式进行实例化。
/**
* 用于调用某个类中的指个方法
*
* @param <T> 类对象
* @param <V> 目标执行方法
* @param <E> 方法返回值
*/
@FunctionalInterface
public interface Executable<T, V, E> {
/**
* 执行方法
*
* @param t 方法所在的类对象
* @param v 要执行的方法
* @return 目标方法的返回值
*/
E execute(T t, V v);
}
BatchHandleUtil 类是一个批量处理的工具类,提供了一个batchQuery方法,用于对数据进行分批处理。这个方法的核心思想是将一个大的ID列表拆分为多个子列表,然后对每个子列表调用指定的方法进行处理。
package com.alaeat.aio.store.utils.func;
import com.alibaba.fastjson2.JSONObject;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
/**
* 批量处理工具类
*/
@Slf4j
public class BatchHandleUtil {
private static final int batchMax = 127;
/**
* 批量查询工具方法。
*
* @param <T> 实体类型
* @param <E> 查询结果类型
* @param t 实体类对象
* @param executable 可执行的查询操作,接收实体和ID列表并返回结果列表
* @param idList 需要查询的ID列表
* @return 所有ID对应的查询结果的列表
*/
public static <T, E> List<E> batchQuery(T t, Executable<T, List<Long>, List<E>> executable, List<Long> idList) {
StringBuilder str =new StringBuilder();
List<E> allResults = new ArrayList<>();
// 将ID列表拆分为多个子列表,每个子列表的大小不超过batchMax
List<List<Long>> partitionedIdLists = Lists.partition(idList, batchMax);
for (List<Long> idSubList : partitionedIdLists) {
// 执行查询并获取结果
List<E> batchResults = executable.execute(t, idSubList);
allResults.addAll(batchResults);
}
return allResults;
}
}
在实际应用中,通过静态工具类 BatchHandleUtil 对方法进行分批调用。
以 JpaTestDao 类为例,通过 batchQuery 方法传递 jpaTestRepository 对象、要调用的方法 findTestInId 以及参数 idList ,即可轻松实现对数据库查询的分批处理。
interface JpaTestRepository extends JpaRepository<TestPo, Long> {
@Query(value = "select * from TestPo test where TestPo.deleted is false and TestPo.id in ?1")
TestPo findTestInId(List<Long> idList);
}
@Service
public class JpaTestDao {
@Autowired
private JpaTestRepository jpaTestRepository;
public List<TestPo> batchTest(List<Long> idList){
// 在此处传入jpa对象、要调用的方法、传参即可完成分批次调用操作。对源代码修改幅度小,且能快速对同类型方法进行适配
return BatchHandleUtil.batchQuery(jpaTestRepository, JpaTestRepository::findTestInId, idList);
}
}
通过函数式接口实现批量处理带来了多种优势:
- 代码复用性高:函数式接口和工具类的通用设计使得代码能够在项目中被多次复用,无需为每一个批量处理的任务单独编写逻辑,从而提高了开发效率。
- 维护简便:由于批量处理的逻辑被集中在工具类中,代码结构清晰,维护和扩展时只需关注工具类本身,减少了对其他代码的影响,维护工作变得更加轻松。
- 高度适配:这种设计模式具备很强的通用性,能够适用于多种类型的方法调用。只要方法符合
Executable接口的定义,就可以灵活地通过batchQuery实现分批处理,增强了代码的灵活性和适应性。
结束语
这篇文章的目的是抛砖引玉,激发大家的灵感。在面对SQL In语句数据量过大的问题时,除了批量处理,还有很多其他的解决方案,比如使用缓存中间件、临时表等。本文主要想展示的是函数式接口的使用,以及它所带来的高扩展性。
函数式接口自从Java 8引入后,提供了一种全新的编程方式。它允许将方法作为参数传递,使得代码更加灵活和可复用。这种特性不仅让代码更易读、更易维护,也为处理复杂业务逻辑时提供了更多的选择。
如果对函数式接口有独特的见解或创新的想法,欢迎在留言区分享。通过彼此的交流与互动,可以一起探索更多的解决方案和应用场景,使代码变得更加高效和优雅。期待看到讨论和建议,共同促进进步!