MyBatis 常见错误与解决方案:从坑中爬出的实战指南
🔍 MyBatis 常见错误与解决方案:从坑中爬出的实战指南
文章目录
- 🔍 MyBatis 常见错误与解决方案:从坑中爬出的实战指南
- 🐛 一、N+1 查询问题与性能优化
- 💡 什么是 N+1 查询问题?
- ⚠️ 错误示例
- ✅ 解决方案
- 📊 性能对比
- 🔄 二、映射异常与懒加载问题
- 💡 常见映射错误
- 🛡️ 映射最佳实践
- ⚙️ 三、配置陷阱与解决方案
- 💡 常见配置问题
- 📝 完整配置示例
- 🛠️ 四、调试技巧与最佳实践
- 💡 高效调试技巧
- 🎯 调试检查清单
- 📊 常见错误速查表
- 💡 五、总结与预防策略
- 📚 核心建议
- 🛡️ 预防策略
- 🔧 必备工具推荐
- 🚀 持续改进建议
🐛 一、N+1 查询问题与性能优化
💡 什么是 N+1 查询问题?
⚠️ 错误示例
// 服务层代码
public List<User> getUsersWithOrders() {// 第一次查询:获取所有用户List<User> users = userMapper.selectAllUsers();for (User user : users) {// 第N次查询:为每个用户查询订单List<Order> orders = orderMapper.selectByUserId(user.getId());user.setOrders(orders);}return users;
}
控制台输出:
DEBUG: ==> Preparing: SELECT * FROM users
DEBUG: ==> Parameters:
DEBUG: <== Total: 100DEBUG: ==> Preparing: SELECT * FROM orders WHERE user_id = ?
DEBUG: ==> Parameters: 1(Integer)
DEBUG: <== Total: 3DEBUG: ==> Preparing: SELECT * FROM orders WHERE user_id = ?
DEBUG: ==> Parameters: 2(Integer)
DEBUG: <== Total: 2...(重复100次)...
✅ 解决方案
方案1:使用连接查询(推荐)
<!-- UserMapper.xml -->
<select id="selectUsersWithOrders" resultMap="userWithOrdersMap">SELECT u.*, o.id as order_id, o.amount, o.create_timeFROM users uLEFT JOIN orders o ON u.id = o.user_id
</select><resultMap id="userWithOrdersMap" type="User"><id property="id" column="id"/><result property="name" column="name"/><collection property="orders" ofType="Order"><id property="id" column="order_id"/><result property="amount" column="amount"/><result property="createTime" column="create_time"/></collection>
</resultMap>
方案2:使用批量查询
// 先批量查询所有订单,再在内存中分组
public List<User> getUsersWithOrdersBatch() {List<User> users = userMapper.selectAllUsers();List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());// 一次查询获取所有订单List<Order> allOrders = orderMapper.selectByUserIds(userIds);// 内存中分组Map<Long, List<Order>> ordersByUserId = allOrders.stream().collect(Collectors.groupingBy(Order::getUserId));users.forEach(user -> user.setOrders(ordersByUserId.get(user.getId())));return users;
}
方案3:使用MyBatis的嵌套查询(小数据量适用)
<resultMap id="userWithOrdersMap" type="User"><id property="id" column="id"/><result property="name" column="name"/><collection property="orders" ofType="Order" select="selectOrdersByUserId" column="id"/>
</resultMap><select id="selectOrdersByUserId" resultType="Order">SELECT * FROM orders WHERE user_id = #{userId}
</select>
📊 性能对比
方案 | 查询次数 | 性能 | 适用场景 |
---|---|---|---|
原始N+1 | N+1 | 极差 | 绝对避免 |
连接查询 | 1 | 优 | 关联数据不多时 |
批量查询 | 2 | 良 | 关联数据较多时 |
嵌套查询 | N+1 | 差 | 小数据量简单场景 |
🔄 二、映射异常与懒加载问题
💡 常见映射错误
- 字段不匹配异常
错误信息:
Cause: org.apache.ibatis.executor.result.ResultMapException:
No constructor found in com.example.User matching [java.lang.Long, java.lang.String]
原因分析:数据库返回的字段与Java实体类不匹配
解决方案:
<!-- 明确指定字段映射 -->
<resultMap id="userResultMap" type="User"><id property="id" column="user_id"/><result property="name" column="user_name"/><result property="email" column="user_email"/><!-- 明确所有字段映射 -->
</resultMap><select id="selectUser" resultMap="userResultMap">SELECT user_id, user_name, user_email FROM users WHERE id = #{id}
</select>
- 空指针异常(NPE)
错误场景:
User user = userMapper.selectById(1);
System.out.println(user.getProfile().getAddress()); // NPE!
解决方案:
<!-- 配置空值检查 -->
<settings><setting name="callSettersOnNulls" value="true"/>
</settings>
// 实体类中添加空值检查
@Data
public class User {private Long id;private String name;private Profile profile = new Profile(); // 默认对象// 或者使用安全访问方法public Address getSafeAddress() {return profile != null ? profile.getAddress() : null;}
}
- 懒加载异常
错误信息:
org.apache.ibatis.executor.loader.LazyLoaderException:
Could not lazy load 'orders' - no session found
解决方案:
<!-- 正确配置懒加载 -->
<settings><setting name="lazyLoadingEnabled" value="true"/><setting name="aggressiveLazyLoading" value="false"/><setting name="lazyLoadTriggerMethods" value=""/>
</settings>
// 确保在Session关闭前访问懒加载属性
try (SqlSession session = sqlSessionFactory.openSession()) {UserMapper mapper = session.getMapper(UserMapper.class);User user = mapper.selectUserWithLazyOrders(1);// 在session关闭前访问懒加载属性List<Order> orders = user.getOrders();session.close(); // 现在可以安全关闭
}
🛡️ 映射最佳实践
- 始终使用:避免依赖自动映射
- 配置默认值:实体类字段提供默认值
- 使用包装类型:优先使用Integer而不是int
- 懒加载谨慎使用:确保在Session生命周期内访问
⚙️ 三、配置陷阱与解决方案
💡 常见配置问题
- Mapper 路径配置错误
错误信息:
org.apache.ibatis.binding.BindingException:
Invalid bound statement (not found): com.example.UserMapper.selectById
解决方案:
<!-- mybatis-config.xml -->
<mappers><!-- 明确指定Mapper路径 --><mapper resource="com/example/mapper/UserMapper.xml"/><mapper class="com.example.mapper.OrderMapper"/><!-- 或者使用包扫描 --><package name="com.example.mapper"/>
</mappers>
# Spring Boot配置
mybatis:mapper-locations: classpath*:mapper/**/*.xmltype-aliases-package: com.example.entity
- 日志配置问题
问题:看不到SQL日志输出
解决方案:
# application.properties
# 正确配置日志级别
logging.level.com.example.mapper=DEBUG
logging.level.org.apache.ibatis=TRACE# 或者使用Log4j2配置
log4j.logger.com.example.mapper=DEBUG
- 缓存配置错误
问题:缓存不生效或脏数据
解决方案:
<!-- 明确配置缓存 -->
<cacheeviction="LRU"flushInterval="60000"size="512"readOnly="true"/><!-- 在需要刷新的操作上配置 -->
<update id="updateUser" flushCache="true">UPDATE users SET name = #{name} WHERE id = #{id}
</update>
📝 完整配置示例
<!-- mybatis-config.xml -->
<configuration><settings><!-- 缓存配置 --><setting name="cacheEnabled" value="true"/><!-- 懒加载配置 --><setting name="lazyLoadingEnabled" value="true"/><setting name="aggressiveLazyLoading" value="false"/><!-- 数据库字段下划线转驼峰 --><setting name="mapUnderscoreToCamelCase" value="true"/><!-- 空值处理 --><setting name="callSettersOnNulls" value="true"/></settings><typeAliases><package name="com.example.entity"/></typeAliases><mappers><package name="com.example.mapper"/></mappers>
</configuration>
🛠️ 四、调试技巧与最佳实践
💡 高效调试技巧
- SQL日志调试
# 开启完整SQL日志
logging:level:com.example.mapper: DEBUGorg.apache.ibatis: TRACEjava.sql.Connection: DEBUGjava.sql.Statement: DEBUGjava.sql.PreparedStatement: DEBUG
- MyBatis内置调试
// 获取实际执行的SQL
String getMappedSql(SqlSessionFactory factory, String statementId, Object parameter) {Configuration configuration = factory.getConfiguration();MappedStatement mappedStatement = configuration.getMappedStatement(statementId);BoundSql boundSql = mappedStatement.getBoundSql(parameter);return boundSql.getSql();
}
- 使用P6Spy监控SQL
# 使用P6Spy数据源
spring:datasource:url: jdbc:p6spy:mysql://localhost:3306/testdriver-class-name: com.p6spy.engine.spy.P6SpyDriver# p6spy.properties
modulelist=com.p6spy.engine.logging.P6LogFactory
logMessageFormat=com.p6spy.engine.spy.appender.SingleLineFormat
🎯 调试检查清单
- ✅ 检查SQL日志是否开启
- ✅ 验证Mapper文件路径是否正确
- ✅ 确认字段映射是否匹配
- ✅ 检查事务边界和Session生命周期
- ✅验证缓存配置是否正确
📊 常见错误速查表
错误现象 | 可能原因 | 解决方案 |
---|---|---|
BindingException | Mapper未找到 | 检查mapper-locations配置 |
NPE | 字段为空 | 配置callSettersOnNulls |
LazyLoadingException | Session已关闭 | 确保在Session内访问懒加载属性 |
ResultMapException | 字段不匹配 | 使用明确的resultMap |
慢查询 | N+1查询 | 使用连接查询或批量查询 |
💡 五、总结与预防策略
📚 核心建议
- 预防优于治疗:建立规范的开发流程
- 测试覆盖:编写全面的单元测试和集成测试
- 代码审查:重点关注SQL性能和映射配置
- 监控告警:生产环境监控慢查询和异常
🛡️ 预防策略
🔧 必备工具推荐
- IDEA MyBatis插件:Mapper接口和XML跳转
- P6Spy:SQL监控和格式化
- Arthas:线上诊断工具
- MyBatis Code Helper:代码生成和检查
🚀 持续改进建议
- 定期SQL审查:检查所有SQL语句的性能
- 统一异常处理:建立标准的错误处理机制
- 性能监控:监控生产环境的SQL执行情况
- 知识分享:定期团队内部分享经验和教训