MyBatis传List调Oracle存储过程:TypeHandler与ARRAY批量插入实战
简介这份PDF资料面向使用Mybatis与Oracle进行持久层开发的中高级Java工程师聚焦于如何将List类型参数传递给Oracle存储过程以实现批量插入数据这一常见难题。资源共1个PDF文件压缩包约61KB内容围绕TypeHandler机制展开讲解如何把Java中的List封装为Oracle的ARRAY类型并传入存储过程。资料完整覆盖建表、创建Oracle对象类型与数组类型、编写存储过程、配置Mybatis的parameterMap与自定义ListHandler、Server端调用等关键环节并给出可参考的代码示例与排错思路。相比Mybatis常规批量插入该方案可摆脱不能实时返回主键、foreach循环集合长度受限等约束同时便于在存储过程中灵活控制事务回滚。目前已有1961人学习适合需要处理Oracle批量写入与事务控制的开发者参考借鉴。1. 从一次批量插入翻车说起MyBatis 传 List 调 Oracle 存储过程到底解决什么去年做某权限管理系统时我遇到一个很典型的场景给单位批量分配菜单权限前端传过来一个逗号分隔的 menuIds 字符串后端要拆成 List 批量写进关联表。一开始用 MyBatis 的foreach标签拼批量 insert数据量小的时候一切正常等到某个单位一次性勾了上千个菜单问题就来了——foreach拼出来的 SQL 长度直接顶到 Oracle 的极限报 ORA-01795in 列表最多 1000 项和 SQL 文本过长的错误。更头疼的是这套批量插入挂在 Spring 事务里一旦中间某条失败整个事务回滚但业务上又希望能拿到「哪几条成功、哪几条失败」的细粒度结果MyBatis 原生批处理根本给不了。后来我把思路换了一下不在 Java 层拼 SQL而是把整个 List 通过 MyBatis 的 TypeHandler 封装成 Oracle 的 ARRAY 类型一次性传给存储过程让 PL/SQL 在数据库端循环插入、自己控制 commit 和 rollback最后通过 OUT 参数把执行结果返回给 Java。这套方案跑通之后批量插入的限制基本消失了事务控制也灵活了很多。这篇就把我当时踩过的坑和完整配置拆开讲清楚适合正在被 MyBatis 批量插入折磨、又想用存储过程接管事务的同行参考。2. 原理拆解TypeHandler 如何把 Java List 变成 Oracle ARRAY2.1 为什么不能直接传 List非要绕 TypeHandler 这一圈JDBC 规范里PreparedStatement 的setObject只认基本类型、字符串、日期和数组它不认识java.util.List。而 Oracle 的存储过程参数如果是自定义的 table of object 类型JDBC 层必须用java.sql.Array接口来传递这个接口的实现类在 Oracle 驱动里就是oracle.sql.ARRAY。所以中间必须有一个「翻译官」把 List 里的每个 Java 对象拆成字段数组再包装成 STRUCT最后组成 ARRAY。MyBatis 提供的BaseTypeHandler就是干这个的——它在setNonNullParameter方法里给你一个PreparedStatement和参数下标你负责把 List 转成 ARRAY 塞进去。这里有个容易被忽略的点MyBatis 的 TypeHandler 默认只处理单值参数当你的参数是 List 且要作为存储过程的 IN 参数时必须显式在parameterMap里指定typeHandler否则 MyBatis 会尝试用默认的 UnknownTypeHandler直接抛类型转换异常。这也是很多人卡在「参数传不进去」这一步的根本原因。2.2 Oracle 端的对象类型和数组类型怎么建Oracle 不允许直接把 Java 的 List 映射成表类型必须先定义两个数据库对象一个 OBJECT 类型描述单行结构一个 TABLE 类型描述整个集合。以单位菜单权限为例建表语句和类型定义如下-- 业务表单位与菜单的关联 create table ZD_UNIT_MENU ( unit_id VARCHAR2(32), menu_id VARCHAR2(32) ); -- 单行对象类型字段名要和 Java 对象属性对应 CREATE OR REPLACE TYPE unit_menu_obj AS OBJECT( unitId VARCHAR2(32), menuId VARCHAR2(32) ); -- 集合类型基于上面的对象类型 CREATE OR REPLACE TYPE unit_menu_table AS table OF unit_menu_obj;注意unit_menu_obj里的字段名unitId、menuId是大小写敏感的后面 Java 端构造 STRUCT 时传参顺序必须和这里一致否则会出现字段错位——插进去的 unit_id 变成了 menu_id这种 bug 排查起来非常费劲。我一般建议字段名全用大写或者全用驼峰团队内统一别混着来。2.3 存储过程里循环插入与事务控制存储过程接收unit_menu_table类型的 IN 参数用FOR i IN 1 .. list0.count LOOP遍历逐条执行 insert。这里用execute immediate动态拼 SQL 是一种写法但更稳妥的做法是直接静态 insert避免拼接字符串带来的注入风险和性能损耗。下面是我后来改过的版本create or replace procedure save_unit_power( list0 in unit_menu_table, result0 out int ) as begin FOR i IN 1 .. list0.count LOOP insert into ZD_UNIT_MENU(UNIT_ID, MENU_ID) values(list0(i).unitId, list0(i).menuId); end loop; commit; result0 : 1; -- 成功返回 1 EXCEPTION WHEN OTHERS THEN result0 : -1; -- 失败返回 -1 ROLLBACK; end save_unit_power;result0是 OUT 参数Java 端调用后可以从 Map 里取出来判断执行结果。这里的事务边界完全在存储过程内部Java 层的 Spring 事务不会介入所以如果你有跨存储过程的业务一致性要求得在 Java 层再包一层补偿逻辑别指望数据库端帮你兜底。3. 完整落地从 parameterMap 配置到 ListHandler 实现3.1 mapper.xml 里的 parameterMap 与 CALLABLE 语句MyBatis 调用存储过程必须用statementTypeCALLABLE参数映射推荐用parameterMap虽然官方更推parameterType#{}但传 ARRAY 类型时 parameterMap 更直观。配置如下parameterMap typejava.util.Map id_map parameter propertylist0 jdbcTypeARRAY javaTypejava.util.List modeIN typeHandlercom.zd.util.ListHandler/ parameter propertyresult0 jdbcTypeDECIMAL javaTypejava.lang.Integer modeOUT / /parameterMap select idaddUnitPower statementTypeCALLABLE parameterMap_map ![CDATA[ CALL save_unit_power(?,?) ]] /selectjdbcTypeARRAY告诉 MyBatis 这个参数走 JDBC 数组通道typeHandler指向你自己写的转换类。modeOUT的 result0 不需要 typeHandlerMyBatis 会自动用 IntegerTypeHandler 处理。这里有个细节parameterMap的type写java.util.Map因为我们要同时传 IN 的 list 和接收 OUT 的 result用 Map 装最方便。3.2 ListHandler 的核心逻辑与连接管理ListHandler 继承BaseTypeHandler重点是setNonNullParameter方法。原始代码里每次转换都新建一个DriverManager.getConnection这是个明显的性能坑——每次调用都开新连接连接池形同虚设。我改成从传入的 PreparedStatement 里拿 Connectionpackage com.zd.util; import java.sql.CallableStatement; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; import oracle.sql.ARRAY; import oracle.sql.ArrayDescriptor; import oracle.sql.STRUCT; import oracle.sql.StructDescriptor; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import com.zd.model.system.UnitMenu; public class ListHandler extends BaseTypeHandler { Override public void setNonNullParameter(PreparedStatement ps, int i, Object o, JdbcType jdbcType) throws SQLException { if (o null) return; ListUnitMenu list (ListUnitMenu) o; // 复用当前 PreparedStatement 的连接避免重复建连 Connection conn ps.getConnection(); try { ARRAY array getArray(conn, UNIT_MENU_OBJ, UNIT_MENU_TABLE, list); ps.setArray(i, array); } catch (Exception e) { throw new SQLException(List 转 ARRAY 失败, e); } } private ARRAY getArray(Connection con, String oracleObj, String oracleList, ListUnitMenu listData) throws Exception { ArrayDescriptor desc ArrayDescriptor.createDescriptor(oracleList, con); STRUCT[] structs new STRUCT[listData.size()]; StructDescriptor structDesc new StructDescriptor(oracleObj, con); for (int i 0; i listData.size(); i) { Object[] result { listData.get(i).getUnitId(), listData.get(i).getMenuId() }; structs[i] new STRUCT(structDesc, con, result); } return new ARRAY(desc, con, structs); } Override public Object getNullableResult(ResultSet rs, String columnName) throws SQLException { return null; } Override public Object getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return null; } }ArrayDescriptor.createDescriptor的第一个参数是数据库里的 TABLE 类型名第二个是连接StructDescriptor对应 OBJECT 类型名。构造STRUCT时传入的Object[]顺序必须和 OBJECT 类型定义完全一致。这里还有个血泪经验Oracle 驱动对类型名大小写敏感UNIT_MENU_OBJ写成小写会报「无效的名称模式」必须大写。3.3 Service 层调用与 OUT 参数回读Service 层把 List 和占位 result 放进同一个 Map调用 mapper 方法后从 Map 里取 OUT 值ListUnitMenu list new ArrayList(); String[] menuIds menuTreeIds.split(,); for (String menuId : menuIds) { UnitMenu um new UnitMenu(); um.setMenuId(menuId); um.setUnitId(unitId); list.add(um); } MapString, Object paramMap new HashMap(); paramMap.put(list0, list); paramMap.put(result0, null); // 占位OUT 参数会回填 unitMenuMapper.addUnitPower(paramMap); Integer result (Integer) paramMap.get(result0); if (result ! null result 1) { // 执行成功 } else { // 执行失败走补偿逻辑 }注意result0初始值给 null 就行MyBatis 执行完 CALLABLE 语句后会把 OUT 参数写回这个 Map。如果你给的是空字符串某些 MyBatis 版本会尝试做类型转换反而报错。这个细节当时卡了我半天日志里只看到类型转换异常完全没提示是 OUT 参数初始值的问题。4. 避坑排查这五个坑我替你踩过了4.1 报「ORA-03115: 不支持的网络数据类型或表示形式」现象调用存储过程时抛 ORA-03115堆栈指向setArray。原因JDBC 连接没有注册 Oracle 的 ARRAY 类型或者驱动版本和数据库版本不匹配。常见于用 ojdbc14 连 11g 以上数据库。解决换成 ojdbc6 或 ojdbc8并在连接 URL 或数据源配置里确认没有禁用 Oracle 扩展类型。如果用的是 Druid 连接池检查connectionProperties里没有把oracle.jdbc.mapDateToTimestamp之类的开关配错。4.2 字符串字段插入后变成乱码或问号现象unit_id 和 menu_id 插进去全是???。原因构造 STRUCT 时字符集不匹配Oracle 驱动需要orai18n.jar来做字符转换缺这个包时中文和特殊字符会丢。解决把orai18n.jar加进 classpath版本和 ojdbc 保持一致。这个包很小但缺了就是不行属于典型的「不知道就永远查不出来」的坑。4.3 List 为空时存储过程报 ORA-06531现象传空 List 进去存储过程在list0.count处抛 ORA-06531引用未初始化的集合。原因空 List 构造出的 ARRAY 长度为 0PL/SQL 端认为集合未初始化。解决在 ListHandler 里判断如果 list 为空直接构造一个空 ARRAY 并正常 setArray或者在存储过程开头加if list0 is null or list0.count 0 then result0 : 1; return; end if;做短路处理。我一般两边都做防御性编程。4.4 OUT 参数取出来始终是 null现象存储过程明明返回了 1Java 端paramMap.get(result0)却是 null。原因parameterMap里 OUT 参数的javaType和存储过程实际返回类型不匹配或者 MyBatis 版本对 CALLABLE 的 OUT 回填有 bug。解决确认javaTypejava.lang.Integer和jdbcTypeDECIMAL对应 Oracle 的out int。如果还不行改用parameterType#{}的写法在#{}里指定modeOUT这是 MyBatis 3.4.6 之后更稳的方式。4.5 批量数据量大时内存溢出现象一次传几万条数据Java 端 OOM。原因ListHandler 一次性构造了所有 STRUCT 对象内存占用是数据量的数倍。解决在 Service 层做分片比如每 2000 条调一次存储过程循环调用。存储过程本身是逐条 insert 的分片不会影响事务语义反而能降低单次内存峰值。分片大小根据你的 JVM 堆和单条数据大小调我一般从 1000 开始试。5. 进阶技巧用注解简化配置与批量分片模板跑通之后我又做了一轮优化把重复代码抽掉。第一个优化是用 MyBatis 注解替代 XML 里的 parameterMap减少配置散落。在 Mapper 接口上直接写Select({ CALL save_unit_power(#{list0, jdbcTypeARRAY, modeIN, typeHandlercom.zd.util.ListHandler}, #{result0, jdbcTypeDECIMAL, modeOUT, javaTypejava.lang.Integer}) }) Options(statementType StatementType.CALLABLE) void addUnitPower(MapString, Object paramMap);这样就不用维护单独的 parameterMap 块参数定义和 SQL 在一起改起来不容易漏。注意Options(statementType StatementType.CALLABLE)不能省否则 MyBatis 会当普通查询处理。第二个优化是分片模板。我写了一个通用的分片工具方法任何需要传 List 调存储过程的场景都能复用public static T int callInBatches(ListT data, int batchSize, FunctionListT, Integer caller) { int lastResult 1; for (int i 0; i data.size(); i batchSize) { int end Math.min(i batchSize, data.size()); ListT sub data.subList(i, end); Integer r caller.apply(sub); if (r null || r ! 1) { lastResult -1; break; // 任一批次失败即中断由调用方决定是否补偿 } } return lastResult; }batchSize我一般设 2000caller传入一个 lambda里面封装 Map 组装和 mapper 调用。这样分片逻辑和业务逻辑解耦换一个存储过程只需要改 lambda 里的内容。验证方面我习惯在存储过程里加一行dbms_output.put_line(batch count: || list0.count);然后在 Java 端用CallableStatement注册输出确认每次实际传进去的条数和预期一致。这个习惯帮我抓到过一次分片边界算错导致最后一批数据丢失的问题——subList的toIndex是开区间写i batchSize没问题但如果手滑写成i batchSize - 1最后一条就永远传不进去。从那以后我每次写分片循环都会强制在单元测试里断言「所有分片元素数量之和等于原始 List 大小」这个断言拦住过好几次边界错误。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

Flutter+鸿蒙端侧Agent:智能家居指令分发与设备联动实战

Flutter+鸿蒙端侧Agent:智能家居指令分发与设备联动实战

1. 项目缘起与整体架构设计1.1 一个真实的痛点:手机里的App都会“听”,但发令的人是我拿到“创建一款智能中枢”这个需求时,我第一反应是:市面上的语音助手、智能家居App已经多到用不过来了,真正缺的根本不是另一个App…

2026/10/17 22:50:46 阅读更多 →
RESTful API 设计标准与 HTTP 状态码实战指南

RESTful API 设计标准与 HTTP 状态码实战指南

1. 为什么我们需要认真聊聊 RESTful API 设计标准做后端开发或者前端联调的朋友,大概率都经历过这样的场景:接口文档上写着“获取用户信息”,结果请求路径是/getUserInfo.do,请求方式用的 POST,返回体里裹了三层data.r…

2026/10/17 22:49:45 阅读更多 →
Valhalla实战:静态工程审阅Claude Code生态与安全审计经验

Valhalla实战:静态工程审阅Claude Code生态与安全审计经验

Valhalla 这个名字,我第一次看到的时候就觉得很有意思。北欧神话里的英灵殿,专门收容战死沙场的勇士,而作为 Claude Code 生态里的一个 Agent Skill,它的职责也带着一股“终审”的味道——所有想进生产环境的工程,都得…

2026/10/17 22:49:45 阅读更多 →

最新新闻

IP地址如何工作:从寻址原理到网络排查实战

IP地址如何工作:从寻址原理到网络排查实战

写这篇总结之前,我先随手翻了翻之前的几篇笔记。基础概念系列连起来看,前面已经把分层模型、以太网、交换机转发这些底子打过了,这一篇正好接上:IP地址是怎么在网络里被"用"起来的。说白了,就是回答一个很实…

2026/10/17 23:47:45 阅读更多 →
Ubuntu 16.04搭建mmdetection实战:版本匹配与踩坑记录

Ubuntu 16.04搭建mmdetection实战:版本匹配与踩坑记录

1. 在16.04上做这件事前,先想清楚三件事1.1 为什么Ubuntu 16.04折腾:GLIBC和Python生态的年代差先说个现实问题:Ubuntu 16.04发布于2016年,自带GLIBC 2.23,而mmdetection的现代版本和配套编译工具链,很多编…

2026/10/17 23:47:45 阅读更多 →
网络药理学全流程实操:从靶点预测到Cytoscape网络图构建

网络药理学全流程实操:从靶点预测到Cytoscape网络图构建

简介:网络药理学将传统“一药一靶”的研究范式拓展为“一药多靶一网”,借助虚拟筛选与分子对接技术,在计算层面快速预测化合物与潜在靶点之间的相互作用。其核心原理是基于化合物结构特征与已知蛋白数据库进行反向匹配,并通过通路…

2026/10/17 23:47:45 阅读更多 →
Nacos启动报load jdbc.properties error的根源与三重校验法

Nacos启动报load jdbc.properties error的根源与三重校验法

1. 问题本质:这不是Nacos故障,而是配置加载链路的“第一道关卡”失守你看到控制台里那行醒目的红色报错:Caused by: java.lang.RuntimeException: [db-load-error]load jdbc.properties error,第一反应可能是“数据库连不上”&…

2026/10/17 23:47:45 阅读更多 →
Codex 100个真实案例 - 用AI做本地密码管理器(AES加密+自动生成)

Codex 100个真实案例 - 用AI做本地密码管理器(AES加密+自动生成)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 23:46:44 阅读更多 →
密码算法安全性检测:攻击模拟、形式化验证与统计分析三层闭环

密码算法安全性检测:攻击模拟、形式化验证与统计分析三层闭环

简介:本资源是一份面向信息安全专业学生、密码学初学者及系统安全开发者的教学型PPT课件,聚焦密码算法安全性评估方法与核心模块设计实践。内容覆盖攻击模拟(暴力破解、侧信道)、形式化验证、统计分析等检测手段,并系统…

2026/10/17 23:46:44 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介:《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南,重点围绕V3与R1两款模型的能力差异与适用场景展开,帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件,压缩包大小约311KB,篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介:这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档,共收录两百余条可直接套用的润色指令,涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度,并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介:本资源是一份基于OpenCV的本科课程设计完整报告,面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生,解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/16 21:35:51 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/17 1:44:59 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 21:54:38 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 14:23:36 阅读更多 →