从写死SQL到优雅ORM:我在项目中踩坑MyBatis的成长之路

Dev开发者
2025-06-18 02:49
阅读 399

开篇:一次深夜调试引发的思考

开篇:一次深夜调试引发的思考

去年年底,我在公司接手了一个用户管理系统重构的任务。这个系统原本是个典型的“老项目”——代码混乱、SQL嵌套在Java逻辑里,维护成本极高。刚接手的时候,我花了整整三天才看懂主流程,光是找一个登录接口的SQL就翻了四个DAO类。那天晚上,我对着满屏的PreparedStatementResultSet,突然意识到:如果不用持久层框架,维护这种项目简直是自虐。

于是我决定引入 MyBatis 来重构整个数据访问层。这篇文章就是想以我的真实经验为主线,聊聊我在使用 MyBatis 过程中的踩坑与收获。


一、问题描述:传统JDBC编程的痛楚

一、问题描述:传统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让代码优雅起来

二、解决方案:用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>

注意这里的两个细节:

  1. 使用 <where> 标签自动处理多余的 ANDWHERE
  2. 使用 ${} 替代 #{} 来支持动态排序字段和顺序(适用于可信任输入)

⚠️ 安全提醒:像 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 日志,在排查慢查询时帮助极大。


四、经验分享:给初学者的一些建议

数据库设计模型-1

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 社区有一些非常好用的插件,比如:

它们帮你解决了很多通用问题,节省大量时间。


五、写在最后:关于MyBatis的一些小感悟

API接口文档-2

回顾这次重构,最大的感受就是:技术选型真的要因地制宜。MyBatis 并不完美,也不是银弹,但它确实适合我们的项目现状,既不会限制我们对 SQL 的掌控,又大幅提升了开发效率。

如果你还在纠结是否用 ORM,我想说一句掏心窝的话:不要为了 ORM 而用 ORM,而是为了解决实际问题。

当然了,我也在慢慢学习更高级的框架如 Hibernate 和 JPA,但我始终相信一句话:

“掌握底层原理的人,才能真正驾驭上层抽象。”

希望这篇真实的 MyBatis 入门实践能够给你一点启发。如果你也有类似的经历,欢迎留言交流!


📅 本文成稿于 2025 年 4 月 5 日凌晨,咖啡喝了一整壶
💻 作者:一位曾靠写 SQL 维生的后端码农

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝