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最基础也最容易踩坑的点,也是面试高频题:
-
#{参数}:预编译占位符(PreparedStatement)- SQL底层生成
?占位符,参数单独传递 - 自动给字符串添加单引号,自动转义特殊字符
- 彻底防止SQL注入,安全性高
- 适用场景:所有普通条件值(id、name、数值、模糊查询参数)
- 模糊查询推荐写法:
like concat('%',#{name},'%')
- SQL底层生成
-
${参数}:直接字符串拼接(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共享
- 开启步骤:
- 全局配置开启
cacheEnabled="true"(默认就是true) - 对应Mapper.xml中添加
<cache/>标签 - 实体类实现
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>
八、常见坑点与开发建议
- 参数传递:单个基本类型参数
#{}里的名字可以任意;多个参数建议使用@Param注解或实体类封装 - 模糊查询:尽量使用
concat('%',#{name},'%')代替${},避免SQL注入 - 延迟加载触发:只有调用关联属性的getter方法时才会触发加载,直接打印对象toString也会触发
- 二级缓存慎用:跨namespace操作时容易出现脏数据,查询多、修改少的场景才适合使用
- 复杂SQL建议用XML:注解适合简单单表CRUD,复杂动态SQL和多表关联用XML更易维护
- resultMap的id标签:主键字段建议用
<id>标签映射,能显著提升映射性能
1万+

被折叠的 条评论
为什么被折叠?



