4-2 MyBatis 整合
概念解析
MyBatis vs JPA
| 特性 | MyBatis | JPA/Hibernate |
|---|---|---|
| SQL 控制 | 完全可控 | 自动生成 |
| 学习曲线 | 低 | 高 |
| 灵活性 | 高 | 低 |
| 维护性 | SQL 分散 | 统一管理 |
| 对象映射 | 手动配置 | 自动映射 |
核心组件
| 组件 | 作用 |
|---|---|
| SqlSessionFactory | 创建 SqlSession |
| SqlSession | 执行 SQL,管理事务 |
| Mapper | 接口 + 注解/XML |
| Executor | SQL 执行器 |
代码示例
1. 引入依赖
<!-- MyBatis Spring Boot Starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<!-- 分页插件 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
2. 配置
mybatis:
mapper-locations: classpath:mapper/**/*.xml
type-aliases-package: com.example.demo.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
cache-enabled: true
lazy-loading-enabled: true
aggressive-lazy-loading: false
# Spring Boot 3.x 使用新配置
settings:
default-fetch-size: 100
default-statement-timeout: 30
3. Mapper 接口
@Mapper
public interface UserMapper {
// 注解方式
@Select("SELECT * FROM t_user WHERE id = #{id}")
@Results({
@Result(property = "id", column = "id"),
@Result(property = "username", column = "username"),
@Result(property = "createTime", column = "create_time",
typeHandler = MybatisLocalDateTimeTypeHandler.class)
})
User findById(Long id);
@Insert("INSERT INTO t_user(username, password, email) " +
"VALUES(#{username}, #{password}, #{email})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);
@Update("UPDATE t_user SET username = #{username} WHERE id = #{id}")
int update(User user);
@Delete("DELETE FROM t_user WHERE id = #{id}")
int delete(Long id);
// 动态 SQL 方式(需要 XML)
List<User> search(UserQuery query);
// 分页查询
List<User> findPage(UserPage page);
}
4. XML Mapper
<?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.demo.mapper.UserMapper">
<!-- 结果映射 -->
<resultMap id="BaseResultMap" type="User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="password" column="password"/>
<result property="email" column="email"/>
<result property="status" column="status"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<!-- 动态 SQL -->
<select id="search" resultMap="BaseResultMap">
SELECT * FROM t_user
<where>
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
<if test="startTime != null">
AND create_time >= #{startTime}
</if>
<if test="endTime != null">
AND create_time <= #{endTime}
</if>
</where>
ORDER BY create_time DESC
</select>
<!-- 一对多查询 -->
<select id="findWithOrders" resultMap="UserOrderMap">
SELECT u.*, o.id as order_id, o.order_no
FROM t_user u
LEFT JOIN t_order o ON u.id = o.user_id
WHERE u.id = #{id}
</select>
<resultMap id="UserOrderMap" type="User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<collection property="orders" ofType="Order">
<id property="id" column="order_id"/>
<result property="orderNo" column="order_no"/>
</collection>
</resultMap>
</mapper>
5. 动态 SQL 标签
<!-- if -->
<select id="findByCondition" resultMap="BaseResultMap">
SELECT * FROM t_user
<where>
<if test="username != null">
AND username = #{username}
</if>
</where>
</select>
<!-- choose when otherwise -->
<select id="findByLevel" resultMap="BaseResultMap">
SELECT * FROM t_user
<where>
<choose>
<when test="level == 'VIP'">
AND status = 1
</when>
<when test="level == 'NORMAL'">
AND status = 0
</when>
<otherwise>
AND status IN (0, 1)
</otherwise>
</choose>
</where>
</select>
<!-- foreach -->
<select id="findByIds" resultMap="BaseResultMap">
SELECT * FROM t_user
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<!-- set -->
<update id="dynamicUpdate">
UPDATE t_user
<set>
<if test="username != null">username = #{username},</if>
<if test="email != null">email = #{email},</if>
</set>
WHERE id = #{id}
</update>
<!-- trim -->
<insert id="insertSelective" useGeneratedKeys="true" keyProperty="id">
INSERT INTO t_user
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="username != null">username,</if>
<if test="email != null">email,</if>
</trim>
<trim prefix="VALUES (" suffix=")" suffixOverrides=",">
<if test="username != null">#{username},</if>
<if test="email != null">#{email},</if>
</trim>
</insert>
6. 分页插件
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public PageInfo<User> getPage(int pageNum, int pageSize) {
// 紧跟查询的代码会被分页
PageHelper.startPage(pageNum, pageSize);
List<User> users = userMapper.findAll();
// PageInfo 包含分页信息
return new PageInfo<>(users);
}
public PageInfo<User> getPage(UserQuery query) {
PageHelper.startPage(query.getPage(), query.getSize());
// 排序
PageHelper.orderBy("create_time DESC");
List<User> users = userMapper.search(query);
return new PageInfo<>(users);
}
}
// 带参数的分页
public interface UserMapper {
// PageHelper 会自动拦截并设置分页参数
List<User> findAll();
}
7. 一对多查询
// Mapper 接口
public interface UserMapper {
User findUserWithOrders(Long id);
}
// XML
<select id="findUserWithOrders" resultMap="UserWithOrdersMap">
SELECT u.*, o.id as order_id, o.order_no, o.total_price
FROM t_user u
LEFT JOIN t_order o ON u.id = o.user_id
WHERE u.id = #{id}
</select>
<resultMap id="UserWithOrdersMap" type="User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<collection property="orders" ofType="Order">
<id property="id" column="order_id"/>
<result property="orderNo" column="order_no"/>
<result property="totalPrice" column="total_price"/>
</collection>
</resultMap>
常见坑点
⚠️ 坑 1:驼峰命名映射
# ❌ 未开启,数据库下划线无法映射到驼峰
mybatis:
configuration:
map-underscore-to-camel-case: false
# ✅ 开启
mybatis:
configuration:
map-underscore-to-camel-case: true
⚠️ 坑 2:Mapper 扫描不到
// ❌ Mapper 不在启动类包下
// com.example.demo.mapper.UserMapper
// SpringBootApplication 在 com.example.demo
// ✅ 方案1:指定扫描路径
@MapperScan(basePackages = {"com.example.demo.mapper", "com.example.other.mapper"})
// ✅ 方案2:确保 Mapper 在启动类同包或子包下
⚠️ 坑 3:分页插件不生效
// ❌ 查询后设置分页
List<User> users = userMapper.findAll();
PageHelper.startPage(page, size); // 太晚了!
// ✅ 正确顺序
PageHelper.startPage(page, size);
List<User> users = userMapper.findAll(); // 必须紧跟
PageInfo<User> pageInfo = new PageInfo<>(users);
面试题
Q1:MyBatis #{} 和 ${} 的区别?
参考答案:
| 特性 | #{} | ${} |
|---|---|---|
| 原理 | PreparedStatement 参数占位符 | 字符串拼接 |
| SQL 注入 | 防止 | 不防止 |
| 类型处理 | 自动类型转换 | 直接替换 |
| 使用场景 | 普通参数 | 动态表名/列名 |
<!-- #{} 预编译 -->
SELECT * FROM t_user WHERE id = ? -- 安全
<!-- ${} 直接拼接(谨慎使用) -->
SELECT * FROM ${tableName} -- 可能有注入风险
ORDER BY ${columnName} -- 动态排序
Q2:MyBatis 如何执行批量操作?
参考答案:
// 方式1:foreach
<insert id="batchInsert">
INSERT INTO t_user(username, email) VALUES
<foreach collection="users" item="u" separator=",">
(#{u.username}, #{u.email})
</foreach>
</insert>
// 方式2:ExecutorType.BATCH
try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
users.forEach(mapper::insert);
sqlSession.commit();
}
Q3:MyBatis 一级缓存和二级缓存?
参考答案:
| 缓存 | 作用域 | 生命周期 | 存储位置 |
|---|---|---|---|
| 一级缓存 | SqlSession | 与 SqlSession 相同 | 本地内存 |
| 二级缓存 | Mapper | 应用级别 | 内存/磁盘 |
<!-- 开启二级缓存 -->
<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/>
注意:Spring Boot + MyBatis 一级缓存默认开启,二级缓存需手动开启