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

大数据App
2025-06-18 20:04
阅读 450

开篇:为什么我会选择分享这个话题?

开篇:为什么我会选择分享这个话题?

在后端开发这条路上,我们总是绕不开数据库操作。从最开始接触JDBC的“痛苦”经历,到后来尝试各种ORM框架,一路走来踩了不少坑。而今天我要和大家分享的,是我用得最多、也最喜欢的一个——MyBatis。

MyBatis不是一个典型的“全自动”ORM框架,但它足够灵活,在性能敏感的场景下尤其有用武之地。它不像Hibernate那样自动帮你处理一切,但正是这种半自动的风格,让我在很多项目中感受到它的魅力和实用性。

这篇文章我会以一个真实的项目案例为背景,讲述我在使用MyBatis过程中遇到的一些典型问题、我的解决思路,以及一些实际开发中的经验总结。如果你刚开始接触MyBatis,或者想深入理解它的核心机制,希望这篇文章能给你带来启发。

项目背景与需求介绍

项目背景与需求介绍

事情发生在两年前,我参与了一个企业级后台系统重构项目。原系统是一个传统的Spring + JDBC的架构,代码臃肿不说,维护起来更是头痛。由于历史原因,SQL和业务逻辑混合严重,数据结构也不够规范。新项目的目标很明确:

  • 提升代码可维护性
  • 提高数据访问层性能
  • 支持多数据源切换(主要是Oracle和MySQL)
  • 后续要接入分库分表方案

当时我们团队技术栈已经确定使用Spring Boot,所以持久层的选择就成了关键点。虽然团队里有人倾向于Hibernate,但我坚持选择了MyBatis,因为它更贴近SQL的特性更适合我们的业务场景,尤其是在性能要求较高的报表模块和高频查询接口上。

于是,我开始了MyBatis的全面引入工作。

初识MyBatis:为什么选择它?

初识MyBatis:为什么选择它?

可能有些同学会问:“为什么不直接用JPA或Hibernate?那样写代码不是更简单吗?”

确实,JPA和Hibernate这类全自动ORM省去了大量模板代码,但也有明显的缺点:

  1. 生成的SQL质量不可控:Hibernate虽然封装得很漂亮,但对于复杂的联表查询和性能优化往往束手无策。
  2. 学习成本高,调试困难:你不得不花很多时间去了解HQL、Criteria API这些非标准SQL的东西。
  3. 灵活性受限:有时候你要做的是一些非常定制化的SQL,这时候Hibernate就显得捉襟见肘了。

而MyBatis则不同,它强调的是“SQL映射”,也就是让开发者自己写SQL,框架只负责参数映射和结果集转换。这听起来是不是有点老派?但恰恰是这种设计,让我们可以完全掌控SQL,做到极致优化。

更重要的是,MyBatis和Spring整合非常好,配合注解、动态SQL等功能,既保持了简洁,又不失灵活性。

遇到的第一个挑战:如何优雅地组织MyBatis的配置?

遇到的第一个挑战:如何优雅地组织MyBatis的配置?

在我刚接手项目的初期,我尝试把所有Mapper XML文件都放到resources目录下,然后通过<mapper>标签逐个引入。但是很快我就发现,随着模块增多,这种方式变得越来越难管理。

举个例子,比如我们有一个订单模块,里面就有OrderMapper.java、OrderMapper.xml、Order.java等多个文件,如果再加几个关联实体类和Service,整个结构就容易混乱。

于是我决定采用如下方式组织代码结构:

src
├── main
│   ├── java
│   │   └── com.example.mapper
│   │       ├── OrderMapper.java
│   │       ├── UserMapper.java
│   │       └── ...
│   │
│   └── resources
│       └── mapper
│           ├── order
│           │   ├── OrderMapper.xml
│           │   └── OrderDetailMapper.xml
│           └── user
│               └── UserMapper.xml

同时,在application.yml中配置扫描路径:

mybatis:
  mapper-locations: classpath:mapper/**/*.xml

这样做的好处是显而易见的:

  • 按功能模块划分,避免XML文件堆积在一个目录
  • 修改和查找对应Mapper更加直观
  • 便于后续做代码自动生成工具时定位资源

这算是我第一个小改进吧,也让整个项目结构看起来更清爽。

第二个挑战:如何处理动态SQL?

MyBatis最强大的功能之一就是它的动态SQL能力。但在实际使用过程中,很多人只是用了简单的if判断,却忽略了它真正的威力。

我记得当时有个查询接口,用户可以根据多个条件组合筛选订单数据,传入的参数包括订单状态、时间段、客户姓名、手机号等等。一开始我是这么写的:

<select id="selectOrders" resultType="Order">
    SELECT * FROM orders WHERE 1=1
    <if test="status != null">
        AND status = #{status}
    </if>
    <if test="startTime != null and endTime != null">
        AND create_time BETWEEN #{startTime} AND #{endTime}
    </if>
    <if test="customerName != null">
        AND customer_name LIKE CONCAT('%',#{customerName},'%')
    </if>
</select>

这段SQL乍一看没问题,但实际上有几个潜在的问题:

  • WHERE 1=1这种写法在正式场合其实不太推荐,不够优雅
  • 如果没有任何条件传入,那就会变成SELECT * FROM orders WHERE 1=1,可能会扫全表,影响性能
  • 多个AND之间的拼接容易出错,尤其是在嵌套条件判断的情况下

于是,我查阅官方文档,改成了使用<where>标签包裹动态条件的方式:

<select id="selectOrders" resultType="Order">
    SELECT * FROM orders
    <where>
        <if test="status != null">
            status = #{status}
        </if>
        <if test="startTime != null and endTime != null">
            AND create_time BETWEEN #{startTime} AND #{endTime}
        </if>
        <if test="customerName != null">
            AND customer_name LIKE CONCAT('%',#{customerName},'%')
        </if>
    </where>
</select>

这样一来,即使没有条件传入,也不会出现WHERE关键字错误,同时也避免了手动写WHERE 1=1的尴尬写法。而且MyBatis在内部会智能拼接AND/前缀。

除此之外,我还用到了<set>标签用于更新操作,<choose>进行条件分支判断,以及<foreach>处理IN查询。这些都是MyBatis非常实用的功能。

第三个挑战:MyBatis事务控制与多数据源配置

项目后期我们需要接入MySQL和Oracle双数据源的支持,这就涉及到MyBatis对多数据源的配置和事务管理问题。

早期的时候,我们所有的数据库操作都在一个数据源里。Spring Boot默认只有一个DataSource,所以MyBatis也能很好地配合事务。但一旦引入了第二个数据源,情况就变了。

我最初的思路是在不同的服务类中注入不同的SqlSessionTemplate@MapperScan扫描不同的包,但这会导致事务无法跨数据源生效。也就是说,如果某个方法需要操作两个数据源的数据,用Spring的@Transactional注解就不起作用了。

最终我们采用了基于AbstractRoutingDataSource的动态数据源方案。大致流程如下:

  1. 自定义一个ThreadLocal存储当前线程使用的数据源标识符。
  2. 创建多个目标数据源(MySQL、Oracle等)。
  3. 构建一个路由数据源,根据当前线程的标识选择具体的数据源。
  4. 在事务入口处通过切面或者手动设置数据源类型。

关键代码如下:

public class DynamicDataSource extends AbstractRoutingDataSource {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    public static void setDataSource(String dataSource) {
        contextHolder.set(dataSource);
    }

    public static String getDataSource() {
        return contextHolder.get();
    }

    public static void clearDataSource() {
        contextHolder.remove();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return getDataSource();
    }
}

然后在Spring配置中:

@Bean
@ConfigurationProperties(prefix = "spring.datasource.oracle")
public DataSource oracleDataSource() {
    return DataSourceBuilder.create().build();
}

@Bean
@Primary
public DataSource dynamicDataSource(DataSource oracleDataSource, DataSource mysqlDataSource) {
    Map<Object, Object> targetDataSources = new HashMap<>();
    targetDataSources.put("oracle", oracleDataSource);
    targetDataSources.put("mysql", mysqlDataSource);

    DynamicDataSource ds = new DynamicDataSource();
    ds.setTargetDataSources(targetDataSources);
    ds.setDefaultTargetDataSource(mysqlDataSource); // 设置默认数据源
    return ds;
}

@Bean
public PlatformTransactionManager transactionManager(DataSource dynamicDataSource) {
    return new DataSourceTransactionManager(dynamicDataSource);
}

注意:这种做法只适用于单体应用,如果是微服务环境下,建议还是通过服务治理来做数据源隔离,因为事务跨库风险极高,维护成本也很高。

踩过的一些坑

坑一:Mapper接口找不到,导致启动失败

这个问题太常见了。通常是因为没有正确配置mybatis.mapper-locations路径,或者没有在启动类加上@MapperScan注解。

解决方案:

  • 检查你的application.yml中配置的mapper路径是否正确;
  • 启动类上加上@MapperScan("com.example.mapper"),指定正确的Mapper接口所在包;
  • 或者在每个Mapper接口上加上@Mapper注解(不推荐,容易遗漏);

坑二:ResultMap映射出错,字段为空

有时候你会发现,明明数据库中有值,但返回的对象属性却是null。这是因为字段名与Java属性名不匹配。

比如数据库字段是user_name,而你的Java属性是userName,这时候就需要在Mapper XML中显式定义映射关系:

<resultMap id="BaseResultMap" type="User">
    <id column="user_id" property="userId"/>
    <result column="user_name" property="userName"/>
    <result column="email_address" property="emailAddress"/>
</resultMap>

当然,也可以通过开启MyBatis的自动映射功能,比如配置:

mybatis:
  mapUnderscoreToCamelCase: true

这样就可以实现user_name → userName的自动转换。

坑三:SQL语句执行慢,日志看不到完整SQL

我们在生产环境上线之后发现某接口响应时间很长,怀疑是SQL的问题,但日志里打印出来的都是类似这样的内容:

==> Preparing: SELECT * FROM orders WHERE status = ?

这显然看不出来真实情况。为了排查性能问题,我开启了MyBatis的日志插件log4jSTDOUT_LOGGING模式:

<!-- mybatis-config.xml -->
<configuration>
    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>
</configuration>

但这种方式只能输出带?占位符的SQL。想要看到真实执行的SQL,必须结合拦截器或使用Druid内置的SQL监控功能。

这里我强烈推荐引入Druid作为连接池,并开启其内置的SQL监控和慢SQL分析功能,对于生产排障帮助极大。

性能优化小技巧

批量插入优化

我们之前有一个接口需要批量导入用户信息到数据库,每批几万条数据。最开始的做法是一个一个调用INSERT语句,结果可想而知,速度极慢,还容易被打断。

后来改为使用MyBatis的<foreach>结合UNION ALL的方式:

<insert id="batchInsert">
    INSERT INTO users (name, email, created_at)
    <foreach collection="list" item="item" separator=" UNION ALL ">
        (#{item.name}, #{item.email}, #{item.createdAt})
    </foreach>
</insert>

但这种方式在Oracle中并不适用,因为Oracle不支持UNION ALL的INSERT语法。

于是我们换成了另一种方式:使用JDBC的BatchUpdate功能,借助MyBatis的SqlSession实现:

public void batchInsert(List<User> userList) {
    SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false);
    try {
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        for (User user : userList) {
            mapper.insert(user);
        }
        sqlSession.commit();
    } finally {
        sqlSession.close();
    }
}

这样不仅效率高,还能保证事务一致性。

缓存优化

为了缓解数据库压力,我们也启用了MyBatis的一级缓存(默认启用)和二级缓存(需要手动配置)。二级缓存的作用域是同一个Mapper namespace下的所有查询。

配置方式如下:

<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>

不过要注意,使用二级缓存的代价是增加内存占用,并且需要考虑缓存失效的问题。在写多读少的场景下,不建议启用二级缓存。

项目落地后的效果

经过一段时间的重构和优化,项目的整体表现提升明显:

  • 接口平均响应时间下降了约37%
  • 数据库QPS有所降低,CPU负载稳定
  • 代码结构清晰,易于维护和扩展
  • 团队成员更容易上手数据库相关开发

特别是在报表查询部分,原本需要2秒才能返回的接口,优化后基本控制在300ms以内。

给读者的一些建议和注意事项

  1. 合理使用MyBatis的功能,不要滥用动态SQL。虽然动态SQL强大,但如果过于复杂反而会影响可读性和维护性。

  2. 注重SQL编写质量,不要偷懒。即使是MyBatis也要关注索引使用、JOIN顺序等问题。很多时候性能瓶颈不在框架本身。

  3. 学会查看执行计划。无论是MySQL的EXPLAIN还是Oracle的Execution Plan,都能帮你快速定位问题。

  4. 善用工具。比如Druid、Logback、IDEA的MyBatis插件,甚至是数据库的慢查询日志,都是排查问题的利器。

  5. 合理使用缓存。MyBatis的缓存不是银弹,要根据实际情况权衡取舍。

  6. 提前规划好模块化结构。良好的项目结构会让你在后续迭代中省去大量重构成本。

写在最后

回顾这一路的学习和实践,我觉得MyBatis带给我的不只是代码上的便利,更多是对数据库交互本质的理解。它教会我,任何框架都不能代替扎实的SQL功底和系统设计能力。

现在回头看那些踩过的坑,其实也都是成长的一部分。技术这条路本就没有捷径,只有不断试错、总结、优化,才能真正掌握一个工具,甚至一套思想。

希望我的这篇分享能让你少走弯路,也能感受到一点来自实战的真实温度。如有不足之处,欢迎留言交流!


附录:常见MyBatis依赖配置示例

<!-- pom.xml 示例 -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.3.1</version>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.18</version>
</dependency>

作者碎碎念
其实我一直觉得,好的技术文章不仅要讲清楚原理,更要讲明白它是怎么在现实世界里跑起来的。MyBatis不是一个完美的工具,但它足够成熟、足够灵活,值得我们好好琢磨。希望你在使用MyBatis的过程中,也能写出高效、优雅、可控的SQL代码!

评论 0

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