动态SQL与MyBatis动态SQL最佳实践
一、动态SQL通用最佳实践
1. 安全第一:防止SQL注入
java
// ❌ 危险的字符串拼接
String sql = "SELECT * FROM users WHERE name = '" + userInput + "'";
// ✅ 参数化查询(MyBatis自动处理)
String sql = "SELECT * FROM users WHERE name = #{name}";
// ✅ 动态表名/列名使用${}时,必须白名单验证
ORDER BY
<choose>
<when test="orderBy == 'name'">name</when>
<when test="orderBy == 'create_time'">create_time</when>
<otherwise>id</otherwise>
</choose>
2. 性能优化原则
xml
<!-- 避免N+1查询问题 -->
<select id="findUserWithOrders" resultMap="userWithOrdersMap">
SELECT u.*, o.*
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id = #{id}
<!-- 优于:先查用户,再循环查订单 -->
</select>
<!-- 合理使用索引 -->
<select id="searchUsers">
SELECT * FROM users
<where>
<!-- 将最可能过滤掉大量数据的条件放前面 -->
<if test="status != null">
AND status = #{status} <!-- status字段有索引 -->
</if>
<if test="name != null">
AND name LIKE #{name} <!-- name字段有索引 -->
</if>
<if test="description != null">
AND description LIKE #{description} <!-- 无索引,放最后 -->
</if>
</where>
</select>
3. 可维护性最佳实践
xml
<!-- 使用<sql>片段提高重用性 -->
<sql id="Base_Column_List">
id, name, email, create_time, update_time
</sql>
<sql id="Where_Clause">
<where>
<if test="id != null">AND id = #{id}</if>
<if test="name != null">AND name LIKE CONCAT('%', #{name}, '%')</if>
<if test="status != null">AND status = #{status}</if>
</where>
</sql>
<select id="selectByCondition" resultType="User">
SELECT
<include refid="Base_Column_List"/>
FROM users
<include refid="Where_Clause"/>
ORDER BY id DESC
</select>
二、MyBatis动态SQL高级最佳实践
1. 智能WHERE条件构建
xml
<!-- 使用<trim>替代<where>以获得更多控制权 -->
<select id="findUsers" resultType="User">
SELECT * FROM users
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="minAge != null">age >= #{minAge}</if>
<if test="maxAge != null">AND age <= #{maxAge}</if>
<if test="name != null">AND name LIKE #{name}</if>
<!-- 使用<bind>处理复杂逻辑 -->
<if test="searchKey != null">
<bind name="searchPattern" value="'%' + searchKey + '%'" />
AND (name LIKE #{searchPattern} OR email LIKE #{searchPattern})
</if>
</trim>
</select>
2. 批量操作优化
xml
<!-- MySQL批量插入优化 -->
<insert id="batchInsert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO users (name, age, email)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.name}, #{item.age}, #{item.email})
</foreach>
<!-- 单次插入数量建议在1000条以内 -->
</insert>
<!-- 批量更新优化(分批次执行) -->
<update id="batchUpdate">
<!-- 在Java代码中分批调用,避免SQL过长 -->
<foreach collection="list" item="item" separator=";">
UPDATE users
SET name = #{item.name}, update_time = NOW()
WHERE id = #{item.id}
</foreach>
</update>
3. 分页查询优化
xml
<!-- 使用PageHelper等分页插件 -->
<select id="selectByPage" resultType="User">
SELECT * FROM users
<where>
<if test="name != null">AND name LIKE #{name}</if>
<if test="status != null">AND status = #{status}</if>
</where>
ORDER BY create_time DESC
<!-- PageHelper会自动添加LIMIT子句 -->
</select>
<!-- 或者使用数据库特定的分页 -->
<select id="selectPage" resultType="User">
SELECT * FROM users
<include refid="Where_Clause"/>
ORDER BY id
<if test="offset != null and limit != null">
LIMIT #{offset}, #{limit}
</if>
</select>
4. 动态表名处理
java
// 使用Provider类处理复杂动态SQL
public class DynamicTableSqlProvider {
public String selectFromDynamicTable(Map<String, Object> params) {
String tableName = (String) params.get("tableName");
String year = (String) params.get("year");
// 表名白名单验证
List<String> validTables = Arrays.asList("orders", "users", "products");
if (!validTables.contains(tableName)) {
throw new IllegalArgumentException("Invalid table name");
}
// 动态表名(如分表场景)
if (year != null) {
tableName = tableName + "_" + year;
}
return new SQL()
.SELECT("*")
.FROM(tableName)
.WHERE("status = #{status}")
.toString();
}
}
三、性能优化策略
1. SQL执行计划优化
xml
<!-- 使用索引提示 -->
<select id="findActiveUsers">
SELECT /*+ INDEX(users idx_status_create) */ *
FROM users
<where>
status = 'ACTIVE'
<if test="createDate != null">
AND create_time >= #{createDate}
</if>
</where>
</select>
<!-- 避免SELECT * -->
<select id="findUserBasicInfo">
SELECT
id, name, email, avatar <!-- 只查询需要的字段 -->
FROM users
WHERE id = #{id}
</select>
2. 条件顺序优化
xml
<!-- 按照选择性高低排列条件 -->
<select id="searchOptimized">
SELECT * FROM orders
<where>
<!-- 高选择性条件放前面 -->
<if test="orderId != null">AND order_id = #{orderId}</if>
<if test="userId != null">AND user_id = #{userId}</if>
<if test="status != null">AND status = #{status}</if>
<!-- 低选择性条件放后面 -->
<if test="createTimeStart != null">AND create_time >= #{createTimeStart}</if>
<if test="createTimeEnd != null">AND create_time <= #{createTimeEnd}</if>
<if test="keyword != null">
<bind name="pattern" value="'%' + keyword + '%'"/>
AND (product_name LIKE #{pattern} OR memo LIKE #{pattern})
</if>
</where>
</select>
3. 缓存策略
xml
<!-- 二级缓存配置 -->
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
<!-- 特定查询缓存控制 -->
<select id="findConfig" resultType="Config" useCache="true" flushCache="false">
SELECT * FROM system_config WHERE config_key = #{key}
</select>
<update id="updateConfig" flushCache="true">
UPDATE system_config SET config_value = #{value} WHERE config_key = #{key}
</update>
四、代码组织与架构
1. 分层SQL管理
java
// 1. 基础CRUD - BaseMapper.xml
public interface BaseMapper<T> {
T selectById(@Param("id") Long id);
int insert(T entity);
int updateById(T entity);
int deleteById(@Param("id") Long id);
}
// 2. 业务查询 - UserMapper.xml
public interface UserMapper extends BaseMapper<User> {
List<User> selectByCondition(UserQuery query);
List<User> selectByIds(@Param("ids") List<Long> ids);
int batchInsert(@Param("list") List<User> users);
}
// 3. 复杂报表 - ReportMapper.xml
public interface ReportMapper {
List<SalesReport> selectSalesReport(ReportQuery query);
}
2. 使用SqlProvider处理复杂逻辑
java
// 复杂动态SQL建议使用Provider类
public class ComplexQuerySqlProvider {
public String buildDynamicReportSql(ReportQuery query) {
SQL sql = new SQL()
.SELECT("d.department_name",
"COUNT(e.id) as employee_count",
"AVG(e.salary) as avg_salary")
.FROM("departments d")
.LEFT_OUTER_JOIN("employees e ON d.id = e.department_id");
if (query.getMinSalary() != null) {
sql.WHERE("e.salary >= #{minSalary}");
}
if (query.getDepartmentIds() != null && !query.getDepartmentIds().isEmpty()) {
sql.WHERE("d.id IN (" +
query.getDepartmentIds().stream()
.map(id -> "#{departmentIds[" + id + "]}")
.collect(Collectors.joining(",")) +
")");
}
sql.GROUP_BY("d.id")
.HAVING("employee_count > 0");
if (query.getOrderBy() != null) {
sql.ORDER_BY(query.getOrderBy());
}
return sql.toString();
}
}
五、测试与调试
1. 单元测试
java
@SpringBootTest
@MybatisTest
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
void testDynamicQuery() {
UserQuery query = new UserQuery();
query.setName("John");
query.setStatus("ACTIVE");
query.setMinAge(18);
List<User> users = userMapper.selectByCondition(query);
// 验证生成的SQL
assertThat(users).isNotEmpty();
assertThat(users).allMatch(u ->
u.getName().contains("John") &&
u.getStatus().equals("ACTIVE") &&
u.getAge() >= 18
);
}
@Test
void testBoundaryConditions() {
// 测试空条件
UserQuery emptyQuery = new UserQuery();
List<User> allUsers = userMapper.selectByCondition(emptyQuery);
// 测试特殊值
UserQuery zeroAgeQuery = new UserQuery();
zeroAgeQuery.setMinAge(0);
List<User> zeroAgeUsers = userMapper.selectByCondition(zeroAgeQuery);
}
}
2. SQL日志调试
yaml
# application.yml
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 或者使用logback
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
logging:
level:
com.example.mapper: DEBUG
六、常见陷阱与解决方案
1. 空集合处理
xml
<!-- 错误:空集合会导致SQL语法错误 -->
<if test="ids != null">
AND id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</if>
<!-- 正确:检查集合大小 -->
<if test="ids != null and ids.size() > 0">
AND id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</if>
<!-- 或者使用OGNL函数 -->
<if test="@org.apache.commons.collections4.CollectionUtils@isNotEmpty(ids)">
AND id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</if>
2. 布尔值判断
xml
<!-- 错误:布尔值可能为false -->
<if test="active != null">
AND is_active = #{active}
</if>
<!-- 正确:明确判断 -->
<if test="active != null">
AND is_active = #{active, javaType=Boolean}
</if>
<!-- 或者 -->
<choose>
<when test="active == true">AND is_active = 1</when>
<when test="active == false">AND is_active = 0</when>
</choose>
3. 数字0的判断
xml
<!-- 问题:status=0时条件不生效 -->
<if test="status != null">
AND status = #{status}
</if>
<!-- 解决方案1:明确包含0 -->
<if test="status != null or status == 0">
AND status = #{status}
</if>
<!-- 解决方案2:使用包装类型 -->
public class Query {
private Integer status; // 不要用int
}
<!-- 解决方案3:特殊处理 -->
<if test="status != null or (status == 0 and includeZero == true)">
AND status = #{status}
</if>
七、架构建议
1. 动态SQL vs 存储过程
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 简单CRUD | MyBatis动态SQL | 易于维护和测试 |
| 复杂业务逻辑 | 存储过程 | 减少网络传输,性能更好 |
| 报表查询 | MyBatis动态SQL | 灵活,便于分页和排序 |
| 数据迁移 | 存储过程 | 事务控制更方便 |
2. 分库分表场景
xml
<!-- 分表查询示例 -->
<select id="selectFromShardingTable" resultType="Order">
SELECT * FROM
<choose>
<when test="userId % 4 == 0">orders_0</when>
<when test="userId % 4 == 1">orders_1</when>
<when test="userId % 4 == 2">orders_2</when>
<when test="userId % 4 == 3">orders_3</when>
</choose>
WHERE user_id = #{userId}
ORDER BY create_time DESC
</select>
3. 多租户架构
xml
<!-- 自动添加租户过滤条件 -->
<sql id="Tenant_Filter">
AND tenant_id = #{tenantId}
</sql>
<select id="selectByTenant" resultType="User">
SELECT * FROM users
<where>
<include refid="Where_Clause"/>
<include refid="Tenant_Filter"/>
</where>
</select>
总结
核心最佳实践清单:
-
安全性
- 永远使用
#{}进行参数绑定 - 对
${}进行严格的白名单验证 - 最小权限原则配置数据库用户
- 永远使用
-
性能
- 避免N+1查询
- 合理使用索引
- 控制批量操作大小
- 选择性使用缓存
-
可维护性
- 使用
<sql>片段提高重用性 - 保持SQL简洁,复杂逻辑考虑拆分
- 统一的命名规范和代码风格
- 使用
-
健壮性
- 处理边界条件和异常情况
- 完整的单元测试覆盖
- 详细的日志记录
-
架构性
- 根据业务场景选择合适的技术方案
- 考虑未来的扩展性和变更成本
- 遵循分层和模块化原则
动态SQL是强大的工具,但需要谨慎使用。在MyBatis中,合理运用动态SQL标签,结合良好的架构设计,可以在保证安全性和性能的前提下,大幅提高开发效率和代码质量。