MyBatis 实操笔记

MyBatis 从入门到进阶实操笔记

本篇是系统学习 MyBatis 的全量实操整理,覆盖 XML 配置、动态 SQL、关联映射、延迟加载、缓存机制、注解开发六大核心模块,所有代码均经过运行验证。文中同时给出 XML 与纯注解两种实现方式,附带常见坑点说明,适合入门复盘与面试复习。

一、环境搭建与核心配置

1.1 全局配置文件

MyBatis 的核心配置文件负责数据库连接、别名、缓存、映射文件扫描等全局配置,以下是完整的 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>
    <!-- 读取外部jdbc配置文件 -->
    <properties resource="jdbc.properties"></properties>

    <!-- 全局设置 -->
    <settings>
        <!-- 开启延迟加载 -->
        <setting name="lazyLoadingEnabled" value="true"/>
        <!-- 按需加载(消极加载),用到关联属性才触发查询 -->
        <setting name="aggressiveLazyLoading" value="false"/>
        <!-- 开启二级缓存总开关 -->
        <setting name="cacheEnabled" value="true"/>
    </settings>

    <!-- 注册别名:包下所有实体类默认使用类名作为别名,不区分大小写 -->
    <typeAliases>
        <package name="com.qcby.domain"/>
    </typeAliases>

    <!-- 环境配置:可配置多套环境,默认使用mysql -->
    <environments default="mysql">
        <environment id="mysql">
            <!-- 事务管理器:使用JDBC原生事务 -->
            <transactionManager type="JDBC"/>
            <!-- 数据源:POOLED使用连接池,UNPOOLED不使用 -->
            <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>
        <!-- 方式1:逐个引入XML文件 -->
        <mapper resource="mappers/UserMapper.xml"/>
        <mapper resource="mappers/AccountMapper.xml"/>
        <mapper resource="mappers/RoleMapper.xml"/>
        <!-- 方式2:包扫描,同时支持XML和注解 -->
        <!-- <package name="com.qcby.mapper"/> -->
    </mappers>
</configuration>

1.2 实体类准备

本次实操涉及三张表:用户表user、账户表account、角色表role,以及用户角色中间表user_role

User 用户实体

public class User implements Serializable{
    private Integer id;
    private String username;
    private Date birthday;
    private String sex;
    private String address;
    // foreach标签批量查询用
    private List<Integer> ids;
    // 一对多:一个用户对应多个账户
    private List<Account> accounts;
    // 省略getter/setter、toString
}

Account 账户实体

public class Account implements Serializable{
    private Integer id;
    private Integer uid; // 用户外键
    private Double money;
    // 多对一:一个账户属于一个用户
    private User user;
    // 省略getter/setter、toString
}

Role 角色实体

public class Role implements Serializable{
    private Integer id;
    private String role_name;
    private String role_desc;
    // 多对多:一个角色对应多个用户
    private List<User> users;
    // 省略getter/setter、toString
}

QueryVo 包装类
用于复杂查询条件的封装,将多个实体类组合成一个参数对象

public class QueryVo implements Serializable {
    private String name;
    private User user;
    private Role role;
    // 省略getter/setter
}

二、基础CRUD(XML实现)

2.1 Mapper 接口定义

public interface UserMapper {
    // 查询所有用户
    List<User> findAll();
    // 根据id查询
    User findById(Integer userId);
    // 新增用户
    void insert(User user);
    // 修改用户
    void update(User user);
    // 删除用户
    void delete(Integer userId);
    // 模糊查询
    List<User> findByName(String username);
    // 查询总记录数
    Integer findByCount();
    // 包装类条件查询
    List<User> findByVo(QueryVo vo);
    // resultMap字段映射演示
    List<User> findUsers();
}

2.2 Mapper.xml 映射实现

<mapper namespace="com.qcby.mapper.UserMapper">
    <!-- 抽取公共SQL片段,提升复用性 -->
    <sql id="findAllSql">
        select * from user
    </sql>

    <!-- 查询所有 -->
    <select id="findAll" resultType="user">
        <include refid="findAllSql"/>
    </select>

    <!-- 根据id查询 -->
    <select id="findById" resultType="user" parameterType="int">
        select * from user where id = #{id}
    </select>

    <!-- 新增用户,返回自增主键 -->
    <insert id="insert" parameterType="user">
        <selectKey keyProperty="id" order="AFTER" resultType="java.lang.Integer">
            select last_insert_id();
        </selectKey>
        insert into user (username,birthday,sex,address) 
        values (#{username},#{birthday},#{sex},#{address})
    </insert>

    <!-- 修改用户 -->
    <update id="update" parameterType="user">
        update user set username = #{username},birthday = #{birthday},
        sex = #{sex},address=#{address} where id = #{id}
    </update>

    <!-- 删除用户 -->
    <delete id="delete" parameterType="Integer">
        delete from user where id = #{id}
    </delete>

    <!-- 模糊查询:${}方式,参数直接拼接进SQL -->
    <select id="findByName" resultType="user" parameterType="string">
        select * from user where username like '%${value}%'
    </select>

    <!-- 聚合查询 -->
    <select id="findByCount" resultType="int">
        select count(*) from user
    </select>

    <!-- 包装类查询:通过OGNL表达式取属性 -->
    <select id="findByVo" parameterType="queryVo" resultType="user">
        select * from user where username = #{user.username}
    </select>

    <!-- resultMap解决字段名与属性名不匹配 -->
    <select id="findUsers" resultMap="userMap">
        select id _id,username _username,birthday _birthday,sex _sex,address _address from user
    </select>
    <resultMap id="userMap" type="user">
        <result property="id" column="_id"/>
        <result property="username" column="_username" />
        <result property="birthday" column="_birthday" />
        <result property="sex" column="_sex" />
        <result property="address" column="_address" />
    </resultMap>
</mapper>

2.3 #{} 与 ${} 核心区别

这是MyBatis最基础也最容易踩坑的点,也是面试高频题:

  1. #{参数}:预编译占位符(PreparedStatement)

    • SQL底层生成?占位符,参数单独传递
    • 自动给字符串添加单引号,自动转义特殊字符
    • 彻底防止SQL注入,安全性高
    • 适用场景:所有普通条件值(id、name、数值、模糊查询参数)
    • 模糊查询推荐写法:like concat('%',#{name},'%')
  2. ${参数}:直接字符串拼接(Statement)

    • 直接把参数拼接进SQL语句,无占位符
    • 不会自动添加单引号,字符串需要手动加''
    • 存在严重SQL注入风险,慎用
    • 适用场景:占位符无法使用的语法位置(动态表名、动态查询列、order by排序字段)
    • 使用约束:必须做参数白名单校验,防止注入攻击

开发规范:优先使用#{};只有语法不支持占位符时,才使用${}

2.4 单元测试

public class UserTest {
    private InputStream in;
    private SqlSession session;
    private UserMapper mapper;

    @Before
    public void init() throws Exception {
        in = Resources.getResourceAsStream("SqlMapConfig.xml");
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
        session = factory.openSession();
        mapper = session.getMapper(UserMapper.class);
    }

    @After
    public void destory() throws IOException {
        session.commit(); // 增删改需要提交事务
        session.close();
        in.close();
    }

    @Test
    public void testFindAll() {
        List<User> list = mapper.findAll();
        for (User user : list) {
            System.out.println(user);
        }
    }
}

三、动态SQL

动态SQL是MyBatis的核心优势之一,可以根据参数条件灵活拼接SQL语句。

3.1 if + where 标签

where标签会自动处理多余的and/or,比手动写where 1=1更优雅。

<!-- 动态条件查询 -->
<select id="findByWhere1" parameterType="user" resultType="user">
    select * from user
    <where>
        <if test="username != null and username != ''">
            and username like #{username}
        </if>
        <if test="sex != null and sex != ''">
            and sex = #{sex}
        </if>
    </where>
</select>

3.2 foreach 标签

用于批量查询、批量插入等场景,支持两种常用写法:

写法1:or 拼接

<select id="findByIds" parameterType="user" resultType="user">
    select * from user
    <where>
        <foreach collection="ids" open="id = " separator="or id = " item="i">
            #{i}
        </foreach>
    </where>
</select>

写法2:in 查询(更常用)

<select id="findByIds2" parameterType="user" resultType="user">
    select * from user
    <where>
        <foreach collection="ids" open="id in ( " separator="," close=")" item="i">
            #{i}
        </foreach>
    </where>
</select>

注意:collection属性值对应参数中的集合属性名;如果直接传入List参数,默认值为list,数组为array

四、关联查询(XML版)

MyBatis通过resultMap实现关联结果映射,核心两个标签:

  • <association>:用于一对一/多对一映射,属性javaType指定关联对象类型
  • <collection>:用于一对多/多对多映射,属性ofType指定集合泛型类型

4.1 多对一关联:账户查用户

4.1.1 立即加载(联表查询)

一次SQL查出所有数据,适合关联数据频繁使用的场景

<select id="findAll" resultMap="accountMap">
    select a.*,u.username,u.address from account a,user u where a.uid = u.id
</select>

<resultMap id="accountMap" type="account">
    <result property="id" column="id" />
    <result property="uid" column="uid"/>
    <result property="money" column="money"/>
    <!-- 关联用户对象 -->
    <association property="user" javaType="user">
        <result property="username" column="username"/>
        <result property="address" column="address"/>
    </association>
</resultMap>
4.1.2 延迟加载(嵌套查询)

先查账户表,用到用户信息时再单独发SQL查询,适合关联数据不常用的场景

<select id="findAll2" resultMap="accountMap1">
    SELECT * from account
</select>

<resultMap type="account" id="accountMap1">
    <id column="id" property="id"/>
    <result column="uid" property="uid"/>
    <result column="money" property="money"/>
    <!-- 
        select:指定调用哪个Mapper的哪个方法查询关联数据
        column:把哪个字段的值作为参数传过去
     -->
    <association property="user" javaType="user" 
                 select="com.qcby.mapper.UserMapper.findById" 
                 column="uid"/>
</resultMap>

4.2 一对多关联:用户查账户

4.2.1 立即加载(左连接)
<select id="findOneToMany" resultMap="userAccountMap">
    select u.*,a.money from user u left join account a on u.id = a.uid
</select>

<resultMap type="user" id="userAccountMap">
    <result property="id" column="id"/>
    <result property="username" column="username"/>
    <result property="birthday" column="birthday"/>
    <result property="sex" column="sex"/>
    <result property="address" column="address"/>
    <!-- 关联账户集合 -->
    <collection property="accounts" ofType="account">
        <result property="money" column="money"/>
    </collection>
</resultMap>
4.2.2 延迟加载(嵌套查询)
<select id="findAll2" resultMap="userMap2">
    select * from user
</select>

<resultMap type="user" id="userMap2">
    <id column="id" property="id"/>
    <result column="username" column="username"/>
    <collection property="accounts" ofType="account" 
                select="com.qcby.mapper.AccountMapper.findByUid" 
                column="id"/>
</resultMap>

4.3 多对多关联:角色查用户

多对多必须通过中间表实现,本质上两边都是一对多映射

<mapper namespace="com.qcby.dao.RoleDao">
    <select id="findAll" resultMap="roleMap">
        SELECT r.*,u.username FROM USER u,user_role ur,role r 
        WHERE u.id = ur.UID AND ur.RID = r.ID
    </select>
    
    <resultMap type="role" id="roleMap">
        <id property="id" column="id"/>
        <result property="role_name" column="role_name"/>
        <result property="role_desc" column="role_desc"/>
        <collection property="users" ofType="user">
            <result property="username" column="username"/>
        </collection>
    </resultMap>
</mapper>

五、延迟加载详解

5.1 核心概念

延迟加载(懒加载)就是按需加载:先查询主表数据,当真正用到关联表数据时,才发送SQL去查询关联表。可以有效减少数据库不必要的多表关联查询,提升性能。

5.2 全局配置

<settings>
    <!-- 延迟加载总开关 -->
    <setting name="lazyLoadingEnabled" value="true"/>
    <!-- 关闭积极加载,改为按需加载:只有调用关联属性的getter时才触发查询 -->
    <setting name="aggressiveLazyLoading" value="false"/>
</settings>

5.3 验证方式

测试时只打印主表属性,观察控制台是否只输出一条SQL;当打印关联属性时,才会输出第二条查询SQL。

@Test
public void testFindAll2() throws Exception {
    List<Account> list = mapper.findAll2();
    for (Account account : list) {
        System.out.println(account.getMoney()); // 只查账户,不触发用户查询
        System.out.println(account.getUser().getUsername()); // 触发用户查询
    }
}

六、MyBatis 缓存机制

MyBatis提供两级缓存,用于减少数据库查询次数,提升性能。

6.1 一级缓存

  • 作用域:SqlSession 级别,同一个SqlSession内有效
  • 原理:第一次查询会将数据存入SqlSession的Map缓存中,第二次相同查询直接从缓存取
  • 失效场景:SqlSession关闭、手动调用clearCache()、执行增删改操作
  • 特点:默认开启,无需额外配置;缓存的是Java对象,两次查询得到同一个对象(地址相同)
@Test
public void testFirstLevelCache() {
    User user1 = mapper.findById(1); // 发SQL查数据库
    User user2 = mapper.findById(1); // 走缓存,不发SQL
    System.out.println(user1 == user2); // true,同一个对象
}

6.2 二级缓存

  • 作用域:Mapper 级别(同一个namespace),跨SqlSession共享
  • 开启步骤
    1. 全局配置开启cacheEnabled="true"(默认就是true)
    2. 对应Mapper.xml中添加<cache/>标签
    3. 实体类实现Serializable接口
  • 特点:缓存的是数据而非对象,每次从二级缓存取出数据时,会重新组装成新对象,因此对象地址不同
<!-- UserMapper.xml中开启二级缓存,3秒刷新一次 -->
<cache flushInterval="3000"/>
@Test
public void testSecondLevelCache() {
    SqlSession session1 = factory.openSession();
    UserMapper mapper1 = session1.getMapper(UserMapper.class);
    User user1 = mapper1.findById(1); // 查数据库,存入一级缓存
    session1.close(); // session关闭,一级缓存数据同步到二级缓存

    SqlSession session2 = factory.openSession();
    UserMapper mapper2 = session2.getMapper(UserMapper.class);
    User user2 = mapper2.findById(1); // 走二级缓存
    System.out.println(user1 == user2); // false,不同对象
}

6.3 一二级缓存对比

对比维度一级缓存二级缓存
作用域SqlSession级别Mapper(namespace)级别
存储内容Java对象零散数据
跨Session不支持支持
默认状态默认开启需手动配置开启
对象地址相同不同

七、注解开发方式

MyBatis也支持纯注解开发,无需XML映射文件,适合简单SQL场景。

7.1 注解版基础CRUD

public interface UserMapper {
    @Select("select * from user")
    @Results(id="userMap",value= {
            @Result(id=true,column="id",property="id"),
            @Result(column="username",property="username"),
            @Result(column="birthday",property="birthday"),
            @Result(column="sex",property="sex"),
            @Result(column="address",property="address")
    })
    List<User> findAll();

    @Select("select * from user where id = #{uid}")
    @ResultMap("userMap")
    User findById(Integer uid);

    @Insert("insert into user (username,birthday,sex,address) values (#{username},#{birthday},#{sex},#{address})")
    void insert(User user);

    @Update("update user set username = #{username},birthday=#{birthday},sex=#{sex},address=#{address} where id = #{id}")
    void update(User user);

    @Delete("delete from user where id = #{id}")
    void delete(Integer userId);
}

7.2 注解版关联查询与延迟加载

  • @One:对应XML的<association>,用于多对一
  • @Many:对应XML的<collection>,用于一对多
  • fetchType = FetchType.LAZY:开启延迟加载
public interface AccountMapper {
    /**
     * 多对一延迟加载
     */
    @Select("select * from account")
    @Results(value= {
            @Result(id=true,column="id",property="id"),
            @Result(column="uid",property="uid"),
            @Result(column="money",property="money"),
            @Result(property="user",javaType= User.class,column="uid",
                    one=@One(select="com.qcby.mapper1.UserMapper.findById",
                             fetchType= FetchType.LAZY))
    })
    List<Account> findAll2();

    /**
     * 一对多延迟加载:查询用户及名下账户
     */
    @Select("select * from user")
    @Results(id="userMap",value= {
            @Result(id=true,column="id",property="id"),
            @Result(column="username",property="username"),
            @Result(property="accounts",column="id",
                    many=@Many(select="com.qcby.mapper1.AccountMapper.findByUid",
                               fetchType=FetchType.LAZY))
    })
    List<User> findAll3();
}

7.3 注解版全局配置

注解开发的配置文件只需扫描Mapper接口包即可

<mappers>
    <package name="com.qcby.mapper1"/>
</mappers>

八、常见坑点与开发建议

  1. 参数传递:单个基本类型参数#{}里的名字可以任意;多个参数建议使用@Param注解或实体类封装
  2. 模糊查询:尽量使用concat('%',#{name},'%')代替${},避免SQL注入
  3. 延迟加载触发:只有调用关联属性的getter方法时才会触发加载,直接打印对象toString也会触发
  4. 二级缓存慎用:跨namespace操作时容易出现脏数据,查询多、修改少的场景才适合使用
  5. 复杂SQL建议用XML:注解适合简单单表CRUD,复杂动态SQL和多表关联用XML更易维护
  6. resultMap的id标签:主键字段建议用<id>标签映射,能显著提升映射性能

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值