4-2 MyBatis 整合

4 阅读3分钟

4-2 MyBatis 整合

概念解析

MyBatis vs JPA

特性MyBatisJPA/Hibernate
SQL 控制完全可控自动生成
学习曲线
灵活性
维护性SQL 分散统一管理
对象映射手动配置自动映射

核心组件

组件作用
SqlSessionFactory创建 SqlSession
SqlSession执行 SQL,管理事务
Mapper接口 + 注解/XML
ExecutorSQL 执行器

代码示例

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 &lt;= #{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 一级缓存默认开启,二级缓存需手动开启