很多刚接触持久层框架的人学 MyBatis 的路径都出奇地一致找一份能跑的 demo把全局配置和映射文件原样抄进去改一下数据库地址跑通一条查询然后觉得自己会了。等到同事把一段复杂点的需求丢过来——多表关联、动态条件、批量更新、返回嵌套对象——才发现自己连#{}和${}的区别都说不清楚报错信息看一眼就想关电脑。我这几年带过不少人也帮人排查过大量BindingException、Invalid bound statement、结果集全为 null 这类问题绝大多数根子都不在 MyBatis 本身而在于学之前没搞清楚它是干什么的、它是怎么跑的。这篇东西就是写给准备上手 MyBatis 的人的不急着让你写代码先把持久层这一层的历史包袱、MyBatis 的设计取舍、核心组件的运行链路讲透。看完之后你再去抄配置脑子里是有图的出了问题知道往哪个环节找。无论你是刚学完 JDBC 的学生还是被 JPA 坑过想换一套方案的后端或者是想搞清楚框架底层的老手这里的内容都能直接用上。1. 先把话说清楚MyBatis 到底解决的是什么问题1.1 从一段原生 JDBC 代码说起理解一个框架的价值最快的办法是回到没有它的年代。假设要从用户表按主键查一条记录纯 JDBC 的写法大致是这样Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DriverManager.getConnection(url, user, password); String sql SELECT id, user_name, age, create_time FROM t_user WHERE id ?; ps conn.prepareStatement(sql); ps.setLong(1, userId); rs ps.executeQuery(); if (rs.next()) { User u new User(); u.setId(rs.getLong(id)); u.setUserName(rs.getString(user_name)); u.setAge(rs.getInt(age)); u.setCreateTime(rs.getTimestamp(create_time)); return u; } } catch (SQLException e) { throw new RuntimeException(e); } finally { if (rs ! null) try { rs.close(); } catch (SQLException ignored) {} if (ps ! null) try { ps.close(); } catch (SQLException ignored) {} if (conn ! null) try { conn.close(); } catch (SQLException ignored) {} } return null;这段代码能跑但你把它放大到一整个项目里看问题就密集出现了。第一资源关闭的三层嵌套 finally 几乎每个方法都要重复一遍谁少写一个大括号就是连接泄漏线上跑几天连接池就爆了。第二SQL 字符串硬编码在 Java 文件里改一个字段名要重新编译、重新打包、重新上线DBA 想在发布前审一遍 SQL 都没法审。第三结果集到对象的映射全靠手写一张二十个字段的表rs.getString能写到你手抽筋而且列名写错一个字母编译期完全不报错运行时才给你一个 null。第四参数赋值是按下标来的ps.setString(1, ...)、ps.setString(2, ...)中间插入一个参数后面全乱。很多人会想那我封装一层工具类不就行了确实早期项目里大量存在这种自研的BaseDao。但一旦你自己开始封装就会撞上新的麻烦分页怎么统一处理动态条件怎么拼才不会漏掉WHERE 11返回值是 List、是单对象还是 Map怎么用一个方法覆盖批量的场景怎么复用连接你实际上是在重新发明一个持久层框架而且大概率发明得比现成的差。MyBatis 就是在这一堆重复劳动之上把SQL 怎么写这件事留给你SQL 怎么执行、结果怎么映射、资源怎么管这件事收归框架。1.2 ORM 不是万能药半自动的取舍逻辑行业里谈持久层绕不开 ORM 这个词。全自动 ORM 的思路是你只描述对象和表的对应关系框架帮你生成 SQL。半自动的思路正好相反SQL 你自己写框架负责把参数塞进去、把结果搬出来。MyBatis 属于后者官方文档里也一直强调它是半自动映射。这个定位不是技术能力不够而是主动取舍。全自动方案的好处是简单场景下开发极快一张单表增删改查几行注解就完事了。但它的问题也很实在复杂查询一旦超出框架的表达能力你要么被迫写一堆框架特有的语法要么干脆退化回原生 SQL两套东西并存维护成本直接翻倍。更麻烦的是性能调优——DBA 拿着慢查询日志找到你你看着框架生成出来的那段又长又绕的 SQL不知道该从哪下手。半自动方案把 SQL 的控制权完整交还给开发者。这对开发者的要求更高你得会写 SQL得知道索引怎么走得明白执行计划。但换来的是确定性——线上跑的那条语句就是你在映射文件里写的那条一个字都不差。我个人的判断标准很简单如果一个系统里有大量多表关联、聚合统计、报表类查询或者对 SQL 性能有明确的调优要求半自动的方案更合适如果系统绝大部分是单表操作、业务模型稳定、团队更希望少写 SQL那全自动方案能省不少事。这不是信仰问题是场景问题后面第 5 节我会展开做对比。1.3 什么样的项目适合上 MyBatis具体到落地我见过用得好和用崩的两类项目特征还挺明显。用得好的通常满足这几条业务查询以手写 SQL 为主团队里有人能把 SQL 写好数据表结构相对稳定但查询条件变化多需要动态拼接有历史遗留的存储过程或者复杂视图需要复用对返回结果的形状有精细控制的需求比如一个查询要返回带嵌套列表的对象。这些场景下 MyBatis 的映射能力是实打实的生产力。用崩的项目往往是另一种情况团队里没人愿意写 SQL所有查询都指望框架生成于是把 MyBatis 当成 JPA 用硬套通用 Mapper结果遇到稍微复杂一点的查询就卡住或者是新手把业务逻辑写进了映射文件的动态 SQL 里一个if套了八层谁都不敢改。这不是框架的锅是使用姿势的问题。提示如果你的项目里 SQL 数量很少、几乎全是单表 CRUD而且团队对 SQL 不熟上 MyBatis 的学习成本可能高于收益。工具选型要看团队的平均水平不是看它是不是主流。2. 学之前必须打牢的三块地基2.1 SQL 功底框架不会替你写 SQL这是我最想强调的一点。MyBatis 的学习曲线里真正陡的部分不在框架 API而在 SQL。框架能帮你把参数安全地传进去、把结果优雅地映射出来但它不能帮你判断这条 SQL 该不该走索引、能不能用覆盖索引、LEFT JOIN和INNER JOIN在这里的区别会不会导致数据缺失。打个比方MyBatis 像一套很好的厨具刀快、锅好、火候稳但它不会替你想这道菜该放多少盐。你给它的 SQL 是SELECT * FROM t_order WHERE DATE(create_time) 2024-01-01它照样老老实实执行只是这个写法会让索引失效几百万行的表扫起来能把数据库拖垮。框架不会拦你也不会警告你它只是忠实地执行。所以在上手之前我建议至少把这几个东西过一遍JOIN的几种类型和它们的语义差别GROUP BY配合聚合函数的使用子查询和EXISTS的适用场景索引的基本原理什么情况下会失效EXPLAIN怎么看重点看type、key、rows、Extra这几列。这些不需要你成为 DBA但至少看到一条慢 SQL 时你得能判断问题出在哪。还有一个很实用的习惯写映射文件之前先把 SQL 放到数据库客户端里跑通确认结果集是对的再搬进 XML。我见过太多人直接在 XML 里调试 SQL改一个字符重启一次应用半小时改不出一条语句。客户端里跑通、EXPLAIN看一眼、确认没问题再贴进映射文件效率差好几倍。2.2 JDBC 基础知道框架在背后替你做了什么第二块地基是 JDBC。不是要你手写 JDBC 代码而是要明白Connection、PreparedStatement、ResultSet这三个东西的角色因为 MyBatis 底层跑的还是它们。这里有个概念必须先搞清楚预编译。PreparedStatement在发送 SQL 之前数据库会先对这个带占位符的语句做语法解析、生成执行计划并缓存起来后续传不同的参数可以复用这个计划。这既是性能优化的手段也是防注入的关键——因为参数是作为数据传过去的不会被当成 SQL 语法的一部分解析。MyBatis 里的#{}之所以安全根子就在这里。还有个容易忽略的点是连接的生命周期。数据库连接是稀缺资源建立一次 TCP 连接加上认证握手开销远大于执行一条简单查询。所以生产环境一定用连接池而连接池的核心约束是借出去的连接必须还回来。MyBatis 的SqlSession本质上就是对Connection的一层包装如果你用完不关连接就还不回去。这就是为什么后面讲生命周期时我会反复强调SqlSession必须close。事务也是一样。JDBC 默认是自动提交每条语句执行完就落库。一旦你手动setAutoCommit(false)就必须显式commit或rollback。MyBatis 把这层封装了但语义没变很多人第一次用的时候发现数据插进去了又没了八成就是事务没提交就关了会话。2.3 反射与动态代理框架的两大底层手法第三块是 Java 基础里的反射和动态代理。这两样东西不影响你写出能跑的代码但决定了你能不能看懂框架的报错以及在出问题时能不能定位。反射的用途很直接MyBatis 需要在你不知情的情况下把ResultSet里的列值塞进你定义的实体对象。它拿到的是Class对象不知道你的类叫什么、有哪些字段只能靠反射在运行时读取字段、调用 setter。所以你写的实体类必须有无参构造方法属性要有 getter/setter这不是框架的洁癖是反射的硬性要求。如果你用的是 Lombok 的Data它已经帮你生成了但如果你手写实体又漏了无参构造运行时就可能报实例化失败的错。动态代理解决的是另一个问题你只写了一个接口比如UserMapper里面只有方法声明一行实现都没有为什么调用userMapper.selectById(1L)就能跑到映射文件里的 SQL因为SqlSession.getMapper()返回的不是你的实现类而是一个 JDK 动态代理对象。这个代理拦截了你的方法调用从方法名反推出要执行哪条MappedStatement然后走完整的执行链路。理解这一点你就能明白为什么下面这些约束是硬性的约束原因违反后的典型报错接口全限定名 映射文件 namespace代理靠 namespace 方法名定位语句Invalid bound statement (not found)方法名 映射文件标签 id同上同上接口不能重载同名方法一个 id 只能对应一条语句启动时解析冲突参数个数大于 1 时建议加Param否则按 arg0/param1 命名容易对不上参数绑定异常或取值为 null有了这三块地基你再去看 MyBatis 的配置项就不会觉得它们是一堆必须这么写的咒语而是能对应到具体的运行时行为上。3. 核心架构拆解五个组件跑通一条查询3.1 SqlSessionFactory 与 SqlSession 的生命周期MyBatis 的入口是两个对象名字很像作用完全不同混淆它们是新手最常犯的错之一。SqlSessionFactory是重量级对象内部持有Configuration全局配置 所有映射语句的注册表和Environment数据源、事务工厂。构建它的过程要解析 XML、注册所有映射、初始化类型处理器开销不小。所以它必须是应用级单例整个应用启动时创建一次之后一直复用。如果你每次查询都new SqlSessionFactoryBuilder().build(...)等于每次都在重新解析一遍配置性能直接崩掉。// 正确做法全局唯一随应用启动创建 String resource mybatis-config.xml; InputStream is Resources.getResourceAsStream(resource); SqlSessionFactory factory new SqlSessionFactoryBuilder().build(is);SqlSession则是轻量级对象代表一次数据库会话内部持有一个Connection。它不是线程安全的绝对不能做成成员变量被多个线程共享。标准用法是用时开、用完关而且关闭必须放在finally里或者用 try-with-resourcestry (SqlSession session factory.openSession()) { UserMapper mapper session.getMapper(UserMapper.class); User user mapper.selectById(1L); session.commit(); // 写操作必须提交 }这里有个细节值得说清楚openSession()默认是不自动提交的openSession(true)才是自动提交。很多人在测试时发现 insert 没生效就是忘了 commit。我个人的习惯是统一用默认的不自动提交手动控制事务边界因为自动提交会让多条语句之间失去原子性。注意和 Spring 整合之后SqlSession的生命周期由框架托管你会看到注入Mapper接口直接就能用看不到openSession。这不是框架消失了而是SqlSessionTemplate帮你做了代理和线程绑定。理解裸用时的生命周期排错时才知道该看哪里。3.2 Executor 执行器与一级缓存的关系SqlSession拿到方法调用后真正干活的是Executor。它有三种基础实现外加一个装饰器这个设计直接决定了很多行为表现。执行器行为特点适用场景SimpleExecutor每次执行都新建 Statement用完即关默认最稳妥ReuseExecutor按 SQL 文本缓存 Statement 复用同一会话内重复执行相同 SQLBatchExecutor攒批统一 flush返回影响行数延迟大批量写入例如初始化数据CachingExecutor装饰器套在上面三种之外负责二级缓存开启二级缓存时自动启用你可以在openSession时指定执行器类型但绝大多数情况下用默认的SimpleExecutor就够了。真要优化批量写入更常见的做法是在映射文件里用foreach拼接批量 insert或者配合框架提供的批量模式而不是死磕执行器类型。一级缓存是所有执行器都有的作用域是同一个SqlSession。它默认开启不需要任何配置。行为是同一条MappedStatement、同样的参数、同样的 SQL在同一个会话内第二次执行时直接返回上次的结果不再发请求给数据库。try (SqlSession session factory.openSession()) { UserMapper mapper session.getMapper(UserMapper.class); User u1 mapper.selectById(1L); // 真实查询 User u2 mapper.selectById(1L); // 命中一级缓存不发 SQL System.out.println(u1 u2); // 输出 true是同一个对象引用 }这个机制能省掉不少重复查询但它也是很多数据不一致问题的来源。最典型的场景是你在同一个会话里先查了一次然后在别处另一个会话把这条数据改了再查一次拿到的还是旧值。另外只要会话内执行了任意的 insert/update/delete一级缓存就会被整个清空——注意是整个不是只清受影响的那条这也是为什么有些人在循环里混着读写时会觉得缓存没起作用。还有一个坑很隐蔽和 Spring 整合后如果你没有把方法放在同一个事务里每次 mapper 调用可能拿到不同的SqlSession一级缓存就等于失效了。所以一级缓存怎么没生效这类问题先看事务边界别急着怀疑配置。3.3 MappedStatement一条 SQL 的完整描述启动解析阶段每一个select、insert、update、delete标签都会被解析成一个MappedStatement对象注册进Configuration里以namespace id作为唯一键。这个对象里装的是这条语句的全部元信息id命名空间加方法名组成的全限定标识sqlSourceSQL 的抽象动态 SQL 会被封装成DynamicSqlSource静态的则是RawSqlSource参数映射配置parameterType、Param带来的ParamMap结果映射配置resultType或者resultMap语句类型、缓存配置、超时时间、fetchSize等执行参数理解这个对象的存在能解释很多现象。比如Invalid bound statement (not found)这个报错本质上就是框架在Configuration的映射表里按namespace.id找不到对应的MappedStatement。可能的原因有映射文件根本没被加载路径不对、Maven 没把 XML 打包进去、namespace写错了、方法名和id不一致、或者接口和 XML 放在了不同的模块里没被扫描到。按这个顺序排查基本一次就能定位。还有一个概念叫BoundSql它在执行阶段才产生代表一条 SQL 经过动态标签计算后的最终形态带?的 SQL 语句加上一个参数列表。调试时如果想看最终执行的 SQL看的就是它。3.4 参数映射与结果映射的两条链路一条语句的执行可以切成两段中间夹着数据库调用前半段把 Java 参数变成 JDBC 参数后半段把ResultSet变回 Java 对象。这两段分别由ParameterHandler、ResultSetHandler配合大量的TypeHandler完成。参数侧TypeHandler负责 Java 类型和 JDBC 类型的双向转换。框架内置了几十种覆盖常见类型。遇到特殊类型比如 JSON 字段、枚举、自定义的金额对象你可以写自己的TypeHandler注册进去。新手常见的困惑是我传了一个日期对象为什么存进去少了几个小时多半是TypeHandler选的 JDBC 类型不对或者数据库字段类型和 Java 类型不匹配。结果侧resultType和resultMap二选一。前者走自动映射规则是按列名去匹配属性名。这里最容易踩的坑是下划线命名数据库列叫user_nameJava 属性叫userName默认的自动映射不开驼峰转换匹配不上结果就是所有字段都是 null你查出来一个空对象还不知道为什么。解决办法有两个要么在全局配置里打开mapUnderscoreToCamelCase要么老老实实写resultMap。settings setting namemapUnderscoreToCamelCase valuetrue/ /settings我个人的偏好是简单单表查询用resultType加驼峰开关够快够省事涉及嵌套对象、集合、字段名和属性名对不上的场景一律用resultMap显式声明。后者啰嗦一点但一眼能看出映射关系后期维护省心得多。4. 配置文件与映射文件最小可运行骨架4.1 全局配置文件的关键节点全局配置通常叫mybatis-config.xml里的标签不少但真正需要你手工管的只有几个。其余的用默认值就行改错了反而出问题。配置项作用我的实践建议environments数据源和事务管理器实际项目基本交给 Spring 托管这段可以不要settings全局行为开关只改必要的重点开驼峰映射和日志实现typeAliases给类型起短名小项目可以用大项目建议写全限定名避免重名冲突typeHandlers自定义类型转换遇到 JSON 字段、枚举时才加mappers注册映射文件生产环境用包扫描别一个个手写plugins拦截器分页、SQL 日志这类插件挂在这里settings里我最建议先开的两项一个是mapUnderscoreToCamelCase一个是日志实现。后者决定了你能不能看到实际执行的 SQLsettings setting namemapUnderscoreToCamelCase valuetrue/ setting namelogImpl valueSLF4J/ /settings配上日志框架之后把 mapper 所在包的日志级别调到 DEBUG控制台就会打印Preparing、Parameters、Total这几行分别是最终 SQL、实际参数值、返回行数。调试阶段这一步能省掉你 80% 的猜测工作。我养成的一个习惯是新项目接入 MyBatis 的第一件事就是把这段日志配好跑通一条查询看到三行输出才继续往下写。关于typeAliases有个细节如果配了包扫描框架会把包下所有类注册成别名别名默认是类名首字母小写。这时候如果两个不同包下都有同名的类注册时会直接报错。所以中大型项目我更倾向于不配别名老老实实写全限定类名虽然长一点但不会出现莫名其妙的冲突。4.2 映射文件的四类标签与动态 SQL映射文件是 MyBatis 的主战场四类标签对应四种语句属性大同小异。以最典型的select为例除了id、parameterType、resultType还有几个容易被忽略但很有用的属性useCache这条语句是否使用二级缓存默认 true需要关掉时显式设为 falseflushCache执行后是否清空缓存查询默认 false增删改默认 truetimeout超时秒数长查询建议设置避免把连接池占死statementTypePREPARED默认、STATEMENT、CALLABLE调用存储过程时用第三个真正的重头戏是动态 SQL。四个标签撑起了绝大部分条件拼接需求if做条件判断choose/when/otherwise做多分支选择where和set负责去掉多余的AND和逗号foreach处理集合遍历。select idsearch resultTypecom.example.demo.User SELECT id, user_name, age FROM t_user where if testname ! null and name ! AND user_name LIKE CONCAT(%, #{name}, %) /if if testminAge ! null AND age gt; #{minAge} /if if testids ! null and ids.size() 0 AND id IN foreach collectionids itemitem open( separator, close) #{item} /foreach /if /where ORDER BY id DESC /select这段代码里有几个必须说清楚的点。where会智能地去掉开头多余的AND或OR如果所有条件都不成立它连WHERE关键字都不会输出——这就是为什么不需要再写WHERE 11那种写法虽然能用但会让索引优化的空间变小也不好看。if里的test属性是 OGNL 表达式gt;是 XML 转义后的直接写大于号会导致 XML 解析失败这是新手第一次写动态 SQL 必踩的坑。foreach的collection属性如果你传的是List固定写list传的是数组写array用了Param就写注解里的名字这点必须记牢写错了就是Parameter ids not found。set用在 update 里作用是去掉最后一个多余的逗号。choose适合多选一的场景比如排序字段只允许从前端白名单里选一个这时候可以用它把非法值挡掉——注意这里必须用${}才能拼字段名但前提是白名单校验已经做过了绝不能把用户输入直接拼进去。4.3 参数占位符#{}与${}的本质差别这个知识点几乎每篇教程都讲但真正理解的人不多。两者的差别不是一个安全一个不安全这么简单而是编译时机不同。#{}会被解析成 JDBC 的?占位符走PreparedStatement预编译参数作为数据传过去。它在 SQL 里是安全的因为数据库从语法层面就不认为它是 SQL 的一部分。而且预编译语句可以被数据库缓存执行计划重复执行时省掉解析开销。${}是纯粹的字符串替换发生在 SQL 拼接阶段替换完之后整条语句才发给数据库。参数会被当成 SQL 语法的一部分解析因此存在注入风险。但它也有不可替代的用途表名、列名、排序关键字这些位置不能用占位符只能用字符串拼接。因为预编译的?只能替换值不能替换标识符。!-- 正确值用 #{} -- SELECT * FROM t_user WHERE user_name #{name} !-- 正确标识符用 ${}但必须有白名单校验 -- SELECT * FROM t_user ORDER BY ${sortColumn} !-- 错误LIKE 里包 #{}会拼成 %?% 报错 -- SELECT * FROM t_user WHERE user_name LIKE %#{name}% !-- 正确LIKE 的两种写法 -- SELECT * FROM t_user WHERE user_name LIKE CONCAT(%, #{name}, %)注意任何时候用${}拼接来自外部的值都必须经过白名单或严格的格式校验。这不是框架会帮你兜底的事出了事没有兜底。4.4 一个能跑起来的最小闭环把上面的东西串起来最小可运行的结构是这样一个mybatis-config.xml负责全局配置和数据源一个UserMapper.xml放 SQL一个UserMapper接口做声明一个实体类承接结果。public interface UserMapper { User selectById(Param(id) Long id); ListUser search(Param(name) String name, Param(minAge) Integer minAge, Param(ids) ListLong ids); }mapper namespacecom.example.demo.mapper.UserMapper select idselectById resultTypecom.example.demo.entity.User SELECT id, user_name, age, create_time FROM t_user WHERE id #{id} /select /mapper调用的时候try (SqlSession session factory.openSession()) { UserMapper mapper session.getMapper(UserMapper.class); User user mapper.selectById(1L); System.out.println(user); }如果这时候打印出来是一个User对象但所有字段都是 null去查两件事列名和属性名是否对得上有没有开驼峰映射以及实体类有没有无参构造和 setter。如果报Invalid bound statement去查 namespace 和方法名。如果报参数找不到去查Param和collection的配置。这三类问题覆盖了新手期 90% 的报错。另外提醒一句如果你用 Maven 构建XML 放在src/main/java下的包目录里默认是不会被复制到target/classes的必须在pom.xml里配置资源过滤否则运行时找不到文件。这个坑我见过太多人踩报错信息还只告诉你找不到资源完全不提打包的事。5. 和全自动 ORM 的取舍不是谁替代谁5.1 三个方案的正面对比把 JDBC、半自动、全自动三套方案放在一起看差异会非常直观维度原生 JDBCMyBatis全自动 ORMSQL 控制权完全在手完全在手框架生成难以干预结果映射手写繁琐易错自动或显式配置全自动复杂查询表达力无上限无上限受框架语法限制数据库移植性需自行处理方言SQL 层需适配较好但仍有方言差异学习成本低但重复劳动多中需懂 SQL中需懂框架模型性能调优空间最大大受限适合团队任意SQL 能力较强的团队偏好少写 SQL 的团队从这张表能看出一条清晰的分界SQL 复杂度和团队 SQL 能力是决定选型的两个主变量。业务简单、团队偏应用层思维全自动方案能显著提升开发速度业务复杂、有明确的性能要求半自动方案的确定性更有价值。5.2 我实际做选型时的判断顺序拿到一个新项目我的判断顺序大概是这样的。先看数据模型的复杂程度如果是一堆单表、关联很少、查询条件固定全自动方案能省掉大量模板代码。再看有没有报表和统计类的需求这类需求往往是多表关联加聚合写在自动生成的链式调用里会非常别扭甚至写不出来。接着看团队的 SQL 水平如果没人能独立写出带子查询的统计语句上 MyBatis 会变成灾难因为框架不替你思考。还有一个经常被忽略的因素是遗留系统。如果数据库里已经有大量视图、存储过程、复杂的历史 SQL半自动方案的接入成本低得多因为你可以把这些 SQL 原样搬进来。全自动方案遇到这种场景往往要在实体映射和原生查询之间来回切换维护起来很难受。最后说个务实的观点这两类框架并不互斥。我见过一些项目核心业务用半自动方案保证可控性配置表、字典表这类简单 CRUD 用另一套方案各取所长。关键是团队要有一套统一的约定别让同一个人在不同模块里反复切换写法那才是真正的维护成本。6. 新手最容易踩的坑与排查实录6.1 常见问题速查表下面这张表是我这些年整理出来的高频问题基本覆盖了入门阶段能遇到的大部分报错。排查的时候从上往下对别一上来就怀疑框架有 bug。现象最可能的原因处理办法Invalid bound statement (not found)namespace 与接口全限定名不一致方法名与 id 不一致XML 没被打包逐项核对三者检查target/classes下有没有 XML查询结果对象字段全为 null列名与属性名不匹配未开驼峰映射实体缺少无参构造或 setter开mapUnderscoreToCamelCase补全 getter/setter参数绑定异常或参数为 null多参数未加Paramforeach的 collection 名写错统一加ParamList 用list数组用array插入成功但数据库没数据会话未commit写操作后显式提交或交给事务管理动态 SQL 报 XML 解析错误、、未转义用lt;、gt;、amp;或用![CDATA[]]LIKE查询报语法错误写成%#{name}%改用CONCAT(%, #{name}, %)连接池耗尽、应用卡死SqlSession未关闭用 try-with-resources确保 close同一会话内查到旧数据一级缓存未失效换会话、手动清缓存或理清读写顺序批量插入极慢逐条提交未走批处理用foreach拼接批量 insert 或开启批量模式排序字段拼接后注入风险用了${}且未校验白名单校验后再拼接6.2 几个不会写在文档里的实操心得第一永远给多参数方法加Param。不加也能跑框架会用arg0、arg1、param1、param2这样的名字但一旦你的 SQL 里引用参数时写错了位置报错信息非常模糊。加上注解之后名字是人看得懂的出问题一眼就能定位。更重要的是if testname ! null这种写法里的变量名直接就等于注解里的名字不加注解就得写arg0可读性差到没法维护。第二调试 SQL 的时候先把参数值打出来。日志里Preparing那行是带占位符的语句Parameters那行才是实际值。很多人看着Preparing: SELECT ... WHERE name ?就开始怀疑参数没传进去其实参数就在下一行。养成看两行的习惯能省掉大量无用排查。第三复杂的动态 SQL 先在数据库里跑一遍。我处理过一个案例一段带五层if的查询本地跑得好好的线上某个条件组合下就报错。最后发现是当所有条件都为空时where虽然能去掉多余的AND但后面紧跟的ORDER BY之前少了一个空格拼出来的语句语法不对。这种问题在 XML 里盯半小时也看不出来但把最终 SQL 复制到客户端一执行错误立刻现形。所以我现在的习惯是只要动态标签超过三个一定先确认拼出来的完整语句长什么样。第四别把业务判断塞进 SQL 里。我见过映射文件里写了几百行的CASE WHEN把各种状态、各种边界条件全铺在 SQL 里。这种代码改一次心惊胆战因为没人知道这段逻辑到底被哪些场景走到。合理的做法是SQL 只负责取数据取回来之后在 Java 层做业务判断。SQL 里保留的应该是那些能让数据库高效完成的过滤和聚合。第五分页别自己拼。手写LIMIT很容易出错尤其是需要同时查总数的时候两条 SQL 的WHERE条件必须完全一致改了一处忘了另一处就是数据错乱。要么用现成的分页插件要么把条件抽成sql片段复用。这一点看起来是小技巧实际上能避免大量隐蔽的不一致问题。第六写映射文件之前先想清楚返回结构。是要一个平坦的对象还是要带嵌套列表的树形结构前者用resultType就够了后者必须上resultMap配合collection和association。这个决定做在前面能避免后面推倒重来。嵌套映射里还有个坑是主键重复导致集合被覆盖需要在resultMap上指定id标签来标识唯一性否则框架无法判断哪些行应该聚合成一个对象。我在实际带人的过程中发现凡是把这些前置知识过一遍的人后面写 MyBatis 的顺畅程度明显不一样报错时也更少慌神因为他们脑子里有一条完整的链路配置怎么解析、语句怎么注册、参数怎么绑定、结果怎么映射、缓存怎么生效。这条链路清楚了框架就不再是黑盒而是一套可以自己拆解、自己调试的工具。至于那些高级特性——自定义拦截器、二级缓存的坑、和各类框架的整合细节——等你把上面这些跑熟了再碰会轻松很多。
阅读完成 · 觉得有帮助?