从“写烂了的 DAO”到优雅操作数据库:我在项目中踩出的 MyBatis 入门心得

RAG小工匠
2025-06-14 13:50
阅读 392

作为一名在互联网公司干后端开发多年的老兵,我深知一个持久层框架对系统开发效率和稳定性的影响有多大。今天想和大家聊聊我在一个实际项目中使用 MyBatis 的入门经历,以及在这个过程中遇到的一些坑和感悟。

希望通过这篇文章,能帮你在学习或使用 MyBatis 时少走点弯路,或者至少知道怎么绕过那些隐藏得很深的大坑。


背景:新需求催生技术选型

背景:新需求催生技术选型

事情要从去年我们部门接到一个新的运营平台项目说起。这个项目主要是为了帮助市场团队做活动管理、用户权益发放、数据报表展示等日常运营支持功能。

我们团队原本是 Spring Boot + Hibernate 的坚定使用者,但这次的需求里有很多查询非常复杂,涉及到多表联合、动态筛选、分页排序等等。传统的 Hibernate 对这些场景的支持略显笨重,而且有时候生成的 SQL 过于啰嗦,也不太好调试。

于是,在一次项目讨论会上,我提议尝试一下 MyBatis,毕竟之前在开源社区里听说它的灵活性更高,尤其是对于需要频繁调整 SQL 的业务场景来说,优势明显。

最终团队决定采用 Spring Boot + MyBatis 的组合,我也顺势接下了这块的技术攻坚任务。


我们遇到了哪些问题?

我们遇到了哪些问题?

刚引入 MyBatis 后,确实有一段适应期,特别是在以下几个方面:

1. 数据库字段与实体类不一致带来的映射难题

我们之前的 ORM 框架基本都是自动做映射的,但现在发现,MyBatis 默认并不会处理像 user_iduserId 这样的命名转换,导致大量手动写 ResultMap,效率很低。

public class User {
    private Long userId;
    private String userName;
}

对应的数据表字段却是:

CREATE TABLE user (
    id BIGINT PRIMARY KEY,
    name VARCHAR(50)
);

如果不配置映射,那查出来就是空值!

2. 动态查询语句拼接混乱

比如我们要根据用户名、注册时间范围等多个条件查询用户,传统做法可能是用 if-else 拼接字符串,SQL 安全性和可读性都差到爆。虽然 Hibernate 有 Criteria API,但我们在换 MyBatis 后就失去了这些便利。

我们需要一种方式,既能灵活构建查询语句,又能避免 SQL 注入风险。

3. 编码风格不统一,多人协作容易出错

由于没有现成的标准模板,不同同事写的 Mapper 文件五花八门,有的还直接把 SQL 写在 XML 里,有的则用上了注解,还有人写了原生 JDBC……后期维护成本极高。

这些问题让我意识到,光会用 MyBatis 是不够的,必须从架构设计层面去规范它,让它真正变成生产力工具,而不是负担。


解决思路和架构设计

微服务架构示意图-1

解决思路和架构设计

针对以上问题,我做了几个关键决策:

一、统一数据库命名规范 + 自动化映射

我们制定了一套新的数据库命名规范,将字段全部转为下划线风格(如 user_name、created_at),然后通过全局配置让 MyBatis 支持自动映射。

# application.yml 配置
mybatis:
  configuration:
    mapUnderscoreToCamelCase: true

这样就能实现自动映射,省去了很多重复写 ResultMap 的麻烦。

二、引入 MyBatis 动态 SQL 组件

我们开始全面使用 <if><choose><where> 等标签来构建动态 SQL 查询,避免手写拼接。举个例子:

<!-- UserMapper.xml -->
<select id="selectUsers" parameterType="map" resultType="User">
    SELECT * FROM user
    <where>
        <if test="userName != null and userName != ''">
            AND name LIKE CONCAT('%', #{userName}, '%')
        </if>
        <if test="minCreateTime != null">
            AND create_time >= #{minCreateTime}
        </if>
        <if test="maxCreateTime != null">
            AND create_time <= #{maxCreateTime}
        </if>
    </where>
</select>

这段 XML 最终会被 MyBatis 渲染成结构清晰的 SQL,避免手动拼接的风险和 bug。

三、规范化 Mapper 层设计

我们制定了如下几条编码规范:

  • 所有 SQL 必须写在 XML 文件中,禁止直接写在注解里(方便统一管理和调试)
  • XML 文件必须命名为 <Entity>Mapper.xml,放在统一资源路径 /resources/mappers
  • Mapper 接口使用标准命名规则:<Entity>Mapper.java
  • 查询接口返回类型尽可能用实体类(POJO)或 DTO,避免使用 Map
  • 复杂更新逻辑应结合事务管理器(@Transactional)

这样一来,整个项目的数据库层结构变得非常清晰,新成员上手也更容易。


核心代码片段示例

以下是我整理的几个 MyBatis 开发中的核心代码模板,供你参考。

1. 实体类定义(简洁明了)

public class Activity {
    private Long id;
    private String name;
    private LocalDateTime startTime;
    private LocalDateTime endTime;
    private Integer status;
}

2. Mapper 接口定义

@Mapper
public interface ActivityMapper {
    List<Activity> selectActivities(@Param("name") String name, 
                                    @Param("status") Integer status);

    int insert(Activity activity);
}

3. XML 中的动态查询 SQL

<select id="selectActivities" resultType="Activity">
    SELECT * FROM activity
    <where>
        <if test="name != null and name != ''">
            AND name LIKE CONCAT('%', #{name}, '%')
        </if>
        <if test="status != null">
            AND status = #{status}
        </if>
    </where>
</select>

4. 插入数据的示例

<insert id="insert">
    INSERT INTO activity (name, start_time, end_time, status)
    VALUES (
        #{name},
        #{startTime, jdbcType=TIMESTAMP},
        #{endTime, jdbcType=TIMESTAMP},
        #{status}
    )
</insert>

这里注意日期类型的字段要指定 jdbcType=TIMESTAMP,否则某些数据库可能插入失败。


我踩过的那些坑

当然啦,刚开始使用 MyBatis 时我也踩了不少坑,下面分享几个比较典型的。

坑1:参数绑定错误,提示找不到属性名

一开始我写了个这样的接口:

List<User> findUsers(String keyword, LocalDateTime minTime);

然后在 XML 里这么调用:

<if test="keyword != null">AND name LIKE '%${keyword}%'</if>

结果一直报错:Parameter 'keyword' not found.

后来才知道,当接口方法有多个参数时,需要用 @Param 注解明确指定每个参数的名称,不然 MyBatis 不知道怎么映射。

正确的写法应该是:

List<User> findUsers(@Param("keyword") String keyword, 
                     @Param("minTime") LocalDateTime minTime);

坑2:XML 文件没被加载,SQL 报错找不到方法

这个问题一般发生在集成阶段,尤其是在打包部署之后。原因是 MyBatis 的 mapper 文件没有正确配置扫描路径。

解决方案是在配置文件中加上:

mybatis:
  mapper-locations: classpath:mapper/**/*.xml

确保项目启动时能正确加载所有的 XML 映射文件。

坑3:LIKE 查询出现 SQL 注入风险

最开始我直接用了 ${} 来拼接模糊查询:

AND name LIKE '%${keyword}%'

这是很危险的做法,可能会导致 SQL 注入攻击。

改成使用预编译 #{},并配合 CONCAT

AND name LIKE CONCAT('%', #{keyword}, '%')

这样安全性高很多,也能防止恶意输入破坏查询逻辑。


实际效果与收益总结

经过几个月的实战打磨,这套基于 MyBatis 的持久层方案已经在我们项目中稳定运行,并取得了不错的效果:

指标 效果描述
查询效率 复杂 SQL 编写更加直观,优化空间更大,整体响应时间下降约 30%
团队协作 统一了编码规范后,新人上手速度快了很多,代码 Review 更加高效
可维护性 SQL 与 Java 逻辑分离,便于后续 DBA 或运维人员介入排查和优化
扩展能力 支持多种数据库方言,兼容性良好,后续可以轻松迁移至 Oracle、PostgreSQL 等

更关键的是,它让我们摆脱了过去 Hibernate 在复杂查询上的局限,同时又不像直接裸写 JDBC 那样繁琐。


几点经验建议送给正在入门 MyBatis 的你

如果你现在刚开始接触 MyBatis,或者准备在项目中使用它,这里有几点我亲测有效的小建议,希望能帮你避开一些陷阱:

✅ 1. 别一开始就搞太复杂的插件,先掌握基础用法

很多人一上来就想装 PageHelper、Dynamic-Datasource 之类的扩展,结果连基本的 XML 配置都没理清楚。建议先把 Mapper 接口 + XML 文件 + 参数映射 搞明白,再逐步深入。

✅ 2. 多用日志输出 SQL,方便定位问题

Spring Boot + MyBatis 可以通过日志级别设置,把执行的 SQL 输出出来:

logging:
  level:
    com.example.mapper: debug

这样就可以看到真实执行的 SQL 是什么样子的,调试时非常实用。

✅ 3. 尽量避免使用 ${},除非你知道自己在做什么

${} 是直接拼接 SQL 字符串,有安全隐患。而 #{} 是占位符,更安全也更适合大多数场景。

✅ 4. 使用 IDE 插件提升开发效率

推荐安装 MyBatisX 这个 IntelliJ IDEA 插件,它可以自动跳转 Mapper 接口与 XML 方法,写完 SQL 还能检查语法是否正确,非常好用。

✅ 5. 分离业务逻辑与数据访问层

MyBatis 是个持久层框架,不是业务逻辑中心。建议把具体的业务逻辑封装在 Service 层,保持 Mapper 接口职责单一,仅负责 CRUD 和简单查询。


结语:MyBatis 不是万能的,但它足够实用

回顾整个项目过程,我深刻体会到一点:选择一个适合当前业务场景的工具比盲目追求新技术更重要

MyBatis 并不像 Hibernate 那样全自动,但在我们这种需要频繁调整 SQL、重视性能优化的项目中,它展现出了独特的优势。

希望这篇文章对你理解 MyBatis 有所帮助,也能让你在使用过程中少走弯路。如果你有任何关于 MyBatis 的问题,欢迎留言交流,我会尽我所能帮忙解答。

最后送大家一句我在项目中常常用来提醒自己的话:代码是用来给人看的,偶尔给机器跑跑。别忘了写得优雅一点,别让以后的自己看不懂今天的 SQL 😄


作者:某互联网大厂后端开发一枚,常年混迹于 Java 生态圈,热爱开源、热爱分享。欢迎关注我的公众号【Java修行手记】获取更多实战干货。

评论 0

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