Appearance
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 注入? | 使用 #{} 而不是 ${},${} 必须做白名单校验 |
