1. MyBatis动态SQL实战精要
动态SQL是MyBatis最强大的特性之一,它允许我们在XML映射文件中构建灵活的SQL语句。在实际项目中,我经常遇到需要根据不同条件拼接SQL的场景,比如多条件搜索、批量操作等。下面分享几个核心用法和踩坑经验。
1.1 基础标签使用指南
<if>标签是最常用的动态元素,但很多人不知道它的判断逻辑是基于OGNL表达式的。比如判断字符串非空应该用test="name != null and name != ''",而不是简单的test="name"。我曾经在一个项目中因为漏掉了空字符串判断,导致查询条件失效。
xml复制<select id="findUsers" resultType="User">
SELECT * FROM users
WHERE 1=1
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
</select>
<choose>、<when>、<otherwise>组合相当于Java中的switch-case结构。在电商项目中,我常用它来处理多种排序需求:
xml复制<select id="findProducts" resultType="Product">
SELECT * FROM products
ORDER BY
<choose>
<when test="sortBy == 'price'">price</when>
<when test="sortBy == 'sales'">sales_count</when>
<otherwise>create_time DESC</otherwise>
</choose>
</select>
1.2 批量操作的最佳实践
<foreach>标签在批量插入时特别有用。但要注意数据库对单条SQL长度的限制。我曾在一次导入万级数据时遇到问题,后来改为分批处理:
xml复制<insert id="batchInsert">
INSERT INTO users (name, email) VALUES
<foreach collection="list" item="user" separator=",">
(#{user.name}, #{user.email})
</foreach>
</insert>
提示:MySQL默认的max_allowed_packet是4MB,大批量插入时建议每批500条左右。
1.3 动态SQL的性能陷阱
<where>标签会自动处理前缀AND/OR,但过度使用会导致SQL解析开销增加。在高并发场景下,我建议:
- 尽量保持SQL结构稳定,避免完全动态
- 对高频查询使用
<sql>片段重用 - 考虑使用MyBatis的缓存机制
xml复制<sql id="userColumns">id, name, email</sql>
<select id="findActiveUsers" resultType="User">
SELECT <include refid="userColumns"/>
FROM users
<where>
status = 'ACTIVE'
<if test="departmentId != null">
AND department_id = #{departmentId}
</if>
</where>
</select>
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 一对一关联查询的三种实现方式
2.1 嵌套结果映射
这是性能最好的方式,通过单条SQL完成关联查询。我在用户-档案关联中常用:
xml复制<resultMap id="userWithProfileMap" type="User">
<id property="id" column="user_id"/>
<result property="name" column="user_name"/>
<association property="profile" javaType="Profile">
<id property="id" column="profile_id"/>
<result property="address" column="address"/>
<result property="phone" column="phone"/>
</association>
</resultMap>
<select id="findUserWithProfile" resultMap="userWithProfileMap">
SELECT
u.id as user_id,
u.name as user_name,
p.id as profile_id,
p.address,
p.phone
FROM users u
LEFT JOIN profiles p ON u.id = p.user_id
WHERE u.id = #{id}
</select>
2.2 嵌套查询(N+1问题)
虽然写法简单,但存在著名的N+1查询问题。我曾在生产环境因为这个问题导致性能下降:
xml复制<resultMap id="userWithProfileMap2" type="User">
<association property="profile" column="id"
select="findProfileByUserId"/>
</resultMap>
<select id="findProfileByUserId" resultType="Profile">
SELECT * FROM profiles WHERE user_id = #{id}
</select>
注意:MyBatis 3.2.6+支持
@FetchMode(LAZY)注解解决部分N+1问题
2.3 注解方式实现
对于简单关联,使用注解更简洁。我在快速原型阶段常用:
java复制public interface UserMapper {
@Select("SELECT * FROM users WHERE id = #{id}")
@Results({
@Result(property = "id", column = "id"),
@Result(property = "profile", column = "id",
one = @One(select = "com.example.mapper.ProfileMapper.findByUserId"))
})
User findUserWithProfile(Long id);
}
3. 一对多关联查询实战技巧
3.1 集合映射的优化方案
处理用户-订单关系时,我推荐这种写法:
xml复制<resultMap id="userWithOrdersMap" type="User">
<id property="id" column="id"/>
<collection property="orders" ofType="Order">
<id property="id" column="order_id"/>
<result property="orderNo" column="order_no"/>
<result property="amount" column="amount"/>
</collection>
</resultMap>
<select id="findUserWithOrders" resultMap="userWithOrdersMap">
SELECT
u.*,
o.id as order_id,
o.order_no,
o.amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id = #{id}
</select>
3.2 分页查询的陷阱
一对多分页时会出现数据条数问题。我的解决方案是:
- 先查询主表分页
- 再批量查询关联数据
- 在内存中组装结果
java复制public PageInfo<User> findUsersWithOrders(int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<User> users = userMapper.findUsers();
// 批量查询订单
List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());
Map<Long, List<Order>> orderMap = orderMapper.findByUserIds(userIds)
.stream().collect(Collectors.groupingBy(Order::getUserId));
// 组装数据
users.forEach(user -> user.setOrders(orderMap.get(user.getId())));
return new PageInfo<>(users);
}
3.3 延迟加载的配置
在mybatis-config.xml中配置:
xml复制<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
这样只有在真正访问orders属性时才会触发查询。
4. 高级技巧与性能优化
4.1 动态表名与字段名
在一些多租户系统中,我使用<bind>标签处理动态表名:
xml复制<select id="findByTenant" resultType="User">
<bind name="tableName" value="'user_' + tenantId"/>
SELECT * FROM ${tableName}
WHERE status = #{status}
</select>
警告:使用${}有SQL注入风险,确保参数可信
4.2 二级缓存与关联查询
在关联查询中使用缓存要特别注意:
- 在映射文件中添加
<cache/>声明 - 对频繁变更的数据关闭缓存
- 考虑使用第三方缓存实现如Ehcache
xml复制<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
<resultMap id="userWithOrdersMap" type="User">
<collection property="orders" column="id"
select="findOrdersByUserId" fetchType="lazy"/>
</resultMap>
4.3 使用SQL构建器
对于复杂动态SQL,Java代码更易维护:
java复制public List<User> findUsers(UserQuery query) {
return new SQL() {{
SELECT("u.*");
FROM("users u");
if (query.getDepartmentId() != null) {
JOIN("dept_user du ON u.id = du.user_id");
WHERE("du.department_id = #{query.departmentId}");
}
if (query.getName() != null) {
WHERE("u.name LIKE CONCAT('%', #{query.name}, '%')");
}
ORDER_BY("u.create_time DESC");
}}.toString();
}
4.4 MyBatis插件开发
我开发过执行时间监控插件:
java复制@Intercepts({
@Signature(type= Executor.class, method="query",
args={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type= Executor.class, method="update",
args={MappedStatement.class, Object.class})
})
public class PerformanceInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
long start = System.currentTimeMillis();
Object result = invocation.proceed();
long end = System.currentTimeMillis();
System.out.println("执行耗时: " + (end - start) + "ms");
return result;
}
}
在配置文件中注册:
xml复制<plugins>
<plugin interceptor="com.example.plugin.PerformanceInterceptor"/>
</plugins>
5. 常见问题解决方案
5.1 XML中的特殊字符处理
在mapper.xml中,<、>等符号需要转义:
xml复制<select id="findRecentUsers">
SELECT * FROM users
WHERE create_time <= #{endDate}
AND create_time >= #{startDate}
</select>
或者使用CDATA区段:
xml复制<select id="findByCondition">
<![CDATA[
SELECT * FROM users
WHERE status <> 'DELETED'
]]>
</select>
5.2 枚举类型处理
推荐实现TypeHandler:
java复制public class StatusTypeHandler extends BaseTypeHandler<Status> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
Status parameter, JdbcType jdbcType) {
ps.setString(i, parameter.getCode());
}
// 其他方法实现...
}
5.3 分页插件整合
PageHelper的常见配置:
java复制// Spring Boot配置
@Configuration
public class MyBatisConfig {
@Bean
public PageInterceptor pageInterceptor() {
PageInterceptor interceptor = new PageInterceptor();
Properties props = new Properties();
props.setProperty("reasonable", "true");
props.setProperty("supportMethodsArguments", "true");
interceptor.setProperties(props);
return interceptor;
}
}
// 使用示例
public PageInfo<User> findUsers(int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<User> users = userMapper.selectAll();
return new PageInfo<>(users);
}
5.4 日志输出配置
在application.properties中:
properties复制logging.level.com.example.mapper=DEBUG
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
或者使用MyBatis Log Free插件,可以格式化输出的SQL。
