从 selectBatchIds 切入:MyBatis-Plus 批量查询工具与 Java 集合、泛型的联动学习文档
一、引子
在后端开发中,「根据多个 ID 批量查询数据」是极为高频的业务场景 —— 例如前端传递一个 ID 列表,后端需一次性获取这些 ID 对应的完整记录。若采用循环调用单条查询接口的方案,会频繁建立数据库连接,大幅增加 IO 开销,严重影响系统性能。
MyBatis-Plus 提供的 selectBatchIds 方法恰好解决了这一痛点:它通过一次 SQL 执行完成批量查询,有效减少数据库交互次数,显著提升效率。
在项目实践中频繁使用该方法时,我曾查阅其源码实现,发现其中集合与泛型的应用设计十分典型,借此机会梳理相关知识点,形成以下学习记录。该方法的核心定义如下:
List<T> selectBatchIds(@Param("coll") Collection<? extends Serializable> idList);
从方法签名可直观发现两处关键技术点,也是本文的核心学习内容:
• 参数类型 Collection<? extends Serializable>:深度融合 Java 集合框架的设计思想与类型约束逻辑;
• 返回值 List
二、参数类型 Collection<? extends Serializable> 涉及「集合框架」的使用与约束;
selectBatchIds 的参数声明看似简洁,实则蕴含 Java 集合框架与泛型的底层设计逻辑。我们从「集合接口选型」与「泛型通配符约束」两个维度拆解分析。
1.为什么选择Collection 作为接口类型?
Collection 是 Java 集合框架的顶层接口之一(位于 java.util 包),是 List、Set、Queue 等所有「单值集合」的父接口,定义了集合的通用操作规范(如 add() 新增元素、size() 获取长度、isEmpty() 判断空值等)。
selectBatchIds 选择 Collection 而非具体实现类(如 ArrayList、HashSet),本质是「面向接口编程」思想的典型实践,核心优势体现在两方面:
(1)更高的灵活性:适配多场景集合输入
• 若需保留 ID 传入顺序(如按前端传递顺序返回结果),可传入 List:
// 传入 List,保留 ID 顺序
List<Long> idList = Arrays.asList(1L, 2L, 3L); userMapper.selectBatchIds(idList);
• 若需先对 ID 去重(如避免重复查询同一记录),可传入 Set:
// 传入 Set,自动去重
Set<Integer> idSet = new HashSet<>(Arrays.asList(1, 2, 3, 3));
userMapper.selectBatchIds(idSet);
(2)更强的解耦性:依赖抽象而非具体实现
Collection 接口仅定义「行为规范」(如遍历、判断空值),不绑定具体实现逻辑。这使得 selectBatchIds 的底层处理(如遍历 ID 生成 IN 条件的 SQL 占位符)可基于抽象接口编写,无需关注传入集合的具体类型(是 List 还是 Set),避免与特定实现耦合,降低后续维护成本。
2.泛型通配符 <? extends Serializable> 是什么意思?
该通配符是 Java 泛型中的「上限通配符」(Upper Bounded Wildcard),核心含义是:集合中的元素必须是 Serializable 接口的实现类(Serializable 本身是接口,实际约束的是其所有实现类)。
(1) 通配符拆解
• ?:表示「未知类型」,是泛型通配符的基础形式,用于表示不确定的类型占位;
• extends Serializable:为「未知类型」设定上限 —— 限定该类型必须是 Serializable 的子类(或实现类,Java 中接口间继承用 extends 关键字)。
(2) Serializable接口的核心作用
Serializable 是 Java 中的「标记接口」(无任何方法或字段定义),仅作为一种「能力标识」:告诉 JVM “该类的对象支持序列化”。
序列化的核心目的是实现对象的跨场景复用:
• 跨网络传输:如微服务间通过 RPC 传递对象(需转为字节流);
• 缓存存储:如将对象存入 Redis(需序列化为字符串或字节数组);
• 持久化:如将对象写入本地文件(需转为字节流)。
而数据库表的主键(ID)常用类型(Integer、Long、String 等)均已实现 Serializable 接口,恰好满足该约束。
(3)约束的价值:提前规避类型错误
通过 <? extends Serializable> 约束,可在编译期拦截不合法的 ID 类型(如未实现 Serializable 的自定义类),避免运行时抛出 NotSerializableException,减少线上故障风险。
三、返回值 List 则用到了「泛型」来保证类型安全与代码复用。
selectBatchIds 的返回值 List
1.泛型参数T的绑定逻辑
要理解 List
// BaseMapper 是泛型接口,T 代表“实体类类型”
public interface BaseMapper<T> {
// 返回值 List<T>:T 与接口的泛型参数一致,即“当前实体类的列表”
List<T> selectBatchIds(@Param("coll") Collection<? extends Serializable> idList);
// 其他方法也依赖 T,如单条查询返回 T
T selectById(Serializable id);
}
当我们自定义 Mapper 接口时,会通过泛型参数传递,将 T 绑定到具体的实体类:
// UserMapper 继承 BaseMapper 时,将 T 指定为 User
public interface UserMapper extends BaseMapper<User> { // 无需重写 selectBatchIds,继承的方法自动适配为:
List<User> selectBatchIds(Collection<? extends Serializable> idList);
}
这一绑定过程的核心价值在于:selectBatchIds 无需针对每个实体类单独定义,仅通过泛型参数即可自动适配不同业务实体,实现 “一次定义,多场景复用”。
2.泛型如何实现代码复用?
selectBatchIds 能成为所有 Mapper 的通用方法,核心依赖泛型 T 的「类型参数化」能力 —— 将 “实体类类型” 从方法逻辑中剥离,用 T 作为占位符,使方法适配任意实体。
(1) 无泛型的弊端:代码冗余严重
假设没有泛型,我们需要为每个实体类重复编写几乎相同的方法:
// 无泛型:为 User 写一次
public interface UserMapper {
List<User> selectUserBatchIds(Collection<? extends Serializable> idList);
}
// 无泛型:为 Order 再写一次(逻辑完全相同,只是类型不同)
public interface OrderMapper {
List<Order> selectOrderBatchIds(Collection<? extends Serializable> idList);
}
// 无泛型:为 Product 还要写...(代码冗余爆炸)
public interface ProductMapper {
List<Product> selectProductBatchIds(Collection<? extends Serializable> idList);
}
(2) 泛型的优势:一次定义,全局复用
通过 BaseMapper
// 泛型方案:一份 BaseMapper 适配所有实体
public interface UserMapper extends BaseMapper<User> {} // 自动有 List<User> 方法
public interface OrderMapper extends BaseMapper<Order> {} // 自动有 List<Order> 方法
public interface ProductMapper extends BaseMapper<Product> {} // 自动有 List<Product> 方法
这种设计不仅消除了代码冗余,还保证了各 Mapper 方法逻辑的一致性,大幅降低维护成本。
3. 泛型如何保障类型安全?
泛型的另一核心价值是「类型安全」—— 通过编译期类型校验,避免运行时类型转换异常,同时减少手动类型转换的繁琐操作。
(1) 编译期拦截非法类型
若未使用泛型,返回值需定义为 List
浙公网安备 33010602011771号