MyBatis基础教程:Java持久层框架入门

♀邓志强
2025-12-17 14:13
阅读 1773

早上八点,咖啡刚泡好,我坐在工位上一边刷着 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&amp;serverTimezone=UTC"/>
        <property name="username" value="root"/>
        <property name="password" value="your_password"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="mapper/ActivityMapper.xml"/>
  </mappers>
</configuration>

吐槽一下&amp; 这个转义符号我每次写都要查一遍,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>

这里有几个坑我必须提:

  1. 字段名和属性名不一致:数据库用下划线(start_time),Java 用驼峰(startTime),必须用 <resultMap> 显式映射,否则查出来全是 null。
  2. 自增主键回填useGeneratedKeys="true" + keyProperty="id" 缺一不可,不然 activity.getId() 拿不到值。
  3. 时间类型处理: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

最热最新
暂无评论
♀邓志强Lv.1
0
影响力
0
文章
0
粉丝