MyBatis基础教程:从“手写JDBC”到优雅操作数据库的实战之路

徐华_技术
2025-06-21 15:52
阅读 394

作为一名在互联网公司工作的后端开发工程师,我每天都要和数据库打交道。刚入行那会儿,我写的代码基本是直接用JDBC连接数据库,结果不仅代码冗长复杂,而且维护起来极其痛苦。后来有幸接触到了MyBatis这个Java持久层框架,它改变了我对数据层开发的看法。

今天我就想结合自己在项目中使用MyBatis的经验,来写一篇接地气的基础教程,分享一下我是如何一步步掌握这个工具,并在工作中解决实际问题的。

项目背景与问题起因

项目背景与问题起因

事情要从我们团队之前做的一个电商后台系统说起。那是一个商品管理系统,包含用户管理、订单管理、库存管理等多个模块。刚开始的时候,为了快速验证业务逻辑,我直接使用了最原始的JDBC方式来做数据库交互。

比如,我需要查询某个用户的订单信息:

Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
    conn = dataSource.getConnection();
    String sql = "SELECT * FROM orders WHERE user_id = ?";
    ps = conn.prepareStatement(sql);
    ps.setInt(1, userId);
    rs = ps.executeQuery();


![系统架构设计图-1](https://code-guide.oss.shanghai.autogptai.club/common/file/download?name=date2025062115/8a495442-9ef2-481f-95c6-627e6b17dc3d.jpg)


    while (rs.next()) {
        Order order = new Order();
        order.setId(rs.getInt("id"));
        order.setUserId(rs.getInt("user_id"));
        order.setAmount(rs.getDouble("amount"));
        // ... 其他字段赋值
        orders.add(order);
    }


![数据流转过程-2](https://code-guide.oss.shanghai.autogptai.club/common/file/download?name=date2025062115/808a8f90-6728-4e1a-95cc-52d5ed391ea6.jpg)


} catch (Exception e) {
    // 异常处理
} finally {
    // 关闭资源
}

这短短几十行代码看着简单,但背后隐藏的问题很多:

  • 重复性高:几乎每个DAO类都会复制粘贴这段样板代码
  • 维护成本高:一旦表结构变更,所有涉及的SQL语句都得跟着改
  • 容易出错:资源未关闭、SQL注入等问题频发
  • 扩展性差:后续要做分页查询、动态SQL、事务管理时非常麻烦

随着功能越来越多,我和同事们经常为了一个简单的数据库操作修改大量代码。这种低效且脆弱的架构,严重影响了我们的迭代速度。

解决方案:引入MyBatis

解决方案:引入MyBatis

我们决定重构数据访问层,选用MyBatis作为ORM框架。虽然当时团队里不少人对MyBatis也不是特别熟,但大家都有一个共识:不能再继续手动写JDBC了!

初识MyBatis

MyBatis 是一个基于 Java 的持久层框架,它不像 Hibernate 这样的全自动 ORM 框架那样“重”,而是更加灵活,允许你通过 XML 文件或注解来定义 SQL 查询。它的理念很明确:SQL 应该由开发者控制,而不是框架自动生成。

我们在 pom.xml 中添加了以下依赖(Spring Boot 项目):

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.3.1</version>
</dependency>

然后配置了数据库连接信息:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/my_shop
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

这样就能让 Spring 自动为我们创建 SqlSessionFactorySqlSession 实例了。

简单示例:告别JDBC模板代码

我们先来看一个最简单的例子:查询某个用户的订单列表。

1. 创建实体类 Order.java

public class Order {
    private int id;
    private int userId;
    private double amount;
    private LocalDateTime createTime;
    // getter/setter略
}

2. 创建Mapper接口 OrderMapper.java

@Mapper
public interface OrderMapper {
    List<Order> selectOrdersByUserId(int userId);
}

3. 编写XML映射文件 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="selectOrdersByUserId" resultType="Order">
        SELECT *
        FROM orders
        WHERE user_id = #{userId}
    </select>
</mapper>

这样,当我们调用 orderMapper.selectOrdersByUserId(userId) 时,MyBatis 就会自动执行对应的 SQL 并将结果映射成 Order 对象列表。

是不是感觉比以前清爽多了?最关键的是不用再写那些 try-catch-close 资源释放的代码,更不用手动赋值字段!

动态 SQL:应对复杂查询场景

后来我们在做库存管理的时候,遇到了一个多条件筛选的需求:用户可以通过商品名、状态、分类等不同组合来过滤库存记录。

这时候传统的拼接 SQL 方式很容易出错,而 MyBatis 提供的 <if><choose><where> 标签就显得非常好用了。

例如:

<select id="queryStocks" parameterType="map" resultType="Stock">
    SELECT *
    FROM stocks
    <where>
        <if test="name != null and name.trim() != ''">
            AND name LIKE CONCAT('%', #{name}, '%')
        </if>

        <if test="categoryId != null">
            AND category_id = #{categoryId}
        </if>

        <if test="status != null">
            AND status = #{status}
        </if>
    </where>
</select>

这样一来,无论前端传哪些参数,都能正确构建出对应的 SQL 语句,避免了大量的字符串拼接工作,也减少了 SQL 注入的风险。

一对多关联映射:复杂关系也能轻松搞定

我们在做订单详情页时发现一个订单可能关联多个商品,这就涉及到了对象之间的嵌套关系。

MyBatis 支持通过 <association><collection> 来实现复杂的对象映射。

例如订单和商品是一对多的关系:

public class Order {
    private int id;
    private List<Product> products; // 一个订单对应多个商品
}

public class Product {
    private int id;
    private String name;
    private double price;
}

XML映射如下:

<resultMap id="orderWithProducts" type="Order">
    <id property="id" column="id"/>
    <collection property="products" ofType="Product">
        <id property="id" column="product_id"/>
        <result property="name" column="product_name"/>
        <result property="price" column="product_price"/>
    </collection>
</resultMap>

<select id="getOrderDetail" resultMap="orderWithProducts">
    SELECT
        o.id,
        p.id AS product_id,
        p.name AS product_name,
        p.price AS product_price
    FROM orders o
    LEFT JOIN order_products op ON o.id = op.order_id
    LEFT JOIN products p ON p.id = op.product_id
    WHERE o.id = #{orderId}
</select>

通过这样的配置,MyBatis 就会自动帮我们把商品集合装进 Order 对象中。以前这些逻辑我们要手动处理,现在一行代码都不用写。

多表联查 vs 单表分次查?

不过这里也带来了一个疑问:我们应该尽量使用一次 join 完成数据加载,还是分开多次查询比较好?

我们在压测的时候发现,在一些高并发场景下,过多的 join 操作会导致 MySQL CPU 占用飙升。特别是在订单详情这种关联三四张表的情况下,性能下降明显。

于是我们做了个小优化:将主表数据单独查出来,再根据 ID 批量查子表的数据。通过 Redis 缓存热点数据 + 合理的数据库索引设计,整体效率反而更高。

这也让我意识到,不要盲目追求一次 SQL 查询完成所有数据获取,合理拆分并利用缓存策略才是提升性能的关键。

效果总结:从“累死”到“优雅”

效果总结:从“累死”到“优雅”

自从我们全面采用 MyBatis 之后,系统的数据访问层发生了翻天覆地的变化:

  • 开发效率大大提高:以前写个查询要半小时,现在十分钟就能搞定。
  • 代码可读性和可维护性变强了:SQL 和 Java 代码分离,便于维护和测试。
  • 错误率大幅降低:自动映射避免了字段赋值错误,动态 SQL 减少了拼接问题。
  • 性能优化更容易进行:通过查看 SQL 日志,我们可以快速定位慢查询和索引缺失问题。

更关键的是,我们在生产环境运行了一段时间后,MySQL 性能表现稳定,没有出现过严重的 DB 负载问题。

当然,这一切的前提是我们对 MyBatis 有深入的理解和良好的使用习惯。下面我再分享几个我在使用 MyBatis 过程中的经验和建议。

我的经验分享 & 使用建议

我的经验分享 & 使用建议

1. 不要滥用 #{} 和 ${}

这是新手最容易踩坑的地方之一。#{} 是预编译的方式,可以防止 SQL 注入;而 ${} 是字符串替换,在某些特殊情况下必须用,但一定要注意安全性。

举个例子:如果你想动态排序字段,可以这么写:

<select id="getAllOrders" resultType="Order">
    SELECT * FROM orders
    ORDER BY ${sortField} ${sortOrder}
</select>

但前提是 sortFieldsortOrder 必须经过白名单校验,否则极易被攻击。推荐的做法是在服务层做字段合法性检查,而不是完全交给 SQL 层去处理。

2. 合理使用 ResultMap

有时候我们会遇到数据库字段和 Java 属性命名不一致的情况。MyBatis 提供了 <result> 标签来手动指定映射关系,避免字段匹配不上导致的空值问题。

举个例子,如果数据库中有字段名是 create_time,而 Java 类中属性是 createTime,就可以这样配置:

<resultMap id="orderResultMap" type="Order">
    <id property="id" column="id"/>
    <result property="createTime" column="create_time"/>
</resultMap>

这样即使命名风格不一致,也能正常映射。

3. 分页查询小心性能陷阱

我们在做后台管理页面时常常需要用到分页功能,常见的写法是:

SELECT * FROM users LIMIT #{pageNum}, #{pageSize}

但如果数据量过大(比如上百万条),这种偏移式的分页会导致性能急剧下降。MyBatis 默认并不强制限制这种写法,但在生产环境中,我们采用了“滚动分页”的方式(即使用上次查询的最后一个 ID 作为起点),有效解决了性能问题。

4. 结合 AOP 做日志打印和异常处理

我们项目中加了一个切面,用来拦截所有的数据库操作,打印 SQL 语句和执行耗时,方便排查线上问题:

@Aspect
@Component
public class MyBatisLogAspect {

    @Around("execution(* com.example.mapper.*.*(..))")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long executionTime = System.currentTimeMillis() - start;

        System.out.println(joinPoint.getSignature() + " executed in " + executionTime + "ms");
        return result;
    }
}

有了这套机制之后,我们能第一时间发现慢 SQL,及时进行优化。

5. 合理规划数据库索引

MyBatis 只是一个 SQL 执行引擎,真正的性能优化还得靠数据库本身的支持。我们在项目初期阶段就会和 DBA 一起规划好索引,特别是在频繁查询的字段上建立合适的复合索引,这对整个系统的响应时间影响非常大。

写在最后

其实 MyBatis 的内容远不止这些,本文只是介绍了我日常开发中最常用的一些核心功能。如果你刚刚开始接触它,不妨从最基础的 CRUD 开始练起,然后逐步尝试动态 SQL、关联映射等高级功能。

技术的成长往往是从“能用”到“会用”,再到“懂原理”。我也曾经历过那段一边查文档一边调试的时期,但现在回头看,MyBatis 让我把更多的精力集中在业务逻辑的设计上,而不是繁琐的数据操作上。

如果你正打算学习 MyBatis,希望这篇文章能给你一点方向和信心。记住,工具的本质是为了提高生产力,而不是增加复杂度。掌握了它,你会感受到一种前所未有的“掌控感”。

加油吧,未来的 Java 高手!

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝