恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

SpringJDBC条件进阶:JdbcTemplate动态查询与参数安全实践

  • 首页
  • 资讯中心
  • /
  • SpringJDBC条件进阶:JdbcTemplate动态查询与参数安全实践

相关资讯

Python GIL深度解析:多线程为何难用满CPU?多进程实战指南 2026/9/7 5:44:01
从义乳到游戏摇杆:开源硅胶成型技术栈全解析 2026/9/7 5:44:00
HC32F460 DDL固件库实战:从工程搭建到外设驱动避坑指南 2026/9/7 5:44:00

最新资讯

小程序K线图绘制实战:HQChart从集成到落地全指南
老照片批量修复实战:用Digielch Professional 4.5高效处理偏色划痕
ZEMAX 2008光学设计入门:老版本价值、破解版风险与双胶合透镜实操
EP100伺服系统完整方案:原理图+PCB+源码全解析
HLW8032与STM32的智能插座电量采集实战:从硬件到校准
MFC CListCtrl单元格编辑实战:从子类化到多类型编辑器

今日推荐

基于YOLOv8和PyQt5的麦穗稻穗检测识别系统设计与实现
UL 1642锂电池安全标准全解析:测试项目、认证流程与避坑指南
BS EN 13814-1-2019游乐设施安全标准:设计与制造核心要点解析

本周热门

超人会飞不算本事:系统稳定依赖清晰规则与边界设计
超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论
基于CNN的调制信号识别:MATLAB实现时频图分类实战

本月精选

自研推理加速器Redwood:两周内实现PyTorch模型高效部署的实战教程
V4L2摄像头采集实战:从camera_client.rar到出图全流程解析
从“谁发明了钢琴键”到知识问答智能体:RAG与记忆工程实践

SpringJDBC条件进阶:JdbcTemplate动态查询与参数安全实践

发布时间:2026/9/7 5:44:01
SpringJDBC条件进阶:JdbcTemplate动态查询与参数安全实践 1. 先理解“条件进阶”到底在解决什么问题SpringJDBC 是 Spring 框架里处理数据库访问的基础方案很多项目在没有引入 MyBatis、JPA 这类重量级 ORM 框架时都会直接用它来操作数据库。JDBC 本身写起来啰嗦SpringJDBC 通过JdbcTemplate把连接管理、异常转换、结果集映射这些重复工作封装掉了让开发者只需要关注 SQL 本身。但实际的业务查询从来不是“查全表”那么简单。用户搜索列表要按关键字过滤后台管理页面要根据时间段、状态、分类组合筛选统计报表可能要动态拼接查询条件。这些场景落到 SpringJDBC 上核心问题就变成一个查询条件不是固定的怎么在代码里安全、灵活、可维护地组装 SQL条件进阶这个主题重点就是解决这类问题。它不是讲某个新 API而是讲一套组合条件查询的写法、策略和防坑思路。适合看这篇文章的人也比较明确正在用JdbcTemplate还没引入 MyBatis但项目里的列表查询开始出现“一个方法写了好几段 if、拼接 SQL 越拼越长”这种情况。也可以说你已经在写动态条件查询但不确定有没有更稳的写法。最值得先关注的能力有三个条件参数怎么拼才不容易出错怎么避免 SQL 注入和参数占位符错位怎么让条件代码在项目变大之后还能维护如果能把这三点想清楚SpringJDBC 的动态查询就不会变成后期维护的负担。2. 先搭一个可以反复试验的基础环境条件进阶不是背 API而是要在代码里反复验证。所以我建议先把一个最小可运行的项目搭起来后面的示例都往这个环境里放。2.1 环境准备和依赖引入我用的环境是普通的 Java 工程Spring Boot 版本是 2.x 或 3.x 都可以核心依赖只需要两个spring-boot-starter-jdbc和对应数据库的驱动。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency这里不需要引入 MyBatis、JPA 或任何 ORM 框架因为我们要看的正是JdbcTemplate在纯 JDBC 层面的条件处理能力。如果你的项目不是 Spring Boot而是传统 Spring 工程那就手动配置一个DataSource和一个JdbcTemplateBean效果一样。2.2 准备一张测试表和测试数据条件查询要验证必须有一张带多种字段类型的表。我一般会建一张简单的商品表或者用户表字段要覆盖字符串精确匹配比如状态字段字符串模糊匹配比如名称搜索数值范围匹配比如价格或年龄区间时间范围匹配比如创建时间区间多个条件组合的情况我这里用一张用户表来做示例CREATE TABLE user_account ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, status TINYINT NOT NULL DEFAULT 1, age INT, created_at DATETIME ); INSERT INTO user_account (username, status, age, created_at) VALUES (zhangsan, 1, 25, 2024-01-01 10:00:00), (lisi, 1, 30, 2024-01-02 11:00:00), (wangwu, 0, 22, 2024-01-03 09:30:00), (zhaoliu, 1, 35, 2024-01-04 15:00:00);这张表覆盖了最常用的条件类型。后面所有示例都基于它来写你拿到项目里换成自己的业务表即可。2.3 理解 JdbcTemplate 查询方法的基本套路SpringJDBC 的查询方法整体思路是SQL 里用?占位符参数按顺序放到可变参数或参数数组里查询结果通过RowMapper或BeanPropertyRowMapper转成对象。单条件查询很简单Repository public class UserDao { Autowired private JdbcTemplate jdbcTemplate; public ListUserAccount findByStatus(int status) { String sql SELECT id, username, status, age, created_at FROM user_account WHERE status ?; return jdbcTemplate.query(sql, new BeanPropertyRowMapper(UserAccount.class), status); } }注意BeanPropertyRowMapper要求数据库列名和实体类属性名能匹配。如果列名是created_at属性名是createdAt默认情况下可能会映射不上。解决方式有两个SQL 里给列起别名或者把BeanPropertyRowMapper配置成下划线转驼峰。Spring Boot 2.x 之后BeanPropertyRowMapper默认是支持下划线转驼峰的但为了保险起见我会在 SQL 里显式写别名避免不同版本行为不一致。单条件没有难度真正的难点在“多个条件同时出现并且每个条件都可能为空”。3. 多条件动态查询的常见实现对比动态条件查询最简单的想法是传入一个实体对象里面有值就拼条件没值就跳过。这个思路没问题问题是怎么拼。3.1 方案一字符串直接拼接最不推荐很多项目刚开始会写出这种代码public ListUserAccount searchUsers(String username, Integer status, Integer minAge, Integer maxAge) { StringBuilder sql new StringBuilder(SELECT id, username, status, age, created_at FROM user_account WHERE 11); if (StringUtils.hasText(username)) { sql.append( AND username LIKE %).append(username).append(%); } if (status ! null) { sql.append( AND status ).append(status); } // ... 其他条件 return jdbcTemplate.query(sql.toString(), new BeanPropertyRowMapper(UserAccount.class)); }这段代码能跑但有两个硬伤。第一SQL 注入风险。username如果是用户输入直接拼进 SQL恶意输入可能改变语句语义。有人说“内部系统不用管”但数据安全不该赌。第二可维护性差。条件一多SQL 字符串交错在 if 里面后期想找一个字段对应的查询逻辑眼睛要看很久。这个方案只适合一次性临时脚本不适合正式项目的数据访问层。3.2 方案二参数占位符配合条件判断推荐正确做法是SQL 的WHERE部分动态拼接但参数值永远通过占位符传递并且传入的参数列表和 SQL 里的?必须一一对应。public ListUserAccount searchUsers(String username, Integer status, Integer minAge, Integer maxAge) { StringBuilder sql new StringBuilder(SELECT id, username, status, age, created_at FROM user_account WHERE 11); ListObject params new ArrayList(); if (StringUtils.hasText(username)) { sql.append( AND username LIKE ?); params.add(% username %); } if (status ! null) { sql.append( AND status ?); params.add(status); } if (minAge ! null) { sql.append( AND age ?); params.add(minAge); } if (maxAge ! null) { sql.append( AND age ?); params.add(maxAge); } sql.append( ORDER BY id DESC); return jdbcTemplate.query(sql.toString(), new BeanPropertyRowMapper(UserAccount.class), params.toArray()); }这个写法有四个关键点SQL 里的?只负责占位不写具体值参数全部放进params列表params.toArray()作为查询参数传入每个条件追加 SQL 的同时必须同步添加参数顺序不能乱WHERE 11这种写法看着有点怪但它提供了很实际的便利后续每个条件都可以直接用AND开头不用判断当前是不是第一个条件。虽然在极大数据量的极端场景下数据库优化器可能对11有一定处理成本但现代数据库基本都能识别并优化掉。如果实在不喜欢也可以用ListString收集条件片段最后用String.join( AND , conditions)组装。3.3 方案三条件对象封装当查询条件太多比如超过五个字段方法参数会越来越长。这时候我习惯单独定义一个查询条件类。public class UserQuery { private String username; private Integer status; private Integer minAge; private Integer maxAge; private String startDate; private String endDate; // getter/setter 省略 }然后 DAO 方法只接收这个查询对象public ListUserAccount searchUsers(UserQuery query) { StringBuilder sql new StringBuilder(SELECT id, username, status, age, created_at FROM user_account WHERE 11); ListObject params new ArrayList(); if (StringUtils.hasText(query.getUsername())) { sql.append( AND username LIKE ?); params.add(% query.getUsername() %); } if (query.getStatus() ! null) { sql.append( AND status ?); params.add(query.getStatus()); } // ... 其他 return jdbcTemplate.query(sql.toString(), new BeanPropertyRowMapper(UserAccount.class), params.toArray()); }这样做的最大好处是以后新增查询条件时只需要往条件类里加字段再在 DAO 方法里加一段 if不用改方法签名调用方也更清晰。3.4 三种方案对比方案安全性可维护性适用场景字符串直接拼接低存在注入风险差条件杂乱临时脚本不推荐用于正式项目参数占位符 if高参数化查询中等小型项目、条件数量少、逻辑清晰条件对象 if高参数化查询较好条件多、查询复杂、后续要扩展比较下来我个人的建议是正式项目默认选择方案三。哪怕当前只有两三个条件也值得用条件对象。因为条件查询几乎是必然扩展的今天两个条件下个月可能就是五个。4. 条件进阶的实用模式基础的 if 追加条件很容易理解但这个思路在真实项目里会碰到一些更具体的问题。下面几个模式是我在项目里反复用到的。4.1 模糊查询的写法差异模糊查询最常见的写法是LIKE %keyword%。在 SpringJDBC 里不能把%直接写进 SQL 的参数位置而是要拼到参数值里。有人会写sql.append( AND username LIKE ?); params.add(% username %);这是正确的。但有个细节值得注意如果用户搜索的关键字本身包含%或_这两个字符在 LIKE 里是通配符可能导致查询结果超出预期。如果需要严格按字面量匹配就要做转义处理用ESCAPE关键字指定转义字符。String keyword username.replace(!, !!) .replace(%, !%) .replace(_, !_); sql.append( AND username LIKE ? ESCAPE ! ); params.add(% keyword %);这个需求不是每个项目都有但如果你的业务允许用户输入特殊字符做搜索这块就要注意。普通的关键字搜索不处理也不会有大问题最多是搜索结果多几条。4.2 in 条件怎么动态处理动态查询经常遇到IN条件比如按多个 ID 查询或者按多个状态查询。如果传入的是一个集合public ListUserAccount findByStatuses(ListInteger statuses) { if (statuses null || statuses.isEmpty()) { return Collections.emptyList(); } StringBuilder sql new StringBuilder( SELECT id, username, status, age, created_at FROM user_account WHERE status IN ( ); // 生成占位符 for (int i 0; i statuses.size(); i) { if (i 0) { sql.append(, ); } sql.append(?); } sql.append()); return jdbcTemplate.query(sql.toString(), new BeanPropertyRowMapper(UserAccount.class), statuses.toArray()); }这里需要注意两点IN列表为空时不能直接执行否则 SQL 会变成WHERE status IN ()语法错误。可以先做空集合判断返回空结果。IN列表元素数量特别多时数据库可能性能下降。一般超过 1000 个 ID我会考虑分批查询或者改用临时表关联。4.3 日期范围查询时间范围查询是列表页里最常见的条件之一。传入的开始时间和结束时间在 SQL 里用和而不是和这样更精确。通常开始时间取当天零点结束时间取下一天的零点。if (query.getStartDate() ! null) { sql.append( AND created_at ?); params.add(query.getStartDate()); } if (query.getEndDate() ! null) { sql.append( AND created_at ?); params.add(query.getEndDate()); }这里要注意一个常见错误如果结束时间直接用2024-01-04可能丢失当天的数据因为2024-01-04 15:00:00大于2024-01-04 00:00:00。所以在传参时要么把结束日期转换成次日零点要么用户在页面上选择日期时后端自动拼接时间。4.4 排序字段动态化查询条件里经常要支持排序但排序字段不能直接用占位符因为 SQL 里ORDER BY ?会被当成字符串常量处理不会解析成列名。正确的做法是排序字段用白名单校验排序方式单独判断。String orderBy id; // 默认排序 if (username.equals(query.getOrderBy())) { orderBy username; } else if (age.equals(query.getOrderBy())) { orderBy age; } // 其他字段同理 String direction DESC; if (asc.equalsIgnoreCase(query.getOrderDirection())) { direction ASC; } sql.append( ORDER BY ).append(orderBy).append( ).append(direction);这里绝对不能直接把用户传的排序字段拼进 SQL否则等于给拼接注入留了后门。白名单是简单可靠的方案。4.5 分页查询SpringJDBC 并没有像 MyBatis PageHelper 那样自带分页插件分页逻辑需要自己写。MySQL 的分页比较简单使用LIMIT ? OFFSET ?int pageNum 1; int pageSize 10; int offset (pageNum - 1) * pageSize; sql.append( LIMIT ? OFFSET ?); params.add(pageSize); params.add(offset); ListUserAccount list jdbcTemplate.query(sql.toString(), new BeanPropertyRowMapper(UserAccount.class), params.toArray());同时还需要一条 COUNT 查询来拿总条数String countSql SELECT COUNT(*) FROM user_account WHERE 11; // 同样追加条件部分但不需要排序和分页 Integer total jdbcTemplate.queryForObject(countSql, Integer.class, params.toArray());这里有一个实践细节COUNT 查询和列表查询的条件部分一样但排序、分页部分不能加。如果你用方案三的条件对象最好把“构建条件 SQL 和参数”抽成一个公共方法列表查询和 COUNT 查询都调用它。这样可以保证两条 SQL 条件完全一致。private SqlAndParams buildWhereSql(UserQuery query) { StringBuilder sql new StringBuilder( WHERE 11); ListObject params new ArrayList(); // 各种条件判断 return new SqlAndParams(sql.toString(), params.toArray()); }4.6 条件过多时的可读性优化如果一张表有十几个字段可以作为查询条件一个方法里堆十段 if 会显得很笨重。这时候可以引入策略模式或者更简单的方式是提取私有方法。比如把每个条件判断独立成方法或者在条件对象里增加一个“转成 SQL 片段”的处理逻辑。不过在小项目里我认为把条件判断控制在同一个方法里反而更直观只要 SQL 和参数都在同一个地方维护就不容易错位。拆得太散反而增加理解成本。5. 参数管理与 SQL 组装顺序的坑动态条件查询里最容易出的问题不是 SQL 语法错误而是参数顺序错位。5.1 为什么参数顺序这么容易乱因为 SQL 是字符串拼接出来的参数是不断 add 到 List 里的。两者分开写一旦中间加了一个条件或者调整了某个条件的顺序SQL 里的?和数组里的参数就可能对不上。举个真实例子。一开始代码是这样sql.append( AND status ?); params.add(status); sql.append( AND username LIKE ?); params.add(% username %);后来有人加了年龄条件但加在了中间sql.append( AND status ?); params.add(status); sql.append( AND age ?); params.add(minAge); // 位置没问题 sql.append( AND username LIKE ?); params.add(% username %);如果追加条件和添加参数的代码不是紧紧挨着的中间隔了其他逻辑就容易出现参数添加顺序和?出现顺序不一致的情况。运行时会报Parameter index out of range或者查出来的数据莫名其妙。5.2 怎么避免参数错位我的习惯是每次追加 SQL 片段紧接着就添加参数中间不要插入其他无关代码。这样每组 SQL 片段和参数形成一个小的“原子操作”可读性最高。有条件的项目可以考虑写一个简单的工具类或内部类把 SQL 片段和参数绑定在一起public class SqlParams { private final StringBuilder sql new StringBuilder(); private final ListObject params new ArrayList(); public void append(String condition, Object... values) { sql.append( ).append(condition); if (values ! null) { Collections.addAll(params, values); } } public String getSql() { return sql.toString(); } public Object[] getParams() { return params.toArray(); } }使用起来更方便SqlParams sp new SqlParams(); sp.append(SELECT * FROM user_account WHERE 11); if (StringUtils.hasText(username)) { sp.append(AND username LIKE ?, % username %); } if (status ! null) { sp.append(AND status ?, status); } jdbcTemplate.query(sp.getSql(), new BeanPropertyRowMapper(UserAccount.class), sp.getParams());这种小工具也不复杂写一次可以复用到很多 DAO 里。5.3 每批条件的先后顺序会影响性能在少数情况下条件的先后顺序会影响 SQL 执行效率。假设业务上大部分查询都带有status 1少部分查询按用户名模糊搜索。对数据库优化器来说如果status字段有索引把它写在前面更利于快速过滤。如果username没有索引LIKE %xxx%会全表扫描把它写在后面可以让前面的条件先缩小结果集再减少 LIKE 扫描的行数。这是个经验判断不是绝对原则。你可以在测试环境观察执行计划再决定条件顺序。但在绝大多数中小规模数据上条件顺序对性能的影响不明显可以先把代码可读性放在第一位。6. 查询性能与安全边界动态条件查询写到一定程度性能和安全性就成了绕不开的话题。6.1 索引与统计动态查询最大的问题是条件组合多了索引不一定能覆盖所有查询路径。比如用户表上建了(status, age)的联合索引那么WHERE status ? AND age ?可以走索引。但如果用户只按username查询这个索引就帮不上忙。建议做法很简单统计业务中高频出现的条件组合针对高频组合建联合索引低频组合不建索引避免索引过多拖慢写入这个环节不建议一开始就做优化而是等项目跑起来、慢查询出现后再调整。SpringJDBC 的动态查询不会改变数据库的索引选择逻辑索引该怎么建还是怎么建。6.2 数据量大时的 limit 还是必须加如果一个动态查询没有带分页又不限制最大返回条数数据库可能一次返回几十万行JVM 内存占用飙升。我一般会在查询方法层面限制一个最大返回行数。比如列表查询必须分页或者后台数据导出类任务也要分批处理不能一条 SQL 把全表拉出来。if (pageSize 0 || pageSize 100) { pageSize 20; }这个限制写在 DAO 层或 Service 层都可以目的就是防止调用方误传了过大的分页参数。6.3 防止 SQL 注入的完整思路前面提到了参数占位符这里再补几个容易忽略的点不要用拼接方式处理ORDER BY、GROUP BY、LIMIT里的字段名要白名单校验不要用参数占位符处理表名、列名因为占位符只能替换值不能替换结构如果必须动态传表名或列名一定要硬编码一个白名单不能直接用用户输入假设你的系统有按不同业务类型查不同表的需求正确做法是MapString, String tableMap new HashMap(); tableMap.put(user, user_account); tableMap.put(order, order_info); String tableName tableMap.get(bizType); if (tableName null) { throw new IllegalArgumentException(不支持的业务类型); }这样即使bizType是用户传的也只能映射到预定义的几张表不会注入其他地方。7. 常见错误与排查方法动态条件查询写多了会遇到几个典型的报错和异常现象。我按排查顺序整理一遍。7.1 报错“Column xxx not found”或“BadSqlGrammarException”现象查询执行时报 SQL 语法错误或者列名不存在。排查顺序先看 SQL 字符串最终是什么。最简单的方式是在 DAO 方法里临时打印sql.toString()。确认列名是否写错数据库里到底是created_at还是create_time。确认表名是否写错有没有加上库名前缀导致误匹配。确认条件片段拼接的位置对不对尤其是AND或OR前后有没有缺空格拼接出来的 SQL 可能变成usernamezhangsan。这类问题 90% 以上是拼写或空格导致不是框架问题。7.2 报错“Parameter index out of range (2 number of parameters, which is 1)”现象SQL 里有两个?但只传了一个参数。排查顺序数一下最终 SQL 里到底有几个?。再看params列表长度是多少。重点检查是不是某个条件进去了但参数没有 add或者某个参数 add 了但 SQL 片段没拼上去。检查有没有条件判断和参数添加逻辑之间插入了return或异常抛出。这类错误几乎都是条件判断和参数列表不同步导致用前面提到的SqlParams工具类能有效避免。7.3 查询结果为空但数据明明存在现象条件查询返回空列表但直接用 SQL 查数据库有数据。排查顺序先检查参数值传进来到底是什么。比如status传入的是null那if (status ! null)就不会拼条件查出来可能是全表不是空。如果查出来是空看是不是status被传了数字 0而业务上 0 是启用还是停用搞反了。检查日期范围是否包含边界值。比如条件用了 startDate而数据里的时间正好等于startDate就会被排除。检查模糊查询的%位置。%拼在左边只能匹配前匹配拼在右边是后匹配两边都有才是包含匹配。确认使用的列名和值的数据类型是否匹配比如数据库字段是VARCHAR但前端传入整数可能会有隐式转换问题。7.4 查询执行很慢现象数据量不大但查询耗时明显偏高。排查顺序查看执行计划确认是否走索引。EXPLAIN SELECT ...是最直接的方式。确认条件里有没有对索引列做函数运算。比如WHERE DATE(created_at) ?会导致索引失效应该改成created_at ? AND created_at ?。检查是不是LIKE %keyword%导致的全文扫描。这种写法索引通常用不上数据量大时只能接受全表扫描或者引入专门的搜索方案。确认是否查询返回了大量不需要的列。有时候SELECT *会把超大字段也拉出来影响性能改成只查需要的列。7.5 并发环境下查询数据不一致动态条件查询本身不存在线程安全问题因为每次查询都会创建新的StringBuilder和List没有共享的可变状态。但如果有人在 DAO 里用了类级别的SimpleDateFormat并发下可能出现时间格式错乱这个坑更隐蔽。我的建议是如果 JDK 8 及以上用LocalDateTime系列配合DateTimeFormatter线程安全不要在 DAO 或 Service 内部定义可变的 SimpleDateFormat 静态字段8. 一个完整示例多条件用户列表查询把前面的思路综合起来写一个完整的 DAO 示例包含条件对象、动态 SQL、分页、排序、COUNT 查询。这可以当作你自己项目的代码模板。8.1 实体类和条件对象public class UserAccount { private Long id; private String username; private Integer status; private Integer age; private LocalDateTime createdAt; // getter/setter 省略 }public class UserQuery { private String username; private Integer status; private Integer minAge; private Integer maxAge; private LocalDateTime startDate; private LocalDateTime endDate; private String orderBy id; private String orderDirection DESC; private Integer pageNum 1; private Integer pageSize 20; // getter/setter 省略 }8.2 构建条件 SQLprivate String buildWhere(UserQuery query, ListObject params) { StringBuilder sql new StringBuilder( WHERE 11); if (StringUtils.hasText(query.getUsername())) { sql.append( AND username LIKE ?); params.add(% query.getUsername() %); } if (query.getStatus() ! null) { sql.append( AND status ?); params.add(query.getStatus()); } if (query.getMinAge() ! null) { sql.append( AND age ?); params.add(query.getMinAge()); } if (query.getMaxAge() ! null) { sql.append( AND age ?); params.add(query.getMaxAge()); } if (query.getStartDate() ! null) { sql.append( AND created_at ?); params.add(query.getStartDate()); } if (query.getEndDate() ! null) { sql.append( AND created_at ?); params.add(query.getEndDate()); } return sql.toString(); }注意这个方法和列表查询、COUNT 查询共用所以不能包含排序和分页。8.3 列表查询 分页 排序public ListUserAccount searchUsers(UserQuery query) { ListObject params new ArrayList(); StringBuilder sql new StringBuilder( SELECT id, username, status, age, created_at FROM user_account ); sql.append(buildWhere(query, params)); String orderBy id; if (username.equals(query.getOrderBy())) { orderBy username; } else if (age.equals(query.getOrderBy())) { orderBy age; } String direction DESC; if (asc.equalsIgnoreCase(query.getOrderDirection())) { direction ASC; } sql.append( ORDER BY ).append(orderBy).append( ).append(direction); int pageNum (query.getPageNum() null || query.getPageNum() 1) ? 1 : query.getPageNum(); int pageSize (query.getPageSize() null || query.getPageSize() 1) ? 20 : query.getPageSize(); sql.append( LIMIT ? OFFSET ?); params.add(pageSize); params.add((pageNum - 1) * pageSize); return jdbcTemplate.query(sql.toString(), new BeanPropertyRowMapper(UserAccount.class), params.toArray()); }8.4 COUNT 查询public int countUsers(UserQuery query) { ListObject params new ArrayList(); String sql SELECT COUNT(*) FROM user_account buildWhere(query, params); Integer count jdbcTemplate.queryForObject(sql, Integer.class, params.toArray()); return count null ? 0 : count; }8.5 为什么 COUNT 和列表查询要分开有人会问能不能一次查询就把总数和列表都拿到在 SpringJDBC 里没有这种内置能力需要分别执行。但这里有一个实践技巧如果你不需要在页面上显示“总共多少条”只是想“取前 20 条”那可以直接执行列表查询不需要 COUNT。如果业务必须显示总数才需要两条 SQL。这个设计不是框架限制而是职责分离的原则。COUNT 查询只关心总数列表查询关心数据和排序两个方法共享同一个buildWhere保证条件一致但各自负责各自的返回结果。8.6 这个示例在真实项目里怎么用Service 层调用示例Service public class UserService { Autowired private UserDao userDao; public PageResultUserAccount pageQuery(UserQuery query) { ListUserAccount list userDao.searchUsers(query); int total userDao.countUsers(query); return new PageResult(total, list); } }PageResult就是一个简单的分页结果封装里面放 total 和 list。如果后续要接入更多筛选条件比如按角色、按用户来源、按最近登录时间只需要改UserQuery和buildWhere。9. 进阶条件组合复杂了怎么办前面的方案适合条件数量在 10 个以内。如果条件更多或者条件之间存在复杂的业务关系比如 A 或 B 同时满足、按分组动态拼接括号就需要更进一步的处理。9.1 用 Specification 思想封装条件很多框架都有类似 JPA Criteria API 那样的条件抽象。SpringJDBC 没有内置这套东西但可以自己做一个轻量的条件构建器。核心思路是定义一个接口public interface Condition { String toSql(ListObject params); }然后每个条件实现这个接口public class SimpleCondition implements Condition { private final String sql; private final Object[] values; public SimpleCondition(String sql, Object... values) { this.sql sql; this.values values; } Override public String toSql(ListObject params) { if (values ! null) { Collections.addAll(params, values); } return sql; } }再提供一个组合类支持and和orpublic class CompositeCondition implements Condition { private final ListCondition conditions new ArrayList(); private final String operator; // AND 或 OR public CompositeCondition(String operator) { this.operator operator; } public CompositeCondition add(Condition condition) { conditions.add(condition); return this; } Override public String toSql(ListObject params) { if (conditions.isEmpty()) { return 11 ; } StringBuilder sb new StringBuilder((); for (int i 0; i conditions.size(); i) { if (i 0) { sb.append( ).append(operator).append( ); } sb.append(conditions.get(i).toSql(params)); } sb.append()); return sb.toString(); } }使用时CompositeCondition condition new CompositeCondition(AND) .add(new SimpleCondition(status ?, 1)) .add(new CompositeCondition(OR) .add(new SimpleCondition(age ?, 30)) .add(new SimpleCondition(username LIKE ?, %zhang%))); String sql SELECT * FROM user_account WHERE condition.toSql(params);这种设计适合条件组合特别灵活的场景但代码复杂度会提升。如果当前项目条件没有复杂到需要组合嵌套不建议一上来就用这种模式前面的SqlParams已经够用。9.2 逻辑分组括号怎么拼有些业务查询可能是WHERE (status 1 OR status 2) AND age 20这种就需要在 SQL 片段里手动加括号。用简单的 if 拼接也能实现但要注意括号的位置。我更推荐把这种“固定业务含义”的查询条件抽取成独立方法不要让buildWhere里的逻辑越来越长。private void applyComplexCondition(StringBuilder sql, ListObject params, UserQuery query) { if (query.getType() ! null) { sql.append( AND (status ? OR status ?)); params.add(query.getType()); params.add(query.getType() 1); // 具体业务规则按实际场景调整 } }把每个复杂的业务条件分离开主方法保持简单可读性会好很多。9.3 多表关联条件下的动态查询如果动态条件涉及JOIN查询核心思路不变只是 SQL 前缀部分要写好JOIN关系后面的条件拼装和单表完全一致。public ListOrderInfo searchOrders(OrderQuery query) { StringBuilder sql new StringBuilder( SELECT o.id, o.order_no, u.username FROM order_info o INNER JOIN user_account u ON o.user_id u.id ); ListObject params new ArrayList(); if (StringUtils.hasText(query.getOrderNo())) { sql.append(AND o.order_no ?); params.add(query.getOrderNo()); } if (StringUtils.hasText(query.getUsername())) { sql.append(AND u.username LIKE ?); params.add(% query.getUsername() %); } return jdbcTemplate.query(sql.toString(), new BeanPropertyRowMapper(OrderInfo.class), params.toArray()); }这里有个经验多表查询的结果映射如果用BeanPropertyRowMapper要注意同名列冲突。比如订单表和用户表都有status字段SQL 查出来的是两张表的全字段映射到实体类时可能互相干扰。解决办法是只查需要的列并且给重复列起别名。SELECT o.id, o.order_no, o.status AS order_status, u.username, u.status AS user_status FROM ...这样映射时就能区分。10. 生产环境落地时的一些建议条件进阶的写法在本地跑通只是第一步真正放到生产环境还应该提前想好下面几件事。10.1 不要过度设计条件构建器有些开发者会把条件构建器做得很重支持各种嵌套、各种函数式写法。但 SpringJDBC 的项目本身通常不复杂过重的抽象会让其他同事看不懂。我的建议是条件少于 10 个直接用SqlParams工具类或 if 拼接条件复杂但稳定独立方法拆分按业务块管理条件灵活多变且需要 API 化再考虑条件抽象模式优先保证代码可读性不要为了“优雅”牺牲直观性。10.2 日志与监控动态 SQL 很难像 MyBatis 那样直接看到完整 SQL因为 SQL 和参数是分离的。排错时没有日志会非常痛苦。建议在开发环境开启 SQL 日志。Spring Boot 配置logging.level.org.springframework.jdbc.core.JdbcTemplateDEBUG logging.level.org.springframework.jdbc.core.StatementCreatorUtilsTRACE这样能打印 SQL 模板和执行参数。生产环境如果怕刷日志可以只在出问题时临时开启或在网关层统一记录慢查询日志。long start System.currentTimeMillis(); ListUserAccount result jdbcTemplate.query(sql.toString(), mapper, params.toArray()); long cost System.currentTimeMillis() - start; if (cost 500) { log.warn(Slow query, cost {} ms, sql: {}, params: {}, cost, sql, params); }慢查询日志一定要带params不然查问题时不知道参数是什么。10.3 单元测试怎么覆盖动态条件动态条件查询最好写单元测试不然每次改动都可能引入回归问题。重点覆盖这些场景所有条件都为空应该返回全表SQL 合法单个条件生效比如只传 status其他不拼组合条件多个条件同时生效参数顺序无误分页参数pageNum 为 1 时 offset 为 0排序白名单传入非法排序字段时使用默认排序Test void testSearchUsers_WithStatusOnly() { UserQuery query new UserQuery(); query.setStatus(1); ListUserAccount result userDao.searchUsers(query); assertNotNull(result); assertTrue(result.stream().allMatch(u - u.getStatus() 1)); }如果项目里还没有引入测试框架建议用spring-boot-starter-test它能覆盖大部分测试需求。10.4 数据权限怎么融合到条件里很多后台系统有数据权限比如“只能看本部门的数据”。这类权限过滤条件最好是固定拼在 DAO 层或 Service 层不能在页面传入否则会被绕过。常见做法if (currentUser.isDataScoped()) { sql.append( AND dept_id IN (?, ?, ?)); params.addAll(currentUser.getDeptIds()); }这个条件应该由后端基于当前登录用户动态生成而不是从请求参数里获取。放在条件构建的最后一段保证任何列表查询都带上数据权限避免越权访问。11. 写到最后的一些实操体会SpringJDBC 的条件查询说到底就是两件事SQL 要动态、参数要安全。做得好的项目代码看起来清爽加条件时不容易出错做得不好的项目一个大方法里堆满字符串连接看着都头痛。我个人最推荐的方式是建立一个轻量的SqlParams工具类或者直接采用条件对象加公共构建方法。它不用引入任何新框架只是在 SpringJDBC 之上做了一层很薄的组织。项目规模不大时用不上Specification、条件构建器等复杂抽象反而更容易让没见过这段代码的同事快速理解。如果你正处在“JdbcTemplate 能用但列表查询越来越乱”的阶段不要急着换 MyBatis先把条件构建这一层整理好。很多时候问题不是框架能力不够而是条件组织方式还停留在粗暴拼接层面。把WHERE构建、参数列表、排序白名单、分页逻辑拆开代码会立刻清晰很多。真的要换 MyBatis 时动态 SQL 的 XML 或注解写法也更容易迁移因为你的业务语义已经拆得很清楚了。条件对象可以直接复用DAO 方法签名也可以保留只是实现方式换掉而已。如果只记住几个关键点我希望是这三个参数永远通过占位符传递排序字段必须白名单SQL 片段和参数添加必须紧挨着写。做到这三条SpringJDBC 的动态条件查询基本不会出大问题。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号