1. 项目背景与核心价值
在传统报表系统开发中,最让人头疼的就是处理五花八门的查询条件组合。每次业务方提出"能不能加个按部门筛选"、"能否同时按日期和金额范围查询"这类需求,开发人员就得修改SQL、调整接口参数。这个项目正是为了解决这个痛点而生——通过动态SQL和条件编排器的组合拳,让前端可以像搭积木一样自由组合查询条件。
我在金融行业做数据平台时,曾遇到过一个月内修改了17次同一张报表的查询逻辑。正是这种经历让我意识到,必须设计一套能灵活应对条件变化的解决方案。现在这套方案已经在生产环境稳定运行3年,支撑日均20万+的查询请求。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术架构设计
2.1 整体技术栈选型
选择SpringBoot作为基础框架是考虑到其快速启动和约定优于配置的特性。对于持久层,我们没有用JPA而是坚持MyBatis,原因很实际:
- 报表查询往往涉及复杂SQL和特定数据库函数(如Oracle的PIVOT)
- 需要精细控制执行计划,JPA生成的SQL难以优化
- 历史系统迁移成本低,大部分企业原有系统都使用MyBatis
动态SQL的实现采用了MyBatis原生的OGNL表达式,比拼接字符串更安全优雅。条件编排器则是自主设计的DSL(领域特定语言),通过JSON结构描述条件关系。
2.2 核心组件交互流程
code复制前端
│
└─提交 JSON条件描述
│
v
Controller层
│
└─验证&转换条件DSL
│
v
Service层
│
├─条件编排器解析
│ │
│ └─生成查询元数据
│ │
│ v
└─动态SQL构造器 ←─ MyBatis映射文件
│
v
数据库查询
这个流程的关键在于条件DSL的设计,我们采用了类似GraphQL的嵌套结构:
json复制{
"conditions": [
{
"field": "createTime",
"operator": "BETWEEN",
"value": ["2023-01-01", "2023-12-31"]
},
{
"logic": "OR",
"conditions": [
{"field": "department", "operator": "IN", "value": ["finance", "hr"]},
{"field": "amount", "operator": "GT", "value": 10000}
]
}
]
}
3. 动态SQL实现细节
3.1 MyBatis动态SQL进阶用法
基础的
xml复制<select id="queryReport" resultMap="reportResult">
SELECT * FROM financial_data
<where>
<foreach collection="paramGroups" item="group" separator=" OR ">
<trim prefix="(" suffix=")" prefixOverrides="AND |OR ">
<include refid="conditionFragment"/>
</trim>
</foreach>
<if test="securityFilter != null">
AND ${securityFilter} <!-- 注意$和#的区别 -->
</if>
</where>
ORDER BY
<choose>
<when test="sortField == 'amount'">abs_amount</when>
<otherwise>create_time</otherwise>
</choose>
</select>
几个关键点:
- 用
标签智能处理WHERE前缀 解决嵌套条件的括号问题 - 慎用${}防止SQL注入,只有可信白名单字段才用
3.2 条件编排器设计
核心类图:
code复制ConditionBuilder
├─ parse(json): QueryMeta
└─ validate(schema)
QueryMeta
├─ whereClause: String
├─ parameters: Map
└─ orderBy: String
FieldMetaRegistry
├─ fieldDefinitions: Map
└─ registerValidator()
实现时的注意事项:
- 字段白名单机制:只允许预注册的字段参与查询
- 操作符限制:不同字段类型允许的操作符不同(日期字段不能LIKE)
- 值转换器:处理前端传参的格式转换(如字符串转Date)
4. 性能优化实战
4.1 动态SQL缓存方案
测试发现频繁解析条件DSL会成为性能瓶颈。我们的解决方案是:
java复制// 使用Guava Cache缓存编译后的查询模板
LoadingCache<String, QueryTemplate> templateCache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(1, TimeUnit.HOURS)
.build(new TemplateLoader());
// 关键代码:生成缓存键时包含条件结构指纹
String cacheKey = md5(conditionJson + fieldMetaVersion);
4.2 分页优化技巧
当遇到深度分页时(如pageNum>1000),采用延迟关联优化:
xml复制<select id="queryLargeReport" resultMap="reportResult">
SELECT t1.* FROM financial_data t1
JOIN (
SELECT id FROM financial_data
<include refid="whereClause"/>
ORDER BY create_time DESC
LIMIT #{offset}, #{pageSize}
) t2 ON t1.id = t2.id
</select>
5. 安全防护措施
5.1 注入攻击防御
除了常规的预编译语句,我们还增加了:
-
字段级权限控制:用户只能查询有权限的字段
java复制// 在解析阶段过滤无权限字段 conditionJson.getConditions().removeIf( c -> !securityContext.hasFieldAccess(c.getField())); -
操作符白名单:限制只能用=、>、<等安全操作符
-
值范围校验:数字类型检查合理范围,字符串限制长度
5.2 防DoS策略
- 条件深度限制:禁止超过3层嵌套条件
- IN列表长度限制:最多允许100个值
- 复杂查询熔断:执行时间超过5秒自动取消
6. 生产环境踩坑记录
6.1 日期处理陷阱
初期遇到时区问题导致查询结果不准。解决方案:
java复制// 统一使用Java8时间API处理
DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
ZonedDateTime zdt = ZonedDateTime.parse(input, formatter);
Instant instant = zdt.toInstant();
6.2 空值处理哲学
对于前端传null值的不同语义:
- null可能表示"不筛选该字段"
- 也可能表示"筛选值为null的记录"
我们最终采用显式声明策略:
json复制{
"field": "approver",
"operator": "IS_NULL" // 或"IS_NOT_NULL"
}
7. 扩展性设计
7.1 自定义函数支持
通过在条件DSL中增加function节点实现:
json复制{
"field": "amount",
"operator": "GT",
"function": {
"name": "exchangeRate",
"params": ["USD", "CNY"]
}
}
对应的SQL生成:
sql复制amount > exchange_rate('USD', 'CNY')
7.2 多数据源适配
通过抽象条件解析器接口,可以支持不同数据库方言:
java复制public interface DialectAdapter {
String handlePagination(String originSql);
String handleFunction(String funcName, List<String> params);
}
// 针对MySQL的实现
@Component
@Profile("mysql")
public class MySqlAdapter implements DialectAdapter {
// 实现分页语法差异等
}
8. 监控与调优
8.1 慢查询监控
在动态SQL执行阶段埋点:
java复制@Around("execution(* com..*Mapper.*(..))")
public Object monitorQuery(ProceedingJoinPoint pjp) {
long start = System.currentTimeMillis();
try {
return pjp.proceed();
} finally {
long cost = System.currentTimeMillis() - start;
if (cost > 1000) { // 超过1秒记录
log.warn("Slow query: {} - {}ms",
pjp.getSignature(), cost);
// 上报到监控系统
}
}
}
8.2 执行计划分析
开发了自动EXPLAIN工具,当检测到复杂条件时:
sql复制EXPLAIN SELECT * FROM ... WHERE [动态条件]
将执行计划结果与查询参数一起记录,便于后续优化。
9. 前端集成方案
9.1 条件构造器UI
基于Vue实现的交互式条件生成器:
javascript复制// 动态渲染字段选择器
<select v-model="currentField" @change="updateOperators">
<option v-for="field in allowedFields" :value="field.name">
{{ field.label }}
</option>
</select>
// 根据字段类型显示不同值输入组件
<component :is="valueComponent" v-model="currentValue"/>
9.2 保存常用查询
用户可以将组合好的条件保存为模板:
java复制@Entity
public class QueryTemplate {
@Id
private String id;
@Lob
private String conditionJson;
private String createdBy;
// 其他元数据...
}
10. 最佳实践建议
-
字段设计原则:
- 预定义所有可查询字段的元数据
- 为每个字段设置合理的默认操作符
- 区分可排序字段和可筛选字段
-
性能调优步骤:
- 先限制返回字段(SELECT * 是大忌)
- 再优化WHERE条件(注意索引命中)
- 最后处理排序和分页
-
版本兼容策略:
- 条件DSL要有版本号字段
- 旧版查询自动适配新版规则
- 提供迁移工具转换历史查询模板
