动态SQL与MyBatis动态SQL最佳实践

动态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 &lt;= #{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 &lt;= #{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>

总结

核心最佳实践清单:

  1. 安全性

    • 永远使用#{}进行参数绑定
    • ${}进行严格的白名单验证
    • 最小权限原则配置数据库用户
  2. 性能

    • 避免N+1查询
    • 合理使用索引
    • 控制批量操作大小
    • 选择性使用缓存
  3. 可维护性

    • 使用<sql>片段提高重用性
    • 保持SQL简洁,复杂逻辑考虑拆分
    • 统一的命名规范和代码风格
  4. 健壮性

    • 处理边界条件和异常情况
    • 完整的单元测试覆盖
    • 详细的日志记录
  5. 架构性

    • 根据业务场景选择合适的技术方案
    • 考虑未来的扩展性和变更成本
    • 遵循分层和模块化原则

动态SQL是强大的工具,但需要谨慎使用。在MyBatis中,合理运用动态SQL标签,结合良好的架构设计,可以在保证安全性和性能的前提下,大幅提高开发效率和代码质量。

相关推荐
瓦尔登湖懒羊羊2 小时前
TCP的自我介绍
后端
小周在成长2 小时前
MyBatis 动态SQL学习
后端
子非鱼9212 小时前
SpringBoot快速上手
java·spring boot·后端
我爱娃哈哈2 小时前
SpringBoot + XXL-JOB + Quartz:任务调度双引擎选型与高可用调度平台搭建
java·spring boot·后端
JavaGuide2 小时前
Maven 4 终于快来了,新特性很香!
后端·maven
开心就好20252 小时前
全面解析iOS应用代码混淆和加密加固方法与实践注意事项
后端
Thomas游戏开发2 小时前
分享一个好玩的:一次提示词让AI同时开发双引擎框架
前端·javascript·后端
龙门吹雪2 小时前
GO 语言处理多个布尔选项的实现方案
开发语言·后端·golang·布尔选项·标识位
毕设源码-钟学长2 小时前
【开题答辩全过程】以 基于Springboot vue肢体残疾人就业服务网站的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端