Skip to content

MyBatis 最佳实践与性能优化

  总结 MyBatis 开发中的最佳实践、常见坑和性能优化技巧,帮助你在生产环境中用好 MyBatis。

一、开发规范

1.1 命名规范

项目结构:
  mapper/    → UserMapper.java      接口
  mapper/    → UserMapper.xml       映射文件
  entity/    → User.java            实体类
  dto/       → UserQuery.java       查询参数对象
  vo/        → UserVO.java          返回视图对象

方法命名:
  selectById        → 根据 ID 查询单个
  selectList        → 查询列表
  selectPage        → 分页查询
  insert            → 新增
  insertBatch       → 批量新增
  update            → 更新
  updateSelective   → 选择性更新(只更新非 null 字段)
  deleteById        → 根据 ID 删除
  deleteByIds       → 批量删除
  count             → 统计数量

1.2 推荐使用 DTO/Query 对象

java
// ❌ 不推荐:直接传多个参数
List<User> selectByCondition(@Param("name") String name,
                              @Param("age") Integer age,
                              @Param("email") String email);

// ✅ 推荐:使用查询对象
@Data
public class UserQuery {
    private String name;
    private Integer minAge;
    private Integer maxAge;
    private String email;
    private String orderBy;
    private String sortDirection;
}

List<User> selectByCondition(UserQuery query);

1.3 避免使用 Map 传参

java
// ❌ 不推荐:Map 传参,类型不安全,可读性差
Map<String, Object> params = new HashMap<>();
params.put("name", "张三");
params.put("age", 25);
userMapper.selectByMap(params);

// ✅ 推荐:使用对象或 @Param
UserQuery query = new UserQuery();
query.setName("张三");
query.setAge(25);
userMapper.selectByCondition(query);

二、性能优化

2.1 解决 N+1 查询问题

N+1 问题:查询 N 条主记录,然后循环查询关联记录,导致 N+1 次数据库查询

示例:
  查询 10 个用户 → 1 次查询
  循环查每个用户的订单 → 10 次查询(N+1)
xml
<!-- ❌ 不推荐:嵌套查询(N+1 问题) -->
<resultMap id="userMap" type="User">
    <collection property="orders" column="id"
                ofType="Order" select="selectOrdersByUserId"/>
</resultMap>

<!-- ✅ 推荐:嵌套结果映射(一条 SQL JOIN) -->
<resultMap id="userMap" type="User">
    <collection property="orders" ofType="Order">
        <id property="id" column="order_id"/>
        <result property="orderNo" column="order_no"/>
    </collection>
</resultMap>

<select id="selectUsersWithOrders" resultMap="userMap">
    SELECT u.*, o.id AS order_id, o.order_no
    FROM user u LEFT JOIN t_order o ON u.id = o.user_id
</select>

2.2 批量操作优化

java
// ❌ 不推荐:循环插入
for (User user : users) {
    userMapper.insert(user);  // 每次一条 SQL
}

// ✅ 推荐:批量插入
userMapper.insertBatch(users);  // 一条 SQL 插入多条

// ✅ 更优:使用 BATCH 执行器
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false);
try {
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    for (User user : users) {
        mapper.insert(user);
    }
    // 批量提交
    sqlSession.commit();
    // 清除缓存,防止内存溢出
    sqlSession.clearCache();
} finally {
    sqlSession.close();
}

2.3 合理使用缓存

适用二级缓存的场景:
  ✅ 读多写少的表(字典表、配置表)
  ✅ 单表查询
  ✅ 数据量小

不适用二级缓存的场景:
  ❌ 频繁更新的表
  ❌ 多表关联查询
  ❌ 对实时性要求高的数据
  ❌ 分布式环境(需要集中式缓存)

2.4 数据库连接池优化

yaml
spring:
  datasource:
    hikari:
      minimum-idle: 10              # 最小空闲连接
      maximum-pool-size: 20         # 最大连接数
      idle-timeout: 600000          # 空闲超时(10 分钟)
      max-lifetime: 1800000         # 连接最大存活时间(30 分钟)
      connection-timeout: 30000     # 连接超时
      connection-test-query: SELECT 1  # 连接测试

2.5 避免 SELECT *

xml
<!-- ❌ 不推荐 -->
<select id="selectAll" resultType="User">
    SELECT * FROM user
</select>

<!-- ✅ 推荐:只查需要的字段 -->
<select id="selectAll" resultType="User">
    SELECT id, name, age, email FROM user
</select>

<!-- ✅ 使用 SQL 片段复用 -->
<sql id="baseColumns">id, name, age, email, create_time</sql>
<select id="selectAll" resultType="User">
    SELECT <include refid="baseColumns"/> FROM user
</select>

2.6 分页优化

java
// ❌ 不推荐:RowBounds 逻辑分页
RowBounds rowBounds = new RowBounds(0, 10);
List<User> users = userMapper.selectAll(rowBounds);

// ✅ 推荐:PageHelper 物理分页
PageHelper.startPage(1, 10);
List<User> users = userMapper.selectAll();
PageInfo<User> pageInfo = new PageInfo<>(users);

三、常见坑与解决方案

3.1 事务不回滚

java
// ❌ 坑:Spring 默认只回滚 RuntimeException 和 Error
@Transactional
public void createOrder() {
    orderMapper.insert(order);
    throw new Exception("业务异常");  // 不会回滚!
}

// ✅ 解决:指定 rollbackFor
@Transactional(rollbackFor = Exception.class)
public void createOrder() {
    orderMapper.insert(order);
    throw new Exception("业务异常");  // 会回滚
}

3.2 大字段导致内存溢出

java
// ❌ 坑:一次性查询大量数据
List<User> users = userMapper.selectAll();  // 10 万条数据 → OOM

// ✅ 解决:流式查询
@Select("SELECT * FROM user")
@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 1000)
@ResultType(User.class)
void selectAllStream(ResultHandler<User> handler);

// 使用
userMapper.selectAllStream(resultContext -> {
    User user = resultContext.getResultObject();
    // 逐条处理
    process(user);
});

3.3 #{ } 和 ${ } 误用

xml
<!-- ❌ 坑:用 ${} 传参数值(SQL 注入风险) -->
<select id="selectByName" resultType="User">
    SELECT * FROM user WHERE name = '${name}'
</select>

<!-- ✅ 正确:用 #{} 传参数值 -->
<select id="selectByName" resultType="User">
    SELECT * FROM user WHERE name = #{name}
</select>

<!-- ✅ 正确:${} 只用于动态表名/列名 -->
<select id="selectByOrder" resultType="User">
    SELECT * FROM user ORDER BY ${orderBy} ${direction}
</select>

3.4 自增主键未返回

java
// ❌ 坑:插入后 id 为 null
User user = new User();
user.setName("张三");
userMapper.insert(user);
System.out.println(user.getId());  // null

// ✅ 解决:配置 useGeneratedKeys
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO user (name) VALUES (#{name})
</insert>

3.5 多参数未用 @Param

java
// ❌ 坑:多参数时不加 @Param,MyBatis 3.4.1 之前会报错
List<User> selectByAgeRange(int minAge, int maxAge);
// XML: #{minAge} 找不到参数

// ✅ 解决:加 @Param
List<User> selectByAgeRange(@Param("minAge") int minAge,
                             @Param("maxAge") int maxAge);

3.6 日期类型映射问题

java
// ❌ 坑:数据库 DATETIME 映射到 java.util.Date 可能丢失精度

// ✅ 解决:使用 LocalDateTime(Java 8+)
public class User {
    private LocalDateTime createTime;
}

// 需要添加依赖
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-typehandlers-jsr310</artifactId>
    <version>1.0.2</version>
</dependency>

3.7 空值处理

xml
<!-- ❌ 坑:update 时可能把字段更新为 null -->
<update id="update">
    UPDATE user SET name=#{name}, age=#{age} WHERE id=#{id}
</update>

<!-- ✅ 解决:动态更新,只更新非 null 字段 -->
<update id="updateSelective">
    UPDATE user
    <set>
        <if test="name != null">name = #{name},</if>
        <if test="age != null">age = #{age},</if>
    </set>
    WHERE id = #{id}
</update>

四、SQL 注入防护

java
// ① 使用 #{} 而不是 ${}
// ② 如果必须用 ${},做白名单校验

private static final Set<String> ALLOWED_COLUMNS =
    Set.of("id", "name", "age", "create_time");
private static final Set<String> ALLOWED_DIRECTIONS =
    Set.of("ASC", "DESC");

public List<User> selectByOrder(String orderBy, String direction) {
    if (orderBy == null || !ALLOWED_COLUMNS.contains(orderBy)) {
        throw new IllegalArgumentException("Invalid column: " + orderBy);
    }
    if (direction == null || !ALLOWED_DIRECTIONS.contains(direction.toUpperCase())) {
        throw new IllegalArgumentException("Invalid direction: " + direction);
    }
    return mapper.selectByOrder(orderBy, direction.toUpperCase());
}

五、速记总结

最佳实践速记:

  开发规范:
    命名要规范,查询用对象,别用 Map 传参
    多用 SQL 片段,少写 SELECT *

  性能优化:
    避免 N+1,多用 JOIN 嵌套结果
    批量操作用 BATCH,分页用 PageHelper
    缓存适可而止,连接池合理配置

  常见坑:
    事务要加 rollbackFor,大结果要流式查询
    #{ } 防注入,${ } 要白名单
    自增主键 useGeneratedKeys,多参数要 @Param

速记口诀:
  命名规范对象传,N+1 避免 JOIN 换,
  批量 BATCH 分页插件,坑点牢记不踩雷。

六、面试要点

问题答案要点
如何解决 N+1 问题?用嵌套结果映射(JOIN)代替嵌套查询
批量操作怎么优化?使用 ExecutorType.BATCH 或 foreach 批量 SQL
事务不回滚怎么办?加 rollbackFor = Exception.class
如何防止 SQL 注入?使用 #{} 而不是 ${},${} 必须做白名单校验