从写死SQL到优雅ORM:我在项目中踩坑MyBatis的成长之路
开篇:一次深夜调试引发的思考

去年年底,我在公司接手了一个用户管理系统重构的任务。这个系统原本是个典型的“老项目”——代码混乱、SQL嵌套在Java逻辑里,维护成本极高。刚接手的时候,我花了整整三天才看懂主流程,光是找一个登录接口的SQL就翻了四个DAO类。那天晚上,我对着满屏的PreparedStatement和ResultSet,突然意识到:如果不用持久层框架,维护这种项目简直是自虐。
于是我决定引入 MyBatis 来重构整个数据访问层。这篇文章就是想以我的真实经验为主线,聊聊我在使用 MyBatis 过程中的踩坑与收获。
一、问题描述:传统JDBC编程的痛楚

1.1 原始系统的痛点
我们原来的数据访问层几乎是裸写 JDBC:
public User getUserById(int id) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
ps = conn.prepareStatement("SELECT * FROM user WHERE id = ?");
ps.setInt(1, id);
rs = ps.executeQuery();
if (rs.next()) {
User user = new User();
user.setId(rs.getInt("id"));
user.setName(rs.getString("name"));
// ...还有十几行字段赋值
return user;
}
} catch (Exception e) {
// ...
} finally {
// close资源...
}
return null;
}
这样的代码有几个显著的问题:
- 代码重复严重:每次操作都要写 try-catch、close 资源
- 可读性差:SQL 和 Java 逻辑混在一起,难以一眼看出业务意图
- 性能隐患:连接池没有统一管理,容易导致连接泄漏或阻塞
- 维护困难:换数据库或加字段要改多个地方,容易遗漏
更可怕的是,有些查询语句是拼接字符串构建的,根本没法动态调试,还存在 SQL 注入风险。
二、解决方案:用MyBatis让代码优雅起来

2.1 初识MyBatis
MyBatis 不是一个完全的 ORM 框架(如 Hibernate),而是一个灵活的 SQL 映射工具。它允许开发者写原生 SQL,但通过 XML 或注解将结果自动映射为 Java 对象,既能掌控 SQL 性能,又能减少样板代码。
我选择它的原因很简单:我们在做高性能查询优化,不希望被 Hibernate 那种全自动化机制束缚;但我们也不能再忍受手写 JDBC 的痛苦。
2.2 我的第一个MyBatis实战
先来看看重构后的 UserMapper.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserMapper">
<select id="getUserById" resultType="com.example.model.User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>
然后定义对应的接口:
package com.example.mapper;
import com.example.model.User;
public interface UserMapper {
User getUserById(int id);
}
调用的时候也很简单:
User user = sqlSession.getMapper(UserMapper.class).getUserById(1);
看起来是不是简洁多了?但这只是开始。
2.3 复杂查询场景下的MyBatis技巧
真正的挑战来自于一些复杂的查询场景,比如:
场景一:条件筛选 + 分页 + 排序
原始实现用了各种字符串拼接,极其难读且容易出错。MyBatis 提供了 <if>、<choose> 等标签帮我们优雅地处理这些情况。
<select id="searchUsers" parameterType="map" resultType="com.example.model.User">
SELECT * FROM user
<where>
<if test="name != null and name.trim() != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="email != null and email.trim() != ''">
AND email LIKE CONCAT('%', #{email}, '%')
</if>
</where>
ORDER BY ${sortField} ${sortOrder}
LIMIT #{offset}, #{pageSize}
</select>
注意这里的两个细节:
- 使用
<where>标签自动处理多余的AND或WHERE - 使用
${}替代#{}来支持动态排序字段和顺序(适用于可信任输入)
⚠️ 安全提醒:像
ORDER BY ${sortField}这种写法虽然灵活,但可能存在注入风险,建议在 Service 层做一些白名单校验。
场景二:多表关联查询
之前手动封装时常常搞不定嵌套对象的转换,现在我们可以轻松搞定:
<resultMap id="userWithRoleMap" type="com.example.model.User">
<id column="id" property="id"/>
<result column="name" property="name"/>
<association property="role" javaType="com.example.model.Role">
<id column="roleId" property="id"/>
<result column="roleName" property="name"/>
</association>
</resultMap>
<select id="findUserWithRole" resultMap="userWithRoleMap">
SELECT u.id, u.name, r.id AS roleId, r.name AS roleName
FROM user u
LEFT JOIN role r ON u.role_id = r.id
WHERE u.id = #{id}
</select>
这样就能直接返回一个带有 Role 对象属性的 User 实例了,大大简化了代码结构。
三、落地实施效果

经过一个月左右的逐步迁移,整个 DAO 层代码量减少了约 40%,错误率明显下降。我们几个开发小伙伴都反馈说:
- 写 SQL 更清晰了
- 查 bug 更快了
- 调试也方便了(可以直接看 XML 文件里的 SQL)
最明显的一个变化是:上线前的测试效率提高了不止一倍,因为很多查询逻辑变得更直观。
我们还发现了一些性能上的收益,尤其是在做了缓存配置后:
3.1 一级缓存 & 二级缓存
MyBatis 默认开启了一级缓存(Session 级),如果你在一个请求生命周期内多次执行同样的查询,可以节省不少数据库访问开销。
我们还开启了基于 Redis 的二级缓存,用于缓存高频访问的配置信息,命中率达到了 75% 以上。
3.2 生产环境的稳定性提升
MyBatis 的日志追踪也非常友好,结合 Logback 输出 SQL 日志,在排查慢查询时帮助极大。
四、经验分享:给初学者的一些建议

4.1 建议1:别怕写XML文件
很多人一开始抗拒 XML 写 SQL,其实这是个误解。合理划分 mapper 文件,按模块组织命名空间,反而会让 SQL 结构更清晰,便于团队协作。
举个例子,我们按模块拆分如下:
src/main/resources/mappers/
├── user/
│ └── UserMapper.xml
├── order/
│ └── OrderMapper.xml
└── config/
└── ConfigMapper.xml
4.2 建议2:合理使用注解和XML
对于简单的 CRUD,可以用 @Select 注解来写,省去 XML 文件。但对于复杂逻辑,还是推荐 XML:
@Select("SELECT * FROM user WHERE id = #{id}")
User selectById(int id);
这样混合使用可以让项目保持灵活性。
4.3 建议3:学会调试SQL输出
开发阶段一定要开启 SQL 日志,Spring Boot 中只需在配置文件里加一句:
logging:
level:
com.example.mapper: debug
这能在控制台看到完整的 SQL 和参数绑定情况,非常有用。
4.4 建议4:了解事务管理机制
MyBatis 并不是自动提交事务的!这点特别重要,尤其在涉及多个操作时,必须手动控制:
SqlSession session = sqlSessionFactory.openSession();
try {
UserMapper mapper = session.getMapper(UserMapper.class);
mapper.updateUser(user);
mapper.updateProfile(profile);
session.commit(); // 手动提交
} catch (Exception e) {
session.rollback(); // 出错回滚
// ...
}
或者配合 Spring 使用声明式事务,更加简洁安全。
4.5 建议5:善用插件,不要自己造轮子
MyBatis 社区有一些非常好用的插件,比如:
- PageHelper:分页神器
- MyBatis Plus:增强功能库,内置CRUD生成器
它们帮你解决了很多通用问题,节省大量时间。
五、写在最后:关于MyBatis的一些小感悟

回顾这次重构,最大的感受就是:技术选型真的要因地制宜。MyBatis 并不完美,也不是银弹,但它确实适合我们的项目现状,既不会限制我们对 SQL 的掌控,又大幅提升了开发效率。
如果你还在纠结是否用 ORM,我想说一句掏心窝的话:不要为了 ORM 而用 ORM,而是为了解决实际问题。
当然了,我也在慢慢学习更高级的框架如 Hibernate 和 JPA,但我始终相信一句话:
“掌握底层原理的人,才能真正驾驭上层抽象。”
希望这篇真实的 MyBatis 入门实践能够给你一点启发。如果你也有类似的经历,欢迎留言交流!
📅 本文成稿于 2025 年 4 月 5 日凌晨,咖啡喝了一整壶 ☕
💻 作者:一位曾靠写 SQL 维生的后端码农

评论 0