深入浅出MyBatis:从入门到精通,一篇就够了!

112 阅读8分钟

MyBatis通过简单的XML或注解来配置和映射原生信息,将接口与Java的POJO(Plain Old Java Objects,普通的Java对象)映射成数据库中的记录。它免除了几乎所有的JDBC代码和参数设置以及结果集的检索。如果你厌倦了Hibernate的笨重和JPA的复杂,那么MyBatis的简洁和强大一定会让你爱不释手。

本文将带你从MyBatis的基础配置开始,逐步深入到参数处理、结果集映射、动态SQL和二级缓存等核心概念。无论你是初学者还是有一定经验的开发者,相信这篇文章都能让你对MyBatis有更深刻的理解。

一、 核心配置文件:mybatis-config.xml

万事开头难,我们先从MyBatis的心脏——核心配置文件开始。这个文件通常命名为 mybatis-config.xml,它配置了MyBatis的全局行为,如数据库环境、类型别名、事务管理器、映射器等。

一个典型的mybatis-config.xml结构如下:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <properties resource="db.properties"/>

    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>

    <typeAliases>
        <typeAlias type="com.example.model.User" alias="User"/>
        <package name="com.example.model"/>
    </typeAliases>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="mappers/UserMapper.xml"/>
        </mappers>

</configuration>

配置项详解:

  • <properties> : 用于引入外部属性文件(如db.properties),方便管理数据库连接等敏感信息。在后续配置中可以使用 ${key} 的形式引用。

  • <settings> : 这是MyBatis中极为重要的调整设置,会改变MyBatis的运行时行为。

    • mapUnderscoreToCamelCase: 自动将数据库中下划线命名的列(如 user_name)映射到Java对象中驼峰命名的属性(如 userName),非常实用!
    • logImpl: 配置MyBatis使用何种日志实现,STDOUT_LOGGING 会将日志输出到标准控制台,便于开发调试。
  • <typeAliases> : 为Java类型设置一个简短的名字。这样做可以减少在Mapper XML中书写冗长的完全限定类名。

  • <environments> : 配置数据库连接环境。可以配置多个 environment,通过 default 属性指定默认使用的环境。

    • transactionManager: 事务管理器,JDBC表示使用JDBC的提交和回滚设置。在Spring集成中通常会使用MANAGED
    • dataSource: 数据源,POOLED表示使用MyBatis内置的连接池。
  • <mappers> : 注册SQL映射文件。MyBatis会从这些文件中加载SQL语句。

二、 Mapper接口与XML

MyBatis的核心思想是接口绑定。我们只需要定义一个Mapper接口(例如UserMapper.java),然后编写一个对应的XML文件(UserMapper.xml)来提供SQL实现。

UserMapper.java (接口)

package com.example.mapper;

import com.example.model.User;

public interface UserMapper {
    User findById(Integer id);
    int insertUser(User user);
}

UserMapper.xml (SQL实现)

<?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.UserMapper">

    <select id="findById" resultType="com.example.model.User">
        select id, user_name, email from users where id = #{id}
    </select>

    <insert id="insertUser" parameterType="com.example.model.User">
        insert into users (user_name, email) values (#{userName}, #{email})
    </insert>

</mapper>

关键点:

  • XML文件的namespace必须与Mapper接口的全限定名完全一致。
  • select, insert, update, delete等标签的id属性必须与接口中对应的方法名一致。
  • parameterType指定了传入参数的类型,resultType指定了返回结果的类型。

三、 插入数据并获取自增ID

在业务中,我们经常需要在插入一条数据后立刻获取它的自增主键ID。MyBatis提供了非常便捷的方式。

只需要在 insert 标签中增加两个属性:useGeneratedKeys="true"keyProperty="id"

<insert id="insertUser" parameterType="User" useGeneratedKeys="true" keyProperty="id">
    insert into users (user_name, email, password)
    values (#{userName}, #{email}, #{password})
</insert>
  • useGeneratedKeys="true": 告诉MyBatis这个操作会生成主键。
  • keyProperty="id": 指定将生成的主键值设置到传入的User对象的哪个属性上。id对应User类中的id字段。

Java代码调用:

User user = new User();
user.setUserName("Gemini");
user.setEmail("gemini@google.com");
user.setPassword("secret");

System.out.println("插入前,User ID: " + user.getId()); // 输出: null

userMapper.insertUser(user); // 执行插入

System.out.println("插入后,User ID: " + user.getId()); // 输出: (数据库生成的自增ID)

执行insertUser方法后,user对象的id属性会被自动回填。

四、#{}${} 的天壤之别

这是MyBatis中一个非常重要且常见的面试题。两者都用于参数替换,但机制完全不同。

  • #{} (预编译参数) :

    • 原理: MyBatis在处理#{}时,会将其替换为 ? 占位符,并使用PreparedStatement来设置参数。

    • 优点: 能有效防止SQL注入。因为参数值是作为PreparedStatement的参数来处理的,而不是直接拼接到SQL字符串中,数据库驱动会对其进行安全处理。

    • 适用场景: 绝大多数情况下都应该使用#{}来传递业务参数。

    select * from users where id = #{userId}
    
  • ${} (字符串替换) :

    • 原理: MyBatis在处理${}时,会直接将${}中的内容原样拼接到SQL语句中。

    • 缺点: 存在严重的SQL注入风险。如果传入的参数来自用户输入且未经过严格过滤,攻击者可以构造恶意的SQL片段。

    • 适用场景: 仅在需要动态替换SQL的非参数部分时使用,例如动态指定表名、列名或ORDER BY的排序字段。使用时必须对参数来源进行严格控制和校验。

    select * from users order by ${orderByCol}
    select * from users order by ${orderByCol}
    

总结: 能用 #{} 就坚决不用 ${},安全第一!

五、 灵活多变的参数封装

MyBatis支持多种方式向SQL传递参数,以应对不同的业务场景。

1. 单个参数

如果方法只有一个参数(基本类型、包装类型、String),在XML中可以直接使用。

// Mapper接口
User findById(Integer id);
<select id="findById" resultType="User">
    select * from users where id = #{id}
</select>

2. 多个参数

当方法有多个参数时,MyBatis默认无法区分它们。需要使用 @Param 注解来为每个参数命名。

// Mapper接口
User findByNameAndEmail(@Param("name") String userName, @Param("emailAddr") String email);
<select id="findByNameAndEmail" resultType="User">
    select * from users where user_name = #{name} and email = #{emailAddr}
</select>

3. 对象参数 (POJO)

这是最常用、最推荐的方式。将参数封装到一个Java对象中,可读性和可维护性都非常好。

// Mapper接口
int updateUser(User user);
<update id="updateUser" parameterType="User">
    update users set
        user_name = #{userName},
        email = #{email}
    where id = #{id}
</update>

4. List参数

当需要执行 IN 查询时,可以使用List或数组作为参数,并结合动态SQL中的 foreach 标签。

// Mapper接口
List<User> findByIds(List<Integer> ids);
<select id="findByIds" resultType="User">
    select * from users where id in
    <foreach item="id" collection="list" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>
  • collection="list": 当参数是List时,固定写list。如果是数组,则写array。如果使用@Param("ids")注解,则写ids
  • item="id": 每次迭代的元素别名。
  • open, close, separator: 用于拼接SQL的开头、结尾和分隔符。

5. Map参数

使用Map传递参数也非常灵活,但可读性不如POJO。

// Mapper接口
List<User> searchUsers(Map<String, Object> params);
<select id="searchUsers" resultType="User">
    select * from users where user_name like #{name} and email = #{email}
</select>

Java调用:

Map<String, Object> map = new HashMap<>();
map.put("name", "%" + keyword + "%");
map.put("email", "test@example.com");
List<User> users = userMapper.searchUsers(map);

六、强大的结果集封装:ResultMap

当数据库列名与Java对象属性名不一致,或者需要处理复杂的关联查询(一对一、一对多)时,resultType的自动映射就显得力不-从心了。这时,ResultMap 就该登场了!

ResultMap 是MyBatis中最强大、最重要的元素,它能让你完全掌控结果集与对象的映射关系。

1. 基础映射 (解决列名属性名不一致)

假设数据库表users的列是 user_id, user_name, user_email,而User类的属性是 id, userName, email

<resultMap id="userResultMap" type="User">
    <id property="id" column="user_id"/>
    <result property="userName" column="user_name"/>
    <result property="email" column="user_email"/>
</resultMap>

<select id="findById" resultMap="userResultMap">
    select user_id, user_name, user_email from users where user_id = #{id}
</select>

通过resultMap,我们将user_id列映射到了id属性,user_name映射到了userName属性,完美解决了不一致问题。

2. 关联映射:一对一 (association)

假设我们有一个Order订单类,它内部包含一个User对象,表示下单的用户。

public class Order {
    private Integer id;
    private String orderNo;
    private User user; // 关联的用户对象
    // getters and setters
}

查询订单信息,同时带出用户信息:

<resultMap id="orderResultMap" type="Order">
    <id property="id" column="order_id"/>
    <result property="orderNo" column="order_no"/>
    <association property="user" javaType="User">
        <id property="id" column="user_id"/>
        <result property="userName" column="user_name"/>
        <result property="email" column="user_email"/>
    </association>
</resultMap>

<select id="findOrderById" resultMap="orderResultMap">
    select
        o.id as order_id,
        o.order_no,
        u.id as user_id,
        u.user_name,
        u.user_email
    from orders o
    left join users u on o.user_id = u.id
    where o.id = #{orderId}
</select>

MyBatis会根据resultMap的定义,将查询出的扁平化结果集自动封装成嵌套的OrderUser对象。

3. 关联映射:一对多 (collection)

假设一个User可以有多个Order订单。

public class User {
    private Integer id;
    private String userName;
    private List<Order> orders; // 关联的订单列表
    // getters and setters
}

查询用户信息,同时带出他所有的订单列表:

<resultMap id="userWithOrdersResultMap" type="User">
    <id property="id" column="user_id"/>
    <result property="userName" column="user_name"/>
    <collection property="orders" ofType="Order">
        <id property="id" column="order_id"/>
        <result property="orderNo" column="order_no"/>
    </collection>
</resultMap>

<select id="findUserWithOrders" resultMap="userWithOrdersResultMap">
    select
        u.id as user_id,
        u.user_name,
        o.id as order_id,
        o.order_no
    from users u
    left join orders o on u.id = o.user_id
    where u.id = #{userId}
</select>

这样,查询结果就会被封装成一个User对象,其orders属性是一个包含该用户所有订单的List

七、 动态SQL:让SQL“活”起来

动态SQL是MyBatis的另一个核心特性,它允许我们根据不同的条件动态地拼接SQL语句,极大地提高了SQL的复用性。

1. if (条件判断)

<select id="findActiveUsers" resultType="User">
    select * from users where 1=1
    <if test="userName != null and userName != ''">
        and user_name like #{userName}
    </if>
    <if test="email != null and email != ''">
        and email = #{email}
    </if>
</select>

如果传入的userName不为空,则拼接AND user_name LIKE ...条件。

2. where (智能处理AND/OR)

上面的if写法有个小问题,如果所有条件都不满足,SQL会是select * from users where 1=1,虽然也能工作,但不够优雅。如果去掉1=1,第一个if满足时,SQL会变成select * from users where and user_name ...,这会产生语法错误。

<where>标签可以智能地处理这个问题:它只在有条件内容时才插入WHERE子句,并且会自动去掉第一个条件前面的ANDOR

<select id="findActiveUsers" resultType="User">
    select * from users
    <where>
        <if test="userName != null and userName != ''">
            and user_name like #{userName}
        </if>
        <if test="email != null and email != ''">
            and email = #{email}
        </if>
    </where>
</select>

这才是动态条件查询的正确打开方式!

3. set (智能处理更新语句的逗号)

update语句中,如果使用if,可能会导致SQL末尾多一个逗号。<set>标签可以解决这个问题。

<update id="updateUserSelective">
    update users
    <set>
        <if test="userName != null">user_name = #{userName},</if>
        <if test="email != null">email = #{email},</if>
        <if test="password != null">password = #{password},</if>
    </set>
    where id = #{id}
</update>

<set>会自动插入SET关键字,并移除结尾多余的逗号。

4. choose, when, otherwise (分支选择)

类似于Java中的switch语句,只执行第一个满足条件的分支。

<select id="findByCondition" resultType="User">
    select * from users where
    <choose>
        <when test="title != null">
            title = #{title}
        </when>
        <when test="author != null">
            author = #{author}
        </when>
        <otherwise>
            is_active = 1
        </otherwise>
    </choose>
</select>

5. foreach (循环)

前面在List参数部分已经见过它了,是处理IN查询的神器。

八、 性能优化:缓存机制

为了提升查询性能,MyBatis内置了两级缓存机制。

1. 一级缓存 (SqlSession级别)

  • 作用域: SqlSession。一级缓存是默认开启的,无法关闭。
  • 生命周期: 当一个SqlSession被创建时,它会持有一个本地缓存。当SqlSession关闭或被清空(commit, rollback)时,缓存就会被清空。
  • 工作原理: 在同一个SqlSession中,如果执行了两次完全相同的查询(相同的SQL、相同的参数),第二次查询会直接从本地缓存中获取结果,而不会再次访问数据库。
  • 注意: 任何insert, update, delete操作都会清空当前SqlSession的一级缓存,以防止脏读。

2. 二级缓存 (Mapper Namespace级别)

  • 作用域: Mapper Namespace。二级缓存是跨SqlSession的,多个SqlSession可以共享同一个Mapper的二级缓存。

  • 如何开启:

    1. mybatis-config.xml中全局开启二级缓存:

      <settings>
          <setting name="cacheEnabled" value="true"/>
      </settings>
      
    2. 在需要开启缓存的Mapper XML文件中添加<cache/>标签:

      <mapper namespace="com.example.mapper.UserMapper">
          <cache/>
          </mapper>
      
  • 工作原理: 当一个SqlSession提交事务(sqlSession.commit())后,它所执行的查询结果如果对应的Mapper开启了二级缓存,就会被存入二级缓存中。其他SqlSession在执行相同的查询时,会先去二级缓存中查找,如果找到就直接返回,不再查询数据库。

  • 缓存失效: 同样地,该namespace下的任何insert, update, delete操作都会导致二级缓存被清空。

  • 注意事项:

    • 放入二级缓存的POJO对象必须实现 java.io.Serializable 接口。
    • 二级缓存适用于读多写少数据变更不频繁的场景。对于实时性要求高的业务,要慎用二级缓存,因为它可能会导致数据不一致(脏读)。
    • <cache>标签有很多属性可以配置,如 eviction(回收策略)、flushInterval(刷新间隔)、size(缓存大小)、readOnly(是否只读)等,可以进行精细化控制。

总结

MyBatis以其简洁灵活高效赢得了众多Java开发者的青睐。它让我们能够专注于SQL本身,通过简单的配置即可完成复杂的数据库操作。

回顾一下,我们今天学习了:

  1. 核心配置mybatis-config.xml是MyBatis的基石。
  2. 基本使用:通过Mapper接口和XML文件实现SQL的绑定与执行。
  3. 参数处理:深入理解了#{}${}的区别,并掌握了多种参数传递方式。
  4. 结果映射:学会了使用强大的ResultMap处理各种复杂的查询结果。
  5. 动态SQL:利用if, where, foreach等标签让SQL变得智能和可复用。
  6. 二级缓存:了解了如何通过二级缓存提升应用性能。

希望这篇详尽的博客能够帮助你构建一个清晰而全面的MyBatis知识体系。当然,MyBatis还有插件、拦截器等更高级的玩法等待你去探索。技术的道路,学无止境,让我们一起不断进步!