MyBatis入门与实战:从JDBC痛点解析到核心机制深度应用 这类工具最值得先看的不是功能列表而是能不能在普通环境里稳定跑起来。MyBatis 就是这样一个东西它不是一个新数据库也不是一个独立系统而是一个帮你把 Java 对象和数据库表记录“粘”起来的持久层框架。如果你写过原生的 JDBC 代码就会知道从连接、拼 SQL、传参、处理结果集到关连接每一步都是重复且容易出错的体力活。MyBatis 的核心价值就是把这些重复劳动标准化、配置化让你能更专注在业务逻辑和 SQL 本身。很多人第一次接触 MyBatis 会困惑它和直接写 JDBC 有什么区别和 Hibernate 又有什么不同其实关键就两点控制权和开发效率。MyBatis 没有像 Hibernate 那样试图完全屏蔽 SQL它承认 SQL 是数据库交互的核心所以把 SQL 的编写权完全交还给你同时帮你解决 JDBC 里那些繁琐的“脏活”。这特别适合对 SQL 有优化需求或者数据库 Schema 比较复杂的项目。我建议先从最小样例开始理解 MyBatis 是怎么把一段 SQL 和一个 Java 方法绑定起来的。这比一上来就研究动态 SQL、缓存、插件这些高级特性要实在得多。下面按实际落地顺序拆一遍。1. 先弄明白 MyBatis 到底解决了 JDBC 的哪些痛点在讨论 MyBatis 怎么用之前得先搞清楚我们为什么需要它。直接使用 JDBC 不是不行但在实际项目中尤其是需要快速迭代和维护时原生 JDBC 的写法会暴露出几个非常具体的问题。1.1 原生 JDBC 的样板代码太多且容易出错一段典型的 JDBC 查询代码即便是最简单的根据 ID 查询用户也需要经历以下步骤// 1. 加载驱动现代 JDBC 通常省略由 SPI 机制自动加载 // 2. 建立连接 Connection conn null; PreparedStatement pstmt null; ResultSet rs null; User user null; try { conn DriverManager.getConnection(url, username, password); // 3. 创建 PreparedStatement编写 SQL String sql SELECT id, name, email FROM user WHERE id ?; pstmt conn.prepareStatement(sql); // 4. 设置参数 pstmt.setInt(1, userId); // 5. 执行查询 rs pstmt.executeQuery(); // 6. 遍历结果集手动映射到 Java 对象 if (rs.next()) { user new User(); user.setId(rs.getInt(id)); user.setName(rs.getString(name)); user.setEmail(rs.getString(email)); // ... 更多字段 } } catch (SQLException e) { // 处理异常 e.printStackTrace(); } finally { // 7. 按顺序关闭资源必须放在 finally 块确保执行 try { if (rs ! null) rs.close(); } catch (SQLException e) { /* ignore */ } try { if (pstmt ! null) pstmt.close(); } catch (SQLException e) { /* ignore */ } try { if (conn ! null) conn.close(); } catch (SQLException e) { /* ignore */ } }这段代码的问题非常明显重复性高每个查询、插入、更新操作都需要重复这 7 步模板代码。资源管理繁琐必须手动管理Connection、PreparedStatement、ResultSet的创建和关闭顺序不能错关闭必须放在finally块否则可能导致连接泄漏。手动映射枯燥易错从ResultSet中一个个getXxx并set到对象属性字段多时极其枯燥且容易因字段名拼写错误或类型不匹配而引入 Bug。SQL 与 Java 代码强耦合SQL 以字符串形式硬编码在 Java 文件中修改 SQL 需要重新编译 Java 代码不利于维护和 DBA 评审。1.2 MyBatis 提供的核心解决方案MyBatis 正是针对上述痛点设计的。它的工作模式可以概括为SQL 与代码分离将 SQL 语句从 Java 代码中剥离写入 XML 配置文件或注解中。这使得 SQL 更清晰便于管理和优化。自动参数映射将 Java 方法传入的参数基本类型、Map、JavaBean自动、安全地设置到 SQL 的占位符#{}中无需手动调用pstmt.setXxx。自动结果集映射将查询返回的ResultSet自动映射成 Java 对象单个对象、List、Map 等省去了手动get/set的繁琐过程。内置连接池与事务管理MyBatis 可以集成如 HikariCP、Druid 等高性能连接池并提供了简单易用的事务管理接口开发者无需关心底层的连接获取与释放。用 MyBatis 实现同样的查询代码会简洁得多。首先在 Mapper 接口中定义一个方法public interface UserMapper { User selectUserById(Integer id); }然后在一个对应的 XML 文件如UserMapper.xml中编写 SQL 和映射规则?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.mapper.UserMapper select idselectUserById resultTypecom.example.model.User SELECT id, name, email FROM user WHERE id #{id} /select /mapper最后在 Java 代码中通过SqlSession获取 Mapper 并调用try (SqlSession session sqlSessionFactory.openSession()) { UserMapper mapper session.getMapper(UserMapper.class); User user mapper.selectUserById(1); // 直接使用 user 对象 }可以看到资源管理SqlSession实现了AutoCloseable、参数设置、结果映射这些“脏活”都被 MyBatis 默默处理掉了。1.3 与 Hibernate 的定位差异为什么是“半自动化”这也是面试常问的点。Hibernate 是一个全自动的 ORM对象关系映射框架它提供了 HQLHibernate Query Language目标是让开发者完全用面向对象的方式操作数据库尽量不写 SQL。这在简单、标准的 CRUD 场景下开发效率极高。但全自动化带来了两个问题对 SQL 的控制力弱自动生成的 SQL 可能不是最优的在复杂查询、大数据量、需要深度优化时开发者往往感到束手束脚。学习曲线陡峭需要理解 Session、缓存、延迟加载、级联等复杂概念调试问题有时比较困难。MyBatis 则自称“半自动化” ORM。它不尝试生成或替换 SQL而是增强 SQL。它认为 SQL 应该是开发者的核心资产框架只负责把 SQL 执行好并把结果方便地送回来。因此MyBatis 的学习成本相对较低对于熟悉 SQL 的开发者来说上手更快在需要复杂 SQL、存储过程调用、多表关联查询等场景下更加灵活和直观。2. 环境搭建与最小可运行示例从零到一跑起来理解了“为什么用”接下来就是“怎么用”。我建议不要一开始就整合 Spring Boot先用最核心的 MyBatis 独立环境跑通一个例子这样能更清楚地看到它的核心组件是如何协作的。2.1 核心依赖与项目结构创建一个普通的 Maven 项目在pom.xml中引入 MyBatis 核心依赖和数据库驱动以 MySQL 为例dependencies !-- MyBatis 核心 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.13/version !-- 请使用当时最新稳定版 -- /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- 日志方便查看执行的 SQL -- dependency groupIdorg.slf4j/groupId artifactIdslf4j-api/artifactId version2.0.9/version /dependency dependency groupIdch.qos.logback/groupId artifactIdlogback-classic/artifactId version1.4.11/version /dependency /dependencies项目目录结构建议如下mybatis-demo/ ├── pom.xml ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ ├── model/ │ │ │ │ └── User.java # 实体类 │ │ │ ├── mapper/ │ │ │ │ └── UserMapper.java # Mapper 接口 │ │ │ └── App.java # 主类包含测试代码 │ │ └── resources/ │ │ ├── mybatis-config.xml # MyBatis 全局配置文件 │ │ └── com/example/mapper/ │ │ └── UserMapper.xml # SQL 映射文件 │ └── test/ │ └── ... # 测试代码2.2 编写核心组件1. 实体类 (User.java)这是一个简单的 POJO (Plain Old Java Object)属性对应数据库表的字段。package com.example.model; public class User { private Integer id; private String name; private String email; // 省略 getter, setter, toString 方法。实际项目中请使用 Lombok 或手动生成。 }2. Mapper 接口 (UserMapper.java)定义数据操作的方法签名。方法名与后续 XML 中的id对应。package com.example.mapper; import com.example.model.User; public interface UserMapper { User selectUserById(Integer id); }3. SQL 映射文件 (UserMapper.xml)这是 MyBatis 的灵魂。namespace属性必须对应 Mapper 接口的全限定名select标签的id对应接口方法名resultType指定返回结果的 Java 类型。?xml version1.0 encodingUTF-8 ? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.mapper.UserMapper !-- 查询语句 -- select idselectUserById resultTypecom.example.model.User SELECT id, name, email FROM user WHERE id #{id} /select /mapper注意#{id}这是 MyBatis 的参数占位符它会安全地进行预编译参数设置能有效防止 SQL 注入。4. MyBatis 全局配置文件 (mybatis-config.xml)这个文件配置数据源、事务管理器、以及告诉 MyBatis 去哪里找 SQL 映射文件。?xml version1.0 encodingUTF-8 ? !DOCTYPE configuration PUBLIC -//mybatis.org//DTD Config 3.0//EN http://mybatis.org/dtd/mybatis-3-config.dtd configuration settings !-- 开启下划线转驼峰命名映射如果数据库字段是 user_nameJava 属性是 userName -- setting namemapUnderscoreToCamelCase valuetrue/ !-- 打印查询语句到日志调试非常有用 -- setting namelogImpl valueSLF4J/ /settings environments defaultdevelopment environment iddevelopment transactionManager typeJDBC/ !-- 配置数据源这里使用最基础的 POOLED -- dataSource typePOOLED property namedriver valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/mybatis_demo?useSSLfalseserverTimezoneUTCcharacterEncodingutf8/ property nameusername valueroot/ property namepassword valueyourpassword/ /dataSource /environment /environments !-- 告诉 MyBatis Mapper 映射文件的位置 -- mappers mapper resourcecom/example/mapper/UserMapper.xml/ /mappers /configuration2.3 编写测试代码并运行在App.java中我们按步骤加载配置、创建SqlSessionFactory、获取SqlSession和Mapper最后执行查询。package com.example; import com.example.mapper.UserMapper; import com.example.model.User; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import java.io.IOException; import java.io.InputStream; public class App { public static void main(String[] args) { // 1. 加载 MyBatis 全局配置文件 String resource mybatis-config.xml; InputStream inputStream null; SqlSession sqlSession null; try { inputStream Resources.getResourceAsStream(resource); // 2. 构建 SqlSessionFactory相当于数据库连接池工厂 SqlSessionFactory sqlSessionFactory new SqlSessionFactoryBuilder().build(inputStream); // 3. 获取 SqlSession代表一次数据库会话 sqlSession sqlSessionFactory.openSession(); // 4. 获取 Mapper 接口的代理对象 UserMapper userMapper sqlSession.getMapper(UserMapper.class); // 5. 调用接口方法执行 SQL User user userMapper.selectUserById(1); System.out.println(查询到的用户: user); // 6. 提交事务如果是查询非必须。增删改需要 sqlSession.commit(); } catch (IOException e) { e.printStackTrace(); } finally { // 7. 关闭会话释放连接回连接池 if (sqlSession ! null) { sqlSession.close(); } } } }运行这段代码如果控制台打印出用户信息并且日志中能看到执行的 SQL 语句类似 Preparing: SELECT id, name, email FROM user WHERE id ?那么恭喜你第一个 MyBatis 程序就跑通了。注意这里最容易忽略的是 XML 文件的路径和namespace的对应关系。Resources.getResourceAsStream是从类路径classpath的根目录开始找文件所以配置文件要放在resources目录下并且路径要写对。namespace必须是接口的全限定名一个字母都不能错。3. 核心机制深度解析参数、结果映射与动态 SQL跑通 Demo 只是第一步。要让 MyBatis 在真实项目中发挥作用必须理解它的几个核心工作机制。很多人卡壳问题都出在这些细节上。3.1 参数传递的多种姿势#{}是 MyBatis 的参数占位符它底层使用PreparedStatement能防止 SQL 注入。参数可以来自多种类型1. 基本类型和 String// Mapper 接口 User selectByAge(int age); // XML select idselectByAge resultTypeUser SELECT * FROM user WHERE age #{age} /select#{age}里的名字age可以任意写对于单个基本类型参数MyBatis 直接使用。2. 多个参数使用Param注解当方法有多个参数时MyBatis 默认会按param1, param2, ...或者arg0, arg1, ...来识别但这可读性差。推荐使用Param注解。ListUser selectByCondition(Param(minAge) Integer minAge, Param(nameLike) String nameLike);select idselectByCondition resultTypeUser SELECT * FROM user WHERE age #{minAge} AND name LIKE CONCAT(%, #{nameLike}, %) /select3. 传入 JavaBean 对象这是最常见的方式属性名直接作为参数名。// 假设有一个 UserQuery 对象有 minAge, maxAge, name 属性 ListUser selectByUserQuery(UserQuery query);select idselectByUserQuery resultTypeUser SELECT * FROM user WHERE age BETWEEN #{minAge} AND #{maxAge} AND name #{name} /select4. 传入 MapMap 的 key 就是参数名。ListUser selectByMap(MapString, Object params);MapString, Object params new HashMap(); params.put(status, 1); params.put(role, admin); ListUser users userMapper.selectByMap(params);select idselectByMap resultTypeUser SELECT * FROM user WHERE status #{status} AND role #{role} /select#{}与${}的本质区别这是面试必问也是安全关键点。#{}是预编译处理。MyBatis 会将其替换为?然后使用PreparedStatement的set方法来安全赋值。能防止 SQL 注入。${}是字符串替换。MyBatis 会直接将${}中的内容替换到 SQL 中不做任何处理。存在 SQL 注入风险。${}一般用于动态指定表名、列名等无法使用预编译的场景。!-- 安全使用 #{} -- SELECT * FROM user WHERE name #{name} !-- 有风险如果 orderBy 来自用户输入可能被注入 -- SELECT * FROM user ORDER BY ${orderBy} !-- 合理使用 ${} 的场景动态表名但表名通常不应来自用户输入 -- SELECT * FROM ${tableName} WHERE id #{id}原则能用#{}绝不用${}。3.2 结果映射的几种方式将查询结果的列映射到 Java 对象的属性是 MyBatis 的另一个核心。1. 自动映射Auto-Mapping当数据库列名如user_name和 Java 属性名如userName遵循一定规则时MyBatis 可以自动完成映射。通过全局配置mapUnderscoreToCamelCase可以开启下划线到驼峰的自动转换。settings setting namemapUnderscoreToCamelCase valuetrue/ /settings这样SQL 查询返回的user_name列会自动映射到userName属性。2. 使用resultType指定返回结果类型的全限定类名或别名在配置文件中定义。适用于属性名和列名能自动映射的简单场景。select idselectUserById resultTypecom.example.model.User SELECT id, user_name, email FROM user WHERE id #{id} /select3. 使用resultMap推荐用于复杂场景当自动映射不满足需求时如列名和属性名差异巨大、存在一对一、一对多关联查询就需要定义resultMap。resultMap iduserResultMap typeUser !-- id 标签指定主键列提高性能 -- id propertyid columnuser_id/ !-- result 标签映射普通列 -- result propertyuserName columnuser_name/ result propertyemail columnemail_address/ !-- 关联对象映射 -- association propertydepartment javaTypeDepartment id propertyid columndept_id/ result propertyname columndept_name/ /association !-- 集合映射一对多 -- collection propertyroles ofTypeRole id propertyid columnrole_id/ result propertyname columnrole_name/ /collection /resultMap select idselectUserWithDetails resultMapuserResultMap SELECT u.id as user_id, u.name as user_name, u.email as email_address, d.id as dept_id, d.name as dept_name, r.id as role_id, r.name as role_name FROM user u LEFT JOIN department d ON u.dept_id d.id LEFT JOIN user_role ur ON u.id ur.user_id LEFT JOIN role r ON ur.role_id r.id WHERE u.id #{id} /selectresultMap提供了最精细的映射控制是处理复杂对象关系的利器。3.3 动态 SQL让 SQL 活起来这是 MyBatis 最强大的特性之一。它允许你在 XML 中编写条件判断、循环等逻辑根据传入参数动态生成不同的 SQL 片段避免了在 Java 代码中拼接 SQL 字符串的丑陋和风险。核心标签if条件判断。choose,when,otherwise多路选择类似 Java 的switch-case。where智能添加WHERE关键字并处理多余的AND或OR。set智能处理UPDATE语句中的SET子句去掉多余的逗号。foreach遍历集合常用于IN查询或批量插入。trim更通用的字符串修剪功能。示例一个多条件查询select idselectUsersDynamic resultTypeUser SELECT * FROM user where !-- 如果 name 不为 null 且不为空字符串则添加条件 -- if testname ! null and name ! AND name LIKE CONCAT(%, #{name}, %) /if !-- 如果 minAge 不为 null则添加条件 -- if testminAge ! null AND age #{minAge} /if if testmaxAge ! null AND age lt; #{maxAge} !-- XML 中 需要转义为 lt; -- /if !-- 如果 roles 集合不为空则生成 IN 子句 -- if testroles ! null and roles.size() 0 AND role IN foreach collectionroles itemrole open( separator, close) #{role} /foreach /if /where ORDER BY id /selectwhere标签会智能处理如果所有if条件都不成立则不会生成WHERE关键字如果成立它会自动去掉第一个条件前的AND或OR。批量插入示例insert idbatchInsertUsers INSERT INTO user (name, email) VALUES foreach collectionlist itemuser separator, (#{user.name}, #{user.email}) /foreach /insert这样传入一个ListUser就能生成一条包含多组值的INSERT语句比循环执行单条INSERT效率高得多。4. 进阶话题与生产实践缓存、插件、整合与排错当基本 CRUD 满足后项目规模扩大就会遇到性能、可维护性、与其他框架整合等更深层次的问题。4.1 缓存机制一级缓存与二级缓存MyBatis 内置了缓存机制来提升查询性能理解它们的工作原理和失效场景至关重要。一级缓存本地缓存范围SqlSession级别。在同一个SqlSession中执行相同的查询第二次会直接从缓存返回结果不会访问数据库。生命周期与SqlSession绑定。SqlSession关闭缓存清空。失效条件执行了 INSERT、UPDATE、DELETE 操作无论是否影响当前缓存的数据。手动调用sqlSession.clearCache()。对 Mapper 执行了flushCache属性为true的查询如某些需要实时性的统计查询。注意在 Spring 整合 MyBatis 时通常将SqlSession的生命周期与一个事务绑定。这意味着在同一个事务中一级缓存生效跨事务即使方法调用相同也会创建新的SqlSession缓存失效。这就是为什么有时“开启事务后 MyBatis 一级缓存导致数据查询不到”会成为问题——因为你可能在一个事务中修改了数据然后期望在另一个新事务中立即查到最新数据但一级缓存帮不上忙。二级缓存范围Mapper级别命名空间级别。多个SqlSession可以共享同一个 Mapper 的二级缓存。开启方式在全局配置中设置setting namecacheEnabled valuetrue/默认就是true。在需要开启的 Mapper XML 文件中添加cache/标签。工作机制当一个SqlSession查询数据后结果会被放到二级缓存中。另一个SqlSession执行相同查询时如果缓存命中则直接返回。提交 (commit) 是关键只有SqlSession执行了commit()如执行了增删改并提交它查询的数据才会被真正刷入二级缓存同时commit()也会清空该 Mapper 命名空间下的所有二级缓存数据以保证数据一致性。使用建议二级缓存适用于读远多于写且数据实时性要求不高的场景如配置表、历史数据查询。对于读写频繁或对一致性要求高的数据如账户余额、库存不建议开启二级缓存或需要精心设计缓存策略如使用更专业的 Redis。注意序列化缓存的对象需要实现Serializable接口。4.2 插件Interceptor扩展 MyBatis 的利器MyBatis 允许你编写插件来拦截其核心组件的执行过程主要是Executor执行器、StatementHandler语句处理器、ParameterHandler参数处理器、ResultSetHandler结果集处理器。这是实现自定义功能如分页、SQL 执行时间监控、数据加解密、多租户数据过滤的底层手段。一个简单的 SQL 执行时间监控插件示例Intercepts({ Signature(type Executor.class, method update, args {MappedStatement.class, Object.class}), Signature(type Executor.class, method query, args {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}) }) public class SqlCostTimeInterceptor implements Interceptor { private static final Logger log LoggerFactory.getLogger(SqlCostTimeInterceptor.class); Override public Object intercept(Invocation invocation) throws Throwable { long startTime System.currentTimeMillis(); try { // 执行原方法 return invocation.proceed(); } finally { long costTime System.currentTimeMillis() - startTime; MappedStatement mappedStatement (MappedStatement) invocation.getArgs()[0]; String sqlId mappedStatement.getId(); log.info(执行 SQL [{}] 耗时 {} ms, sqlId, costTime); // 可以在这里添加慢 SQL 告警逻辑 if (costTime 1000) { log.warn(慢 SQL 警告: {} 耗时 {} ms, sqlId, costTime); } } } Override public Object plugin(Object target) { return Plugin.wrap(target, this); } Override public void setProperties(Properties properties) { // 可以读取配置参数 } }然后在mybatis-config.xml中配置该插件plugins plugin interceptorcom.example.plugin.SqlCostTimeInterceptor/ /plugins像 PageHelper 这样的知名分页插件就是基于此机制实现的。开发插件需要对 MyBatis 运行原理有较深理解谨慎使用。4.3 与 Spring/Spring Boot 整合在实际企业开发中几乎不会单独使用 MyBatis而是与 Spring 框架整合让 Spring 的 IOC 容器来管理 MyBatis 的组件SqlSessionFactory、Mapper接口代理等和事务。Spring Boot 整合最简方式添加依赖dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version3.0.3/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency在application.yml中配置数据源和 MyBatisspring: datasource: url: jdbc:mysql://localhost:3306/demo username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver hikari: # 使用 HikariCP 连接池 connection-timeout: 30000 maximum-pool-size: 10 mybatis: # mapper.xml 文件位置 mapper-locations: classpath:mapper/*.xml # 实体类所在包用于配置 type-aliases-package type-aliases-package: com.example.model configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印 SQL在 Mapper 接口上添加Mapper注解或者在主类上加MapperScan(com.example.mapper)扫描整个包。在 Service 中直接Autowired注入 Mapper 接口使用即可。事务由 Spring 的Transactional注解管理。这种整合方式极大简化了配置是当前的主流做法。4.4 常见问题与排查链路遇到 MyBatis 相关问题时建议按以下顺序排查“BindingException: Invalid bound statement (not found)”现象最常见的启动错误提示找不到 Mapper 方法对应的 SQL 语句。原因MyBatis 找不到namespace.id对应的 SQL。排查检查 Mapper 接口的namespace全限定名和 XML 中的namespace是否完全一致包括大小写。检查方法名和 XML 中 SQL 标签的id是否一致。检查mapper-locations配置的路径是否包含了你的 XML 文件。检查 Maven 构建时XML 文件是否被正确复制到了target/classes目录下检查pom.xml中是否有resources配置。SQL 执行报错但 SQL 在数据库客户端执行正常原因往往是参数映射或 SQL 语法在 MyBatis 动态处理后与预期不符。排查开启日志在配置中设置log-impl为StdOutImpl或SLF4J查看 MyBatis 实际发送到数据库的 SQL 和参数。这是最有效的调试手段。检查动态 SQL 中的条件判断if是否因参数为null或空导致 SQL 结构异常如WHERE后直接跟AND。检查#{}和${}的使用是否正确。查询结果映射失败部分属性为 null原因列名和属性名无法自动映射。排查确认是否开启了mapUnderscoreToCamelCase。查看日志中的 SQL确认 SELECT 的列名是什么。使用resultMap进行显式、精确的映射。检查数据库返回的列名是否包含空格等特殊字符。性能问题现象查询慢。排查通过日志查看 SQL 执行时间可用插件。检查是否产生了 N1 查询问题在循环中查询关联数据。应使用association或collection进行连接查询一次性获取。检查二级缓存是否配置合理是否因频繁更新导致缓存频繁失效。分析 SQL 本身在数据库端使用EXPLAIN查看执行计划。事务不生效现象在 Spring 中Transactional标注的方法内前面的更新操作失败但后面的操作没回滚。排查确认方法是否是public的Spring AOP 基于代理对非 public 方法无效。确认异常是否被捕获且未抛出。默认只对RuntimeException和Error回滚检查是否抛出了受检异常Exception且未在Transactional中声明rollbackFor。确认是否在同一个类中方法 A 调用了方法 BB 有Transactional这会导致 B 的事务注解失效代理对象问题。可通过将 B 方法移到另一个 Service或使用AopContext.currentProxy()解决。我个人更建议先把单任务跑稳再考虑批量和接口。对于 MyBatis真正落地时最该盯住的不是它有多少炫酷的功能而是 XML 文件的管理、SQL 的质量、以及如何与项目中的连接池、事务管理器、监控体系顺畅协作。很多项目后期的问题根源都在初期对这些“基础设施”的随意处理上。如果只是学习默认配置够用如果要长期使用就要把日志规范、SQL 评审流程和性能监控提前规划好。