MyBatis基础教程:Java持久层框架入门
从JDBC到MyBatis:一次真实项目中持久层的蜕变之路

开篇:为什么会讲这个话题?
作为一个做了将近十年 Java 后端开发的技术负责人,我经历过最“原始”的时代:所有的数据库操作都靠手写 JDBC,SQL 和业务代码混杂在一起,维护成本高、错误频发。直到有一天,我们在一个中型电商平台重构项目中遇到了数据库访问瓶颈,才真正下定决心去优化我们的持久层设计。
也正是那个时候,我们开始系统性地引入 MyBatis。这篇文章我会以第一人称的方式,结合我们实际项目的演进过程,带你走一遍我们当时从零开始学习和应用 MyBatis 的全过程,顺便也会穿插一些踩过的坑和解决方法,希望你读完之后对 MyBatis 的理解和使用能有一个更实在的认知。
项目背景:传统JDBC带来的痛苦回忆
2021年,我们接到一个电商平台重构项目。老系统是用纯 JDBC 实现的数据访问层,随着用户量和订单量增长,性能问题逐渐暴露出来:
- 数据库操作代码重复度极高
- SQL 和业务逻辑耦合严重,改个字段要动多个地方
- 每次新增表都要重新写 CRUD 模板代码
- 多表关联查询时结果集处理复杂且容易出错
举个例子,当时一个商品详情页需要连查 product, sku, category, stock 四张表,光是结果映射那块代码就有两百多行,稍有不慎就 NullPointerException。
这种情况下,我们就得选一个新的持久层框架。那时候主流的选择有 Hibernate、MyBatis,还有部分团队在试 JPA + Spring Data。
问题描述:到底该选哪种持久层技术?
我们当时调研了几个主流选项:
| 技术 | 优点 | 缺点 |
|---|---|---|
| Hibernate | 全自动 ORM,几乎不用写 SQL | 性能难控制,动态查询拼装困难,学习曲线陡峭 |
| JPA(Spring Data) | 易用性强,与 Spring 生态无缝整合 | 对复杂查询支持差,性能调优难度大 |
| MyBatis | 灵活性高,手动控制 SQL,性能可控 | 需要自己写 SQL,不如 Hibernate 方便 |
我们做了一个小型 Demo,分别用 Hibernate、JPA 和 MyBatis 实现同一个商品模块的查询逻辑,最终决定选择 MyBatis,主要是出于以下几点考虑:
- 我们团队已经积累了不少 SQL 经验,更愿意保留对 SQL 的掌控;
- 查询需求复杂,经常涉及多表聚合、分页、排序等,Hibernate 的 Criteria API 不够灵活;
- 对性能要求较高,特别是在首页推荐和商品搜索这类高频接口上;
- 已有的数据结构比较复杂,ORM 自动映射风险太高,容易产生 N+1 查询问题。
解决方案:MyBatis 是怎么被“请”进来的
我们选择了 MyBatis + MyBatis-Spring-Boot-Starter 的组合,整个接入过程其实挺顺利的,但也确实经历了一些调整期的小波折。整个过程中,有几个关键点我觉得值得分享:
✅ 第一步:基础依赖引入
我们在 Spring Boot 项目中直接添加了依赖:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
✅ 第二步:配置数据源和 MyBatis
application.yml 中我们这样配置:
spring:
datasource:
url: jdbc:mysql://localhost:3306/ecommerce?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.ecommerce.model
这一步完成后,我们就可以通过注解或 XML 来编写 SQL 映射了。
代码实践:看一个真实的例子
下面是一个简单的商品信息查询示例:
📁 Model 类定义
public class Product {
private Long id;
private String name;
private BigDecimal price;
private Integer stock;
// getter/setter...
}
📁 Mapper 接口定义
@Mapper
public interface ProductMapper {
Product selectById(Long id);
List<Product> selectByCategoryId(Long categoryId);
int insert(Product product);
}
📁 XML 映射文件(ProductMapper.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.ecommerce.mapper.ProductMapper">
<select id="selectById" resultType="Product">
SELECT * FROM product WHERE id = #{id}
</select>
<select id="selectByCategoryId" resultType="Product">
SELECT *
FROM product
WHERE category_id = #{categoryId}
ORDER BY create_time DESC
</select>
<insert id="insert">
INSERT INTO product (name, price, stock)
VALUES (#{name}, #{price}, #{stock})
</insert>
</mapper>
📁 Service 层调用示例
@Service
public class ProductService {
@Autowired
private ProductMapper productMapper;
public Product getProductById(Long id) {
return productMapper.selectById(id);
}
public List<Product> getProductsByCategory(Long categoryId) {
return productMapper.selectByCategoryId(categoryId);
}
public void addProduct(Product product) {
productMapper.insert(product);
}
}
是不是感觉特别清爽?相比之前一堆 ResultSet 处理代码,现在只需要定义接口和 SQL 就搞定了!
踩坑经验:这些雷我们替你踩过了
❌ 雷区一:XML文件路径配错了,半天没发现
刚开始的时候,总是提示 Mapped Statements collection does not contain value for xxx,后来才发现是 mapper-locations 配置不对。如果你用的是 Maven,默认资源放在 src/main/resources/mapper/ 目录下,确保打包时会被正确复制进去。
解决方案:检查路径是否匹配,必要时加上通配符,比如 classpath*:mapper/**/*.xml
❌ 雷区二:参数绑定不清晰导致空指针
比如有时候写了这样的 SQL:
SELECT * FROM product WHERE category_id = #{categoryID}
但接口里方法参数名却是 Long categoryId,注意大小写问题!
解决方案:要么统一命名风格(建议用驼峰),或者显式指定参数名:
List<Product> selectByCategoryId(@Param("categoryID") Long categoryId);
❌ 雷区三:忘记 <resultMap>,结果映射混乱
一开始我们有个字段叫 productName,但数据库是 product_name,没有做别名也没写 resultMap,结果一直为 null。
解决方案:要么加别名,要么显式写 <resultMap>:
<resultMap id="productResultMap" type="Product">
<id column="id" property="id"/>
<result column="product_name" property="name"/>
<result column="product_price" property="price"/>
...
</resultMap>
效果总结:换用 MyBatis 后的变化
自从全面采用 MyBatis 后,我们取得了几个明显的成效:
- 代码整洁度提升明显:SQL 和业务逻辑分离,便于维护。
- 性能优化空间更大:可以针对性优化慢查询,甚至结合缓存机制。
- 排查问题更方便:日志中可以直接看到执行的 SQL,定位问题效率高。
- 新人上手更快:新同事只需要了解 SQL 语句和基本映射规则就能干活。
特别是针对电商常见的 列表分页查询、商品推荐、库存更新 这些场景,用 MyBatis 写起来非常灵活,也避免了 Hibernate 那种生成一大堆无用 SQL 的尴尬局面。
经验分享:写给正在入门 MyBatis 的你
作为一名过来人,我想给你几点建议:
- 不要害怕写 SQL:有些人觉得 ORM 更高级,其实在企业级项目中,掌握 SQL 才是最根本的能力。
- 善用动态 SQL 标签:像
<if>、<choose>、<set>这些标签在构建复杂的条件查询时简直是神器。 - 适当抽象通用方法:我们团队封装了一个
BaseMapper<T>,包含常用的增删改查,节省了很多样板代码。 - 结合 Druid 或 MyBatis-Plus 增强功能:
- 如果你的项目规模不大,可以试试 MyBatis-Plus,它提供了很多便捷的扩展方法;
- 想监控 SQL 性能?用上 Druid,实时查看慢 SQL。
- 关注连接池配置:别以为引入 MyBatis 就万事大吉了,连接池设置不合理一样会拖累系统性能。我们用了 HikariCP,默认最大连接数是 10,后来改成 30,效果立竿见影。
结语:坚持才是硬道理
回想当初在 MyBatis 上摸索的过程,其实是挺艰难的 —— 要学各种标签、要注意 SQL 注入问题、还要时刻提防事务管理的坑。但正是那一段时间的坚持,让我和团队真正掌握了如何写出高性能、易维护的数据访问层代码。
如今再回头看,我很庆幸当初选择了 MyBatis,而不是盲目追求全自动的 ORM。毕竟,在真实的企业开发中,灵活比自动化更重要。希望这篇文章能帮助你少走一点弯路,早点掌握 MyBatis 的精髓。
如果你也在用 MyBatis,欢迎留言交流实战中的问题,我们一起进步!

评论 0