从 JDBC 到 MyBatis:一个后端工程师的成长之路
我是一个工作了五年的后端 Java 工程师,这几年一直在做系统后台的开发工作,参与过多个中大型项目的架构和实现。回想刚开始入行时,写数据库交互的部分总是让人头疼——手写 SQL、管理连接池、处理结果集,不仅效率低还容易出错。
直到后来接触到 MyBatis 这个框架,才真正体会到了持久层框架的魅力。它不像 Hibernate 那样“封装太重”,也不像纯 JDBC 那么“原始”。它的设计很贴近我们日常工作的实际需求,灵活性和性能之间找到了一个不错的平衡点。
所以今天我想结合自己的真实项目经验,来聊聊我对 MyBatis 的理解和使用心得,特别是对于刚入门的同学来说,如何快速上手并避免踩坑。
一、初识 MyBatis:一次真实项目的痛楚

大概三年前,我在一家做供应链系统的公司负责订单模块的开发。项目本身不大,但我们需要频繁与数据库打交道,尤其是订单状态变更、库存调整这类业务逻辑。
当时我们用的是最原始的 JDBC + DAO 模式,代码大概是这样的:
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
ps = conn.prepareStatement("SELECT * FROM orders WHERE user_id = ?");
ps.setInt(1, userId);
rs = ps.executeQuery();
while (rs.next()) {
Order order = new Order();
order.setId(rs.getLong("id"));
order.setUserId(rs.getInt("user_id"));
// ...一堆 set 方法
}
} catch (SQLException e) {
// 异常处理...
} finally {
// 关闭资源...
}
这段代码看起来没什么问题,但一旦业务复杂起来,你会发现到处都是类似的样板代码,而且很容易漏掉某些字段赋值,或者忘记关闭资源导致连接泄漏。再加上不同人写的 SQL 风格不统一,维护起来特别痛苦。
问题总结:
- 样板代码太多
- 容易出现资源泄露
- SQL 和 Java 对象之间的映射关系手工处理
- 可读性差、不易维护
二、为什么选择 MyBatis?

在那次项目上线后,我们团队决定对数据库访问层进行重构。我当时也是第一次正式接触 MyBatis,开始研究怎么用它替代原有的 JDBC 写法。
MyBatis 最吸引我的几个特点是:
1. 灵活的 SQL 控制
你可以完全掌握 SQL 的编写权,MyBatis 只负责帮你自动映射参数和结果。这相比 Hibernate 的自动 SQL 生成机制,更适合我们的场景(经常需要优化查询性能)。
2. 轻量级、易集成
不需要复杂的配置,直接通过 XML 或注解定义 SQL 映射即可。适合中小型项目快速搭建。
3. 支持动态 SQL
比如 <if>、<foreach> 这些标签,能帮助你写出结构清晰又功能强大的条件查询语句。
4. 天然支持分页、缓存等高级特性(需配合插件)
MyBatis 提供了良好的扩展接口,后续我们可以加入 PageHelper 做分页,或是二级缓存提升性能。
三、MyBatis 实战演练:从零搭建订单 DAO 层

下面我们就以订单模块为例,一步步看看如何用 MyBatis 来构建数据库访问层。
1. 依赖引入(Spring Boot 项目)
如果你用的是 Spring Boot,只需要加上如下依赖即可:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
2. 数据库表结构
为了简单起见,假设我们有如下订单表 orders:
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
product_code VARCHAR(50),
amount DECIMAL(10,2),
status TINYINT DEFAULT 0,
create_time DATETIME
);
对应的实体类 Order.java:
public class Order {
private Long id;
private Integer userId;
private String productCode;
private BigDecimal amount;
private Byte status;
private LocalDateTime createTime;
// getter / setter ...
}
3. Mapper 接口
创建一个 OrderMapper 接口:
@Mapper
public interface OrderMapper {
List<Order> selectByUserId(@Param("userId") Integer userId);
}
4. Mapper XML 文件(SQL 映射)
新建文件 resources/mapper/OrderMapper.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.OrderMapper">
<select id="selectByUserId" resultType="com.example.entity.Order">
SELECT *
FROM orders
WHERE user_id = #{userId}
</select>
</mapper>
5. 启动类添加扫描路径(非 Spring Boot 可跳过)
@SpringBootApplication
@MapperScan("com.example.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
现在,我们就可以直接注入 OrderMapper 并使用了:
@Autowired
private OrderMapper orderMapper;
public List<Order> getOrders(Integer userId) {
return orderMapper.selectByUserId(userId);
}
是不是比以前清爽太多了?更关键的是,MyBatis 自动帮我们完成了数据库记录到 Java 对象的映射,大大减少了出错率。
四、挑战升级:动态 SQL 实战

有一次我们需要根据用户输入的多个筛选条件来查订单,包括用户 ID、商品编码、订单状态、时间段等。
这时候就需要用到 MyBatis 的动态 SQL 了。看下最终的写法:
<select id="searchOrders" parameterType="map" resultType="com.example.entity.Order">
SELECT *
FROM orders
<where>
<if test="userId != null">
AND user_id = #{userId}
</if>
<if test="productCode != null and productCode != ''">
AND product_code LIKE CONCAT('%', #{productCode}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
<if test="startTime != null">
AND create_time >= #{startTime}
</if>
<if test="endTime != null">
AND create_time <= #{endTime}
</if>
</where>
</select>
这个 <where> 标签非常贴心,会自动忽略开头的 “AND” 或 “OR”,还能动态拼接条件。
这样写出来的 SQL,不仅逻辑清晰,而且非常易于后期维护和拓展。
五、常见问题和经验分享
在使用 MyBatis 的过程中,我也踩过不少坑,这里总结几点建议,送给正在学习或已经使用的小伙伴。
1. 使用 resultMap 显式映射字段,避免歧义
虽然可以直接用 resultType="Order",但如果表字段名和 Java 属性不一致,推荐显式定义 resultMap,比如:
<resultMap id="orderResultMap" type="Order">
<id property="id" column="id"/>
<result property="userId" column="user_id"/>
<result property="productCode" column="product_code"/>
<!-- 其他字段 -->
</resultMap>
<select id="selectAll" resultMap="orderResultMap">
SELECT * FROM orders
</select>
2. 使用日志插件查看执行的 SQL
调试阶段一定要打开 SQL 日志,方便排查问题。可以在配置文件中启用:
logging:
level:
com.example.mapper: debug
你会在日志中看到类似如下的输出:
==> Preparing: SELECT * FROM orders WHERE user_id = ?
==> Parameters: 123(Integer)
3. 小心空集合的 foreach 问题
当你传入一个空列表给 <foreach>,MyBatis 默认不会报错,但可能让你的 SQL 语法出错。例如:
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
如果 ids 是空的,那么最后的 SQL 就变成 ( ),很可能引起语法错误。建议加一层判断:
<if test="ids != null and ids.size() > 0">
AND id IN
<foreach ... />
</if>
六、MyBatis 的进阶玩法
随着项目越做越大,我们会逐渐接触到一些更高级的功能:
1. 一级缓存 & 二级缓存
MyBatis 自带的一级缓存默认是开启的(基于 SqlSession)。二级缓存可以通过配置实现跨 SqlSession 的数据缓存,适用于读多写少的场景。
2. 分页插件 PageHelper
在后台管理系统中,分页几乎是标配。PageHelper 插件可以非常方便地实现分页逻辑,只需一句:
PageHelper.startPage(pageNum, pageSize);
List<Order> list = orderMapper.selectAll();
PageInfo<Order> pageInfo = new PageInfo<>(list);
它会在你执行查询前自动修改 SQL 加上 limit 子句。
3. 批处理操作(Batch Insert/Update)
对于需要大批量插入或更新数据的情况,可以使用 SqlSession.batchInsert() 或自定义批量 SQL,提升数据库吞吐量。
七、生产环境的经验教训
不要把业务逻辑放在 XML 中
虽然 MyBatis 支持<script>标签嵌入复杂逻辑,但这会让 SQL 更难阅读和测试。保持 SQL 的简洁性,尽量只用简单的条件拼接。合理使用主键策略
比如在 MySQL 中我们可以设置useGeneratedKeys和keyProperty自动获取主键,而不是手动查询 max(id)。避免 N+1 查询问题
当有多表关联查询时,注意使用<association>和<collection>做延迟加载或者联表查询优化。SQL 注入防范
所有查询必须使用#{}占位符,禁止拼接字符串。如果有模糊查询需求,也应使用CONCAT('%', #{xxx}, '%')而不是在代码里拼接%value%。
八、写在最后
MyBatis 不是最先进的 ORM 框架,但它足够轻量、灵活,也非常适合国内大多数后端团队的实际需求。作为一线开发者,我真心觉得它是值得花时间深入掌握的技术。
希望这篇文章能帮你快速理解 MyBatis 的基本用法,并避免常见的坑。后续我还会继续分享更多关于 MyBatis 的实战技巧,比如多数据源、性能调优、MyBatis Plus 的使用等,欢迎关注。
最后送大家一句话:
“好的工具应该让你专注业务,而不是被底层细节绊倒。”
而 MyBatis 正是那个好帮手。

评论 0