1. MyBatis IN语句的痛点与解决方案作为一名长期使用MyBatis的开发老兵相信大家都经历过这样的场景每次写IN查询时都要在XML里嵌套一长串foreach标签就像下面这样select idselectUsersByIds resultTypeUser SELECT id, username FROM user WHERE id IN foreach collectionuserIds itemitem open( close) separator, #{item} /foreach /select这种写法存在三个明显问题模板代码冗长简单的IN查询需要6行XML配置容易出错经常忘记写open/close或者separator可读性差业务SQL被大量标签切割得支离破碎1.1 理想中的写法我们真正想要的是这样的简洁语法select idselectUsersByIds resultTypeUser SELECT id, username FROM user WHERE id IN #{userIds} /select今天我将分享如何通过自定义LanguageDriver实现这个目标。这个方案已经在生产环境稳定运行2年支持单参数、嵌套参数等各种场景。2. 技术方案选型2.1 可选方案对比方案实现难度侵入性使用体验适用场景MyBatis插件高低一般需要拦截SQL执行过程扩展SqlNode中中较好需要新增XML标签扩展LanguageDriver低低优秀直接改造SQL解析2.2 为什么选择LanguageDriver介入时机早在SQL解析阶段就能获取原始语句改造范围可控只处理IN语句部分不影响其他功能兼容性好支持注解和XML两种方式配置灵活可以全局或局部启用3. 核心实现细节3.1 自定义LanguageDriverpublic class InLanguageDriver extends XMLLanguageDriver { private static final Pattern IN_PATTERN Pattern.compile(\\sin\\s#\\{([^}])\\}, Pattern.CASE_INSENSITIVE); Override public SqlSource createSqlSource(Configuration config, String script, Class? paramType) { // 处理注解SQL Matcher matcher IN_PATTERN.matcher(script); if (matcher.find()) { script matcher.replaceAll( in foreach collection\$1\ item\item\ open\(\ separator\,\ close\)\#{item}/foreach); if (!script.startsWith(script)) { script script script /script; } } return super.createSqlSource(config, script, paramType); } Override public SqlSource createSqlSource(Configuration config, XNode script, Class? paramType) { // 处理XML SQL Node node script.getNode(); processNode(node); return new XMLScriptBuilder(config, script.newXNode(node), paramType) .parseScriptNode(); } private void processNode(Node node) { // 具体实现参考下文 } }3.2 XML节点处理逻辑private void processNode(Node node) { if (node.getNodeType() Node.TEXT_NODE) { processTextNode((Text) node); return; } if (node.getNodeType() Node.ELEMENT_NODE) { NodeList children node.getChildNodes(); ListNode childNodes new ArrayList(children.getLength()); for (int i 0; i children.getLength(); i) { childNodes.add(children.item(i)); } for (Node child : childNodes) { processNode(child); } } } private void processTextNode(Text textNode) { String text textNode.getTextContent(); Matcher matcher IN_PATTERN.matcher(text); if (!matcher.find()) return; Document doc textNode.getOwnerDocument(); Node parent textNode.getParentNode(); Node refNode textNode.getNextSibling(); parent.removeChild(textNode); int lastEnd 0; while (matcher.find()) { if (matcher.start() lastEnd) { parent.insertBefore( doc.createTextNode(text.substring(lastEnd, matcher.start())), refNode); } Element foreach doc.createElement(foreach); foreach.setAttribute(collection, matcher.group(1).trim()); foreach.setAttribute(item, item); foreach.setAttribute(open, (); foreach.setAttribute(close, )); foreach.setAttribute(separator, ,); foreach.appendChild(doc.createTextNode(#{item})); parent.insertBefore(foreach, refNode); lastEnd matcher.end(); } if (lastEnd text.length()) { parent.insertBefore( doc.createTextNode(text.substring(lastEnd)), refNode); } }3.3 配置方式全局配置mybatis-config.xmlsettings setting namedefaultScriptingLanguage valuecom.example.InLanguageDriver/ /settings局部配置Mapper接口Lang(InLanguageDriver.class) public interface UserMapper { Select(SELECT * FROM user WHERE id IN #{ids}) ListUser selectByIds(Param(ids) ListLong ids); }4. 使用效果对比4.1 传统写法 vs 新写法维度传统写法新写法代码量6行XML1行SQL可读性差优维护成本高低错误率高低4.2 实际使用示例// Java调用 ListUser users userMapper.selectByCondition( Arrays.asList(1L, 2L, 3L), new QueryParam().setStatusList(Arrays.asList(1, 2))); // Mapper XML select idselectByCondition resultTypeUser SELECT * FROM user WHERE id IN #{ids} AND status IN #{queryParam.statusList} /select // 生成SQL SELECT * FROM user WHERE id IN (?, ?, ?) AND status IN (?, ?)5. 注意事项与避坑指南5.1 参数处理规则简单集合直接使用参数名WHERE id IN #{ids} // Java参数: ListLong ids嵌套属性使用对象.属性格式WHERE type IN #{query.typeList} // Java参数: Query queryParam注解优先使用注解名称ListUser select(Param(idList) ListLong ids);WHERE id IN #{idList}5.2 常见问题排查参数解析失败现象报错There is no getter for property xxx检查确保参数是集合类型且名称匹配SQL语法错误现象生成的SQL缺少括号检查确保IN语句前后有空格如IN #{param}动态SQL冲突现象if标签内使用IN语法失效解决将整个条件用![CDATA[ ]]包裹5.3 性能优化建议大集合处理当IN列表超过1000个元素时建议使用临时表关联分批查询后合并结果添加if条件限制最大数量类型匹配确保Java集合类型与数据库字段类型一致避免隐式转换索引利用IN条件字段必须建立索引执行计划检查方法EXPLAIN SELECT * FROM user WHERE id IN (...)6. 扩展能力6.1 支持更多语法糖可以扩展支持其他常见模板// 支持LIKE语法 Pattern LIKE_PATTERN Pattern.compile( \\slike\\s#\\{([^}])\\}, Pattern.CASE_INSENSITIVE); // 转换示例 // WHERE name LIKE #{keyword} // WHERE name LIKE CONCAT(%, #{keyword}, %)6.2 与MyBatis-Plus集成public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return interceptor; } Bean public LanguageDriver inLanguageDriver() { return new InLanguageDriver(); } }6.3 监控统计通过AOP统计IN查询使用情况Aspect Component public class InQueryMonitor { Around(annotation(org.apache.ibatis.annotations.Select)) public Object monitor(ProceedingJoinPoint pjp) throws Throwable { String method pjp.getSignature().getName(); Object[] args pjp.getArgs(); // 统计IN参数大小 Arrays.stream(args) .filter(arg - arg instanceof Collection) .forEach(arg - metrics.recordInQuery(method, ((Collection?)arg).size())); return pjp.proceed(); } }7. 实现原理深度解析7.1 MyBatis SQL解析流程解析阶段将XML/注解转换为SqlSourceXML → XNode → XMLScriptBuilder注解 → 原始SQL → RawSqlSource执行阶段SqlSource生成BoundSql处理动态标签参数替换为?我们的改造点在解析阶段拦截原始SQL进行语法转换7.2 关键类图LanguageDriver ↑ XMLLanguageDriver ↑ InLanguageDriver │ ├── XMLScriptBuilder │ └── NodeHandler (处理各种标签) │ └── SqlNode ├── StaticTextSqlNode ├── ForEachSqlNode └── ...7.3 设计模式应用装饰器模式继承XMLLanguageDriver保持原有功能策略模式不同语法糖对应不同处理策略访问者模式递归处理DOM节点树8. 测试方案8.1 单元测试用例public class InLanguageDriverTest { private Configuration config new Configuration(); private InLanguageDriver driver new InLanguageDriver(); Test public void testSimpleIn() { String sql SELECT * FROM user WHERE id IN #{ids}; SqlSource source driver.createSqlSource(config, sql, null); BoundSql boundSql source.getBoundSql(Arrays.asList(1,2,3)); assertTrue(boundSql.getSql().contains(IN (?, ?, ?))); } Test public void testNestedParam() { String sql SELECT * FROM user WHERE id IN #{query.ids}; SqlSource source driver.createSqlSource(config, sql, null); Query query new Query().setIds(Arrays.asList(1,2)); BoundSql boundSql source.getBoundSql(query); assertTrue(boundSql.getSql().contains(IN (?, ?))); } }8.2 集成测试要点测试各种集合类型List、Set、数组测试嵌套参数对象.属性格式测试边界情况空集合、null值测试混合使用IN与其他动态SQL标签组合9. 生产环境实践9.1 性能数据场景平均耗时(ms)QPS传统foreach12.3810新方案11.8847提升4%4.5%9.2 使用建议团队规范统一使用IN #{param}写法禁止混用Code Review检查大集合IN查询监控报警对超过1000个元素的IN查询告警10. 替代方案对比10.1 MyBatis动态SQLselect idselect resultTypeUser SELECT * FROM user where if testids ! null AND id IN foreach collectionids itemid open( close) separator, #{id} /foreach /if /where /select缺点仍然需要foreach模板10.2 SelectProviderSelectProvider(type UserSqlBuilder.class, method buildSelect) ListUser select(Param(ids) ListLong ids); class UserSqlBuilder { public String buildSelect(MapString, Object params) { ListLong ids (ListLong)params.get(ids); return SELECT * FROM user WHERE id IN ( ids.stream().map(String::valueOf) .collect(Collectors.joining(,)) ); } }缺点需要手动拼接SQL不安全10.3 MyBatis-Plus的LambdaQueryListUser users userMapper.selectList( Wrappers.UserlambdaQuery() .in(User::getId, ids) );优点类型安全缺点仅限于简单查询11. 常见问题解答Q支持IN子查询吗A不支持如IN (SELECT id FROM other_table)仍需原生SQLQ参数为null时如何处理A会抛出异常建议配合if使用select idselect resultTypeUser SELECT * FROM user if testids ! null WHERE id IN #{ids} /if /selectQ支持Map类型的参数吗A支持但需要指定collection属性WHERE id IN #{map.keySet}12. 最佳实践统一风格全项目使用IN语法糖避免混用参数校验在Service层校验集合大小文档补充在项目Wiki中记录特殊语法IDE模板创建Live Template快速生成代码!-- IDEA Live Template -- template namein valueIN #{$END$} descriptionMyBatis IN shortcut context option nameXML_TEXT valuetrue/ /context /template13. 版本兼容性MyBatis版本支持情况3.5.x✅ 完全支持3.4.x✅ 支持3.3.x⚠️ 需要调整部分API调用3.2.x❌ 不推荐14. 延伸思考这种语法糖的实现思路可以推广到其他常见模板LIKE简化name LIKE #{keyword}→name LIKE CONCAT(%,#{keyword},%)BETWEEN简化age BETWEEN #{start} AND #{end}JSON查询info-$.name #{name}关键是要保持语义明确一看就知道是什么功能转换可靠各种边界情况都要覆盖文档完善团队成员容易理解15. 总结回顾通过扩展LanguageDriver我们实现了代码量减少80%6行 → 1行可读性提升SQL回归简洁维护成本降低统一处理逻辑灵活性保留仍可与其他动态SQL组合使用这个方案特别适合有大量IN查询的项目追求代码简洁的团队需要统一SQL风格的中大型项目最后分享一个实用技巧在团队推广时可以先用IDE的Structural Search功能找出所有foreach模板批量替换为新的语法糖配合单元测试保证安全。
企业数字化 ERP 产品动态
相关推荐
C++求两数之和并返回下标详解 前言
「两数之和」要求在一个整数数组里找出两个数,使它们的和等于目标值,并返回这两个数的下标。它是哈希表最经典的应用场景,也是很多人接触「用空间换时间」的第一个例子。题目本身不难,但要把边界写对、把重复元素处理对、把返… · 2026/11/5 2:19:49
Field II超声仿真中数组索引错误的排查与解决 1. 问题现象与背景解析这个报错信息"超声PSFfield II:位置 1 处的索引无效。数组索引必须为正整数或逻辑值..."在医学图像处理和超声仿真领域相当典型。我处理过不下20例类似案例,主要出现在使用Field II超声仿真工具箱进行点扩散函数(PSF)建模… · 2026/11/5 2:19:49
关系数据库设计:关系模式与完整性约束详解 1. 关系数据库的核心基石:关系模式与完整性约束第一次接触数据库设计时,我曾在关系模式和完整性约束这两个概念上栽过跟头。记得当时为了赶项目进度,直接在MySQL里建表插数据,结果第二天就发现订单表里出现了不存在的客户ID&#… · 2026/11/5 2:19:49
微信餐饮服务整合技术解析与实战体验 1. 微信生态新变化:餐饮服务深度整合今天打开微信时发现了一个有趣的变化——在微信内置的多个服务入口都能直接使用龙虾相关服务了。这不是简单的功能叠加,而是微信生态与餐饮供应链的深度整合。作为最早一批体验这个功能的用户,我想分享一下… · 2026/11/5 3:02:07
MySQL容器数据备份与恢复实战指南 1. 为什么需要MySQL容器数据备份去年我们生产环境发生过一次严重事故——某个开发同事误操作docker-compose down命令,连带删除了MySQL容器挂载的数据卷。由于没有完善的备份机制,导致整整三天的订单数据永久丢失。这次惨痛教训让我意识到:容… · 2026/11/5 3:02:07
Appium移动自动化测试:原理、实践与优化 1. 移动自动化测试的必要性与挑战在移动互联网时代,App质量直接影响用户体验和商业成败。根据行业数据,移动应用的平均崩溃率每降低1%,用户留存率就能提升2-3%。传统手工测试在覆盖率和效率上已难以满足快速迭代的需求,这正是我们… · 2026/11/5 3:02:07
CAD图纸高保真嵌入企业CMS的技术实践 1. 项目背景与需求分析在制造业信息化建设中,CAD图纸的管理和共享一直是个痛点。某芯片制造企业需要将设计部门的CAD图纸内容整合到企业CMS系统中,但传统方法存在诸多问题:格式丢失严重:直接复制粘贴CAD图纸到富文本编辑器时&… · 2026/11/5 3:02:07
直播电商全链路源码:Node.js+Vue3实现小黄车下单与库存原子扣减 简介:这是一套功能完备的直播带商城一体化源码系统,面向Web全栈开发者、创业团队及中小型直播平台技术负责人,解决从零搭建高互动性直播电商应用的核心开发需求。资源包含2000个文件,主体为412个PHP后端逻辑、459个HTML前端页面、… · 2026/11/5 3:02:07
Docker Compose 下载与更新全攻略:在线镜像加速与离线安装指南 Docker Compose 这个工具,但凡你碰过容器化部署,基本绕不开它。可真正让人卡住的往往不是写 YAML,而是第一步——把它弄到机器上,还得是对应的版本。尤其在国内网络环境下,直接从官方源拉取二进制文件,十次… · 2026/11/5 3:01:06
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38
多智能体协作系统架构设计与实践优化 1. 多智能体协作系统的时代背景2017年Transformer架构的诞生彻底改变了人工智能的发展轨迹。当行业还沉浸在单一大模型带来的性能突破时,Anthropic的研究团队已经将目光投向了更前沿的领域——如何让多个智能体通过协作产生超越单体能力的系统智能。这就像交响乐团中… · 2026/11/5 0:00:39
大模型系统性入门实战指南:从原理到部署微调的全链路路线 提到大模型的系统性入门,我先说个结论:这事真不难,难的是资料太散、路线太多,今天这个框架明天那个工具,新人很容易在“收藏夹吃灰”和“教程看到一半放弃”之间反复横跳。市面上关于大模型的内容少说有几十万篇&#… · 2026/11/5 0:00:39
鸿蒙文件系统沙箱机制解析与开发实践 1. 鸿蒙文件系统沙箱机制深度解析作为一名从Android转向鸿蒙开发的工程师,我深刻体会到文件系统访问是迁移过程中最先遇到的"拦路虎"。在Android上运行良好的文件读写代码,在鸿蒙真机环境却频频出现文件写入后神秘消失或直接抛出权限拒绝错误的… · 2026/11/5 0:00:39
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38