MyBatis基础教程:Java持久层框架入门
早上八点,咖啡刚泡好,我坐在工位上一边刷着 GitHub Trending 一边想着:都快三年了,是不是该换个环境了?手里的 Java 项目还在用着那套老旧的 JDBC 模板代码,每次写 DAO 层都像在给祖宗上香——仪式感十足,效率却低得令人发指。
上周五晚上,产品经理又甩来一个“紧急需求”(其实只是他昨天才想起来的功能),要求三天内上线。我盯着屏幕看了五分钟,突然意识到:再不引入个像样的 ORM 框架,我怕是要在这家公司熬成化石。于是,我决定把 MyBatis 捧上神坛——不是因为它多新潮,而是它够稳、够轻、够适合我们这种“保守派”。
被逼出来的技术选型
说起来有点惭愧,作为写了三年 Java 的“老油条”,我一直坚持手写 SQL。理由很朴素:控制感。Hibernate 那种全自动映射?总觉得黑盒太多,线上一出问题连日志都看不懂。但现实狠狠打了我的脸——上个月双11压测期间,一个简单的用户查询接口因为拼接 SQL 出错,直接导致数据库连接池爆满,运维小哥半夜打电话骂我“手写代码一时爽,线上事故火葬场”。
痛定思痛,我开始认真研究 MyBatis。它不像 Hibernate 那样试图“替你思考”,而是给你一把锋利的刀——SQL 你来写,结果映射它来管。这种半自动的模式,正好契合我这种“既要自由又要安全”的矛盾心理。
而且,开源社区对它的支持简直不要太友好。GitHub 上 MyBatis 官方仓库 star 数超过 17k,issues 响应快,文档也清晰。更重要的是,我们团队里好几个同事跳槽去大厂,面经里都提到 MyBatis 是必考项。这不就是信号吗?
实战:从零搭建一个 MyBatis 项目
废话不多说,直接上手。假设我们要做一个简单的“运营后台”,管理一批推广活动(Activity)。表结构如下:
CREATE TABLE activity (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
start_time DATETIME,
end_time DATETIME,
status TINYINT DEFAULT 0 COMMENT '0-未开始,1-进行中,2-已结束'
);
第一步:Maven 引入依赖
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- 别忘了 JUnit,不然怎么证明你写的代码能跑? -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
第二步:配置 mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/ops_db?useSSL=false&serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="your_password"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/ActivityMapper.xml"/>
</mappers>
</configuration>
吐槽一下:
&这个转义符号我每次写都要查一遍,XML 真是反人类设计。不过比起写 50 行 try-catch-finally 来关 connection,这点痛苦算什么?
第三步:写实体类和 Mapper 接口
public class Activity {
private Long id;
private String name;
private LocalDateTime startTime;
private LocalDateTime endTime;
private Integer status;
// getter / setter 省略,但实际项目里我会用 Lombok
}
public interface ActivityMapper {
List<Activity> selectAll();
Activity selectById(Long id);
void insert(Activity activity);
void update(Activity activity);
}
第四步:写 XML 映射文件(重点!)
<!-- mapper/ActivityMapper.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.ActivityMapper">
<resultMap id="ActivityResultMap" type="com.example.model.Activity">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="startTime" column="start_time"/>
<result property="endTime" column="end_time"/>
<result property="status" column="status"/>
</resultMap>
<select id="selectAll" resultMap="ActivityResultMap">
SELECT * FROM activity ORDER BY id DESC
</select>
<select id="selectById" parameterType="long" resultMap="ActivityResultMap">
SELECT * FROM activity WHERE id = #{id}
</select>
<insert id="insert" parameterType="com.example.model.Activity" useGeneratedKeys="true" keyProperty="id">
INSERT INTO activity(name, start_time, end_time, status)
VALUES(#{name}, #{startTime}, #{endTime}, #{status})
</insert>
<update id="update" parameterType="com.example.model.Activity">
UPDATE activity
SET name = #{name}, start_time = #{startTime}, end_time = #{endTime}, status = #{status}
WHERE id = #{id}
</update>
</mapper>
这里有几个坑我必须提:
- 字段名和属性名不一致:数据库用下划线(
start_time),Java 用驼峰(startTime),必须用<resultMap>显式映射,否则查出来全是 null。 - 自增主键回填:
useGeneratedKeys="true"+keyProperty="id"缺一不可,不然activity.getId()拿不到值。 - 时间类型处理:MySQL 的
DATETIME对应 Java 的LocalDateTime,需要确保 JDBC 驱动版本 >= 8.0,否则会报No TypeHandler found。
生产环境踩过的雷
别以为本地跑通就万事大吉。上周我把代码推到测试环境,结果测试妹子反馈“新增活动后列表看不到”。我本地反复测都没问题,直到我登录测试数据库——发现 status 字段被设成了 NULL!
原因?我在 INSERT 语句里写了 status = #{status},但新建对象时没显式设置 status,默认是 null。而 MySQL 表定义里虽然有 DEFAULT 0,但 MyBatis 传了 null 就会覆盖默认值。
解决方案:用 <trim> 或 <if> 动态 SQL:
<insert id="insert">
INSERT INTO activity
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="name != null">name,</if>
<if test="startTime != null">start_time,</if>
<if test="endTime != null">end_time,</if>
<if test="status != null">status,</if>
</trim>
<trim prefix="VALUES (" suffix=")" suffixOverrides=",">
<if test="name != null">#{name},</if>
<if test="startTime != null">#{startTime},</if>
<if test="endTime != null">#{endTime},</if>
<if test="status != null">#{status},</if>
<if test="status == null">0,</if> <!-- 默认值 -->
</trim>
</insert>
当然,更优雅的做法是在 Java 层初始化对象时就设好默认值。但线上救火的时候,谁还顾得上优雅?
性能与运维视角
MyBatis 虽然轻量,但在高并发场景下也有讲究。我们运营后台高峰期 QPS 能到 300+,以下是几点经验:
| 优化点 | 说明 |
|---|---|
| 连接池配置 | 使用 POOLED 数据源时,poolMaximumActiveConnections 至少设为 20,避免连接争抢 |
| SQL 日志 | 开发环境开启 log4j.logger.org.apache.ibatis=DEBUG,方便排查慢查询 |
| 二级缓存慎用 | 多节点部署时,MyBatis 二级缓存是进程内缓存,会导致数据不一致,建议用 Redis 统一缓存 |
| 预编译 SQL | 所有 #{} 参数都会被预编译,天然防 SQL 注入,比 $ {} 安全得多 |
另外,不要迷信“全自动”。有一次实习生想偷懒,用 MyBatis Generator 自动生成全套 CRUD,结果生成的 updateByPrimaryKeySelective 方法在线上把 status 字段更新成了 NULL(因为传参对象里没设这个字段)。最后还是我手动改回显式更新。
写在最后:保守派的自我救赎
说实话,作为一个习惯手写 SQL 的老派程序员,刚接触 MyBatis 时我是抗拒的。总觉得“加了一层抽象,性能会不会掉?”、“万一出问题调试更麻烦怎么办?”
但真用起来才发现:好的工具不是取代你的思考,而是解放你的双手。MyBatis 让我不再重复写那些枯燥的 ResultSet 解析代码,又能保留对 SQL 的完全控制。上线两周,DAO 层代码量减少了 60%,Bug 率几乎为零。
最近我在 GitHub 上看到一个叫 MyBatis-Flex 的新项目,号称“比 MyBatis-Plus 更灵活”。虽然我现在主力还在啃 Rust(真的香!),但说不定哪天跳槽新公司,就得靠这些 Java 技术栈吃饭了。
所以啊,别死守“手写一切”的执念。技术人最重要的不是坚持某种风格,而是解决问题的能力。今天你嫌弃的框架,明天可能就是你简历上的亮点。
对了,这篇文章的示例代码我已经扔到 GitHub 了([链接假装存在]),欢迎 star、fork、issue——只要别提“为什么不用 Spring Boot 集成”,因为我司架构还没升级 😅。
好了,咖啡凉了,该去开站会了。产品经理又在群里@我:“那个需求……能提前一天吗?”

评论 0