MyBatis动态SQL的另一种可能:用@SelectProvider实现复杂条件查询(附完整Builder类写法)
MyBatis动态SQL的另一种可能用SelectProvider实现复杂条件查询附完整Builder类写法在开发企业级应用时我们经常遇到需要根据多种条件动态组合查询的场景。传统的XML方式虽然功能强大但对于需要频繁调整查询逻辑的项目来说维护成本较高。而SelectProvider注解配合Builder模式提供了一种更灵活、更符合Java编码习惯的动态SQL解决方案。1. 为什么选择SelectProvider当查询条件超过3个且存在多种组合可能时XML中的if标签会迅速变得臃肿。上周我重构一个用户管理模块时发现一个查询方法竟然包含了17个条件判断这样的代码不仅难以维护性能也不理想。SelectProvider的核心优势在于类型安全SQL构建过程完全在Java代码中完成编译器可以帮助检查语法错误可复用性Builder类的方法可以被多个Mapper方法复用可测试性SQL构建逻辑可以单独进行单元测试IDE支持代码补全、重构工具都能正常使用// 典型的使用场景示例 SelectProvider(type UserQueryBuilder.class, method buildSearchQuery) ListUser searchUsers(Param(criteria) UserSearchCriteria criteria);2. 构建健壮的SQL Builder类2.1 基础Builder实现一个完整的Builder类应该处理以下关键问题条件判空处理安全的参数绑定动态排序支持分页参数集成public class UserQueryBuilder { public String buildSearchQuery(MapString, Object params) { UserSearchCriteria criteria (UserSearchCriteria) params.get(criteria); return new SQL() { { SELECT(user_id, user_name, email, create_time); FROM(users); if (StringUtils.isNotBlank(criteria.getName())) { WHERE(user_name LIKE CONCAT(%, #{criteria.name}, %)); } if (criteria.getStatus() ! null) { WHERE(status #{criteria.status}); } if (criteria.getStartDate() ! null) { WHERE(create_time #{criteria.startDate}); } if (criteria.getEndDate() ! null) { WHERE(create_time #{criteria.endDate}); } if (CollectionUtils.isNotEmpty(criteria.getOrderBy())) { ORDER_BY(criteria.getOrderBy().stream() .map(order - order.getField() order.getDirection()) .collect(Collectors.joining(, ))); } } }.toString(); } }2.2 高级技巧条件组合对于更复杂的场景我们可以引入条件组合逻辑public String buildAdvancedQuery(MapString, Object params) { UserSearchCriteria criteria (UserSearchCriteria) params.get(criteria); SQL sql new SQL() .SELECT(*) .FROM(users); // 构建条件列表 ListString conditions new ArrayList(); if (StringUtils.isNotBlank(criteria.getName())) { conditions.add(user_name LIKE CONCAT(%, #{criteria.name}, %)); } if (criteria.getMinAge() ! null) { conditions.add(age #{criteria.minAge}); } // 组合条件 if (!conditions.isEmpty()) { String whereClause conditions.stream() .collect(Collectors.joining( AND )); sql.WHERE(whereClause); } return sql.toString(); }3. 实战多条件用户搜索系统让我们通过一个完整的案例来展示如何实现一个生产级的多条件查询系统。3.1 定义查询条件DTO首先创建一个专门用于承载查询条件的DTOpublic class UserSearchCriteria { private String name; private Integer minAge; private Integer maxAge; private Date registerStartDate; private Date registerEndDate; private ListOrderSpecification orderBy; // 排序规范内部类 public static class OrderSpecification { private String field; private Direction direction; // 枚举定义排序方向 public enum Direction { ASC, DESC } // getters and setters } // getters and setters }3.2 实现完整的Builder类public class UserQueryBuilder { private static final String TABLE_NAME users; public String buildDynamicQuery(MapString, Object params) { UserSearchCriteria criteria (UserSearchCriteria) params.get(criteria); SQL sql new SQL().SELECT(*).FROM(TABLE_NAME); applyWhereClause(sql, criteria); applyOrderBy(sql, criteria); applyPagination(sql, params); return sql.toString(); } private void applyWhereClause(SQL sql, UserSearchCriteria criteria) { ListString conditions new ArrayList(); if (StringUtils.isNotBlank(criteria.getName())) { conditions.add(name LIKE CONCAT(%, #{criteria.name}, %)); } if (criteria.getMinAge() ! null) { conditions.add(age #{criteria.minAge}); } if (criteria.getMaxAge() ! null) { conditions.add(age #{criteria.maxAge}); } if (criteria.getRegisterStartDate() ! null) { conditions.add(register_date #{criteria.registerStartDate}); } if (criteria.getRegisterEndDate() ! null) { conditions.add(register_date #{criteria.registerEndDate}); } if (!conditions.isEmpty()) { sql.WHERE(conditions.stream().collect(Collectors.joining( AND ))); } } private void applyOrderBy(SQL sql, UserSearchCriteria criteria) { if (CollectionUtils.isNotEmpty(criteria.getOrderBy())) { String orderByClause criteria.getOrderBy().stream() .map(order - order.getField() order.getDirection()) .collect(Collectors.joining(, )); sql.ORDER_BY(orderByClause); } } private void applyPagination(SQL sql, MapString, Object params) { if (params.containsKey(offset) params.containsKey(limit)) { sql.OFFSET(#{offset}).LIMIT(#{limit}); } } }3.3 Mapper接口定义Mapper public interface UserMapper { SelectProvider(type UserQueryBuilder.class, method buildDynamicQuery) ListUser searchUsers(Param(criteria) UserSearchCriteria criteria, Param(offset) Integer offset, Param(limit) Integer limit); SelectProvider(type UserQueryBuilder.class, method buildDynamicQuery) long countUsers(Param(criteria) UserSearchCriteria criteria); }4. 性能优化与最佳实践在实际项目中我们还需要考虑以下关键点4.1 SQL注入防护虽然MyBatis的#{}语法已经提供了参数绑定功能但在动态构建SQL时仍需注意永远不要直接拼接用户输入到SQL语句中对于排序字段等必须动态指定的部分应该进行白名单校验// 安全的字段校验方法 private boolean isValidOrderField(String fieldName) { SetString validFields new HashSet(Arrays.asList( name, age, register_date )); return validFields.contains(fieldName); }4.2 缓存策略对于频繁执行的复杂查询可以考虑以下缓存优化二级缓存在Mapper级别配置缓存本地缓存对Builder生成的SQL进行缓存结果缓存对查询结果进行缓存// 使用Guava缓存Builder生成的SQL private static final CacheString, String SQL_CACHE CacheBuilder.newBuilder() .maximumSize(1000) .expireAfterWrite(1, TimeUnit.HOURS) .build(); public String buildCachedQuery(MapString, Object params) { String cacheKey generateCacheKey(params); try { return SQL_CACHE.get(cacheKey, () - buildDynamicQuery(params)); } catch (ExecutionException e) { return buildDynamicQuery(params); } }4.3 日志与调试为了方便调试复杂的动态SQL建议开启MyBatis的SQL日志为Builder类添加详细的日志输出实现SQL美化功能方便阅读// 日志配置示例 logging: level: com.example.mapper: DEBUG org.mybatis.spring.SqlSessionUtils: DEBUG在Builder类中添加日志public String buildDynamicQuery(MapString, Object params) { // ...构建逻辑... String finalSql sql.toString(); log.debug(Generated SQL: {}, finalSql); return finalSql; }5. 扩展应用场景SelectProvider的灵活性使其适用于许多特殊场景5.1 动态表名查询当需要根据条件查询不同表时public String queryFromDynamicTable(MapString, Object params) { String tableName (String) params.get(tableName); return new SQL() { { SELECT(*); FROM(tableName); WHERE(status ACTIVE); } }.toString(); }5.2 复杂Join查询对于需要动态关联表的场景public String buildJoinQuery(MapString, Object params) { boolean includeDepartment (boolean) params.get(includeDepartment); SQL sql new SQL() .SELECT(u.*) .FROM(users u); if (includeDepartment) { sql.SELECT(d.name as department_name) .LEFT_OUTER_JOIN(departments d ON u.department_id d.id); } return sql.toString(); }5.3 批量操作虽然MyBatis提供了Insert等注解但批量操作使用Provider更灵活public String buildBatchInsert(MapString, Object params) { ListUser users (ListUser) params.get(users); StringBuilder sb new StringBuilder(); sb.append(INSERT INTO users (name, age) VALUES ); for (int i 0; i users.size(); i) { if (i 0) sb.append(, ); sb.append((#{users[).append(i).append(].name}, ) .append(#{users[).append(i).append(].age})); } return sb.toString(); }