MyBatis基础教程:从“手写JDBC”到“框架初体验”的真实成长路径
大家好,我是后端开发工程师小李。今天想和大家分享一下我在刚入行那会儿是如何一步一步从“手写JDBC代码”迈向使用MyBatis的那段经历。
其实我第一次真正接触到MyBatis是在一个内部数据平台项目中。我们团队当时需要对数据库操作进行重构,原本是每个DAO层方法都自己打开连接、执行SQL、关闭资源,结果随着表结构越来越多,代码也变得越来越难维护,甚至出现了很多重复代码。那时候我才意识到:如果不能把精力集中在业务逻辑上,而是天天在拼字符串和处理各种JDBC异常,那这个项目迟早要崩溃。
项目背景与痛点分析

当时的项目是一个用于内部数据分析的平台,前端有报表展示,后端涉及十几个核心业务实体,比如用户信息、点击事件日志、商品配置等。每个业务模块都有各自的DAO类,大量重复地写着如下类似的代码:
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
String sql = "SELECT * FROM user WHERE id = ?";
ps = conn.prepareStatement(sql);
ps.setLong(1, userId);
rs = ps.executeQuery();
User user = null;
if (rs.next()) {
user = new User();
// 手动映射字段...
}
return user;
} catch (Exception e) {
// 异常处理...
}
每次加个字段、改个查询条件都要手动调整这些代码。更头疼的是,不同的程序员编码习惯不同,导致有些代码风格混乱、错误处理也不一致。
为什么选择MyBatis?


在那次技术评审会上,leader提出两个选项:一个是迁移到Spring Data JPA,另一个是引入MyBatis。考虑到几个因素,我们最终选择了后者:
- 灵活性要求高:部分查询需要用到复杂的自定义SQL(尤其是多表联合分页);
- 性能敏感场景多:数据量大时对SQL优化有较强诉求;
- 历史包袱较重:已有不少现成的SQL语句可以直接复用,不想重新写DSL或者HQL。
于是我的第一个任务就是:搭建MyBatis的基础骨架,迁移现有的UserDAO模块。
初识MyBatis:从“懵圈”到“真香”

第一步:引入依赖 & 配置数据源
我们采用的是Spring Boot + MyBatis的组合方式,直接通过Maven引入starter包:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
然后配置application.yml:
spring:
datasource:
url: jdbc:mysql://localhost:3306/data_platform?useSSL=false&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/**/*.xml
type-aliases-package: com.example.data.model
这一步非常顺利,不过刚开始我还是踩了不少坑。比如忘记在Mapper接口上加@Mapper注解,又或者XML里写错了namespace导致找不到映射。
第二步:实现第一个MyBatis Mapper接口
原来的UserDao类被改造成了一个接口+XML的方式。
UserMapper.java:
@Mapper
public interface UserMapper {
User selectById(Long id);
}
对应的UserMapper.xml:
<mapper namespace="com.example.data.mapper.UserMapper">
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>
Service层就可以直接注入UserMapper来使用了:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User getUserById(Long id) {
return userMapper.selectById(id);
}
}
是不是清爽了很多?!
不过这只是冰山一角。后面还有更多挑战。
遇到的挑战与解决方案详解

挑战一:复杂查询中的字段映射
有一天产品经理说:“能不能给用户加一个统计他的最近一次登录时间?”我们之前都是单表操作,现在突然要join多个表,字段开始变多了。
问题:
新字段last_login_time不在user表,而在login_log表中。怎么办呢?
我的做法:
- 在XML中编写带JOIN的SQL;
- 使用
resultMap显式映射字段;
修改后的XML如下:
<resultMap id="UserWithLoginResultMap" type="User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<!-- 新增字段 -->
<result column="last_login_time" property="lastLoginTime"/>
</resultMap>
<select id="selectUserWithLastLoginTime" resultMap="UserWithLoginResultMap">
SELECT u.id, u.username, l.last_login_time
FROM user u
LEFT JOIN (
SELECT user_id, MAX(login_time) AS last_login_time
FROM login_log GROUP BY user_id
) l ON u.id = l.user_id
WHERE u.id = #{id}
</select>
这样即使字段名不一致也可以映射清楚,再也不用手动set了。
挑战二:批量插入时性能不佳
某个需求是支持批量导入用户数据。我一开始用了简单的for循环逐条insert:
for (User user : userList) {
userMapper.insert(user);
}
但在测试环境跑的时候发现插入1万条数据竟然要几十秒,完全不可接受。
原因分析:
每条INSERT都是一次网络IO,加上事务开销,效率极低。
解决方案:
使用MyBatis的批量插入能力:
<insert id="batchInsert">
INSERT INTO user (username, email)
VALUES
<foreach collection="list" item="user" separator=",">
(#{user.username}, #{user.email})
</foreach>
</insert>
Java调用:
userMapper.batchInsert(users);
这样一来,整个插入过程只需要一次网络通信,速度大幅提升。后来我们还做了分批次提交,避免单条SQL太大导致数据库拒绝服务。
挑战三:动态SQL的拼接困境
还有一个功能是根据条件筛选用户,比如可以根据用户名、邮箱、注册时间等多个参数组合查询。之前都是硬编码拼接WHERE子句,非常容易出错。
MyBatis解决方案:
使用MyBatis的动态SQL标签,特别是<where>和<if>:
<select id="selectByCondition" parameterType="map" resultType="User">
SELECT * FROM user
<where>
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="email != null and email != ''">
AND email = #{email}
</if>
<if test="registerTimeStart != null">
AND register_time >= #{registerTimeStart}
</if>
</where>
</select>
传参方式:
Map<String, Object> params = new HashMap<>();
params.put("username", "Tom");
params.put("registerTimeStart", "2024-01-01");
List<User> users = userMapper.selectByCondition(params);
这个功能上线后极大地提高了搜索的灵活性。
小插曲:XML中占位符误写引发的事故
有一次上线之后发现有个SQL报错,说是参数找不到。经过排查才发现,在动态SQL里写了#{userName},而传进去的键却是username。这属于命名风格的问题,建议统一用驼峰格式或下划线格式。
为了避免这类问题,最好在团队中规范参数命名规则,并在代码Review阶段多检查。
使用MyBatis后的好处总结
- 开发效率提升明显:不用再关心JDBC底层细节,节省了大量的模板代码。
- 可维护性更强:SQL集中管理,便于排查、优化和后期审计。
- 易于扩展:动态SQL让复杂查询更灵活,适合变化频繁的业务。
- 性能可控性强:不像Hibernate那样隐藏太多细节,可以自由优化SQL语句。
特别是在生产环境中,我们通过MyBatis的日志输出插件结合Druid监控,能够快速定位慢查询,大大减少了线上故障响应时间。
我的经验分享与实用建议
如果你刚开始接触MyBatis,这里有几个我走过的弯路,希望你能避开:
✅ 1. 合理设计Mapper层接口
不要把所有的SQL都放在一个Mapper接口里。按照业务划分,比如UserMapper、OrderMapper、ReportMapper,清晰明了。
✅ 2. 利用MyBatis Generator生成基础代码
对于简单CRUD操作,可以使用MyBatis Generator自动生成Mapper、Model、XML文件。省去大量手工编写的痛苦。
✅ 3. SQL尽量与业务分离,方便维护
把SQL写在XML中而不是注解里,这样后续做SQL优化或审计时更容易查找和修改。
✅ 4. 学会使用缓存(二级缓存)
MyBatis支持二级缓存,适用于读多写少的场景。但我们当时考虑到数据实时性比较高,所以没启用。不过如果你的需求场景允许,记得开启它能大大减少数据库压力。
✅ 5. 注意数据库设计与接口设计的联动
我们在早期曾忽略了一点:某些表字段命名不规范,导致映射麻烦。比如有的字段叫created_at,有的叫createTime。这其实是数据库设计的问题,但会影响MyBatis的使用效率。
建议提前约定:
- 数据库字段使用下划线命名法;
- Java Bean使用驼峰命名法;
- 保持自动映射的简洁性。
✅ 6. 生产环境记得开启慢日志记录
配合日志插件(如log4j2),将耗时较长的SQL打出来,有助于后续性能调优。
结尾感悟:工具只是手段,理解才是关键

现在回想起来,虽然MyBatis只是一个ORM框架,但它背后承载的是对数据库编程思想的理解。从最原始的JDBC一步步走到框架层面,让我明白了两个字——抽象。
好的工具可以帮助我们屏蔽底层细节,但如果不懂得其背后的原理,当遇到问题时依然无从下手。所以我一直坚持一点:哪怕你使用框架也要知道它在做什么。
如今MyBatis仍然是Java生态中最流行的数据持久化框架之一。它不像JPA那么笨重,也不像原生JDBC那么繁琐。它给了开发者足够的自由,又提供了强大的封装能力。
如果你正在入门Java后端开发,希望这篇文章能帮助你少走些弯路,更快地上手实战项目。
毕竟,真正的成长,往往发生在面对实际问题的那一刻。
如果你喜欢这类真实工作经验分享,欢迎留言交流或转发文章。如果你也有类似的经历,也可以留言分享,我们一起探讨更好的解决方案。

评论 0