news 2026/9/8 8:03:02

SpringJDBC条件查询进阶:动态SQL与防注入实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringJDBC条件查询进阶:动态SQL与防注入实战

先说一个很常见的场景:在后台管理系统中,列表查询页面几乎都带筛选条件,用户可能按姓名查、按状态查、按时间区间查,也可能什么都不选直接查全部。如果查询条件少,用if判断拼 SQL 还算能忍,可一旦条件多起来,代码里全是if (xxx != null)拼接,不仅看着累,后续加一个字段还要小心翼翼改好几处,稍不注意就漏了空格或者多了一个AND,运行起来直接报 SQL 语法错误。

这一篇就围绕SpringJDBC的条件查询进阶来展开,重点解决动态条件如何拼、如何防止 SQL 注入、如何写出结构清晰且可维护的查询代码。适合正在使用JdbcTemplate做数据访问、想从“能跑”进阶到“写得规范”的 Java 开发者。


1. 条件查询的痛点与解决思路

1.1 直接拼接 SQL 有哪些问题

先看一段很常见的写法:

String sql = "SELECT * FROM user WHERE 1=1"; if (StringUtils.hasText(name)) { sql += " AND name = '" + name + "'"; } if (status != null) { sql += " AND status = " + status; }

这段代码在功能上没错,但它有几个明显问题:

第一,SQL 注入风险name如果是用户输入的内容,直接拼进 SQL 字符串里,攻击者可以构造' OR '1'='1这类参数,轻则查询范围被扩大,重则配合其他语句造成数据损坏。

第二,可读性差。条件一多,这个if块会越来越长,SQL 字符串和 Java 代码混杂在一起,阅读成本很高。

第三,维护成本高。每新增一个查询条件,都需要在字符串拼接处小心修改,一旦漏掉空格、多写一个AND,排查起来非常消耗时间。

1.2 SpringJDBC 解决方案的整体思路

SpringJDBC并没有像 MyBatis 那样提供 XML 映射文件,也没有像 MyBatis-Plus 那样提供现成的QueryWrapper,但它提供的JdbcTemplateNamedParameterJdbcTemplate足够支撑我们写出灵活、安全、可维护的动态条件查询。

核心思路有三点:

  • 使用?占位符或者:name命名参数代替字符串拼接,从语法层面规避 SQL 注入。
  • 将 SQL 主体和条件部分的构建分开,让代码结构更清晰。
  • 借用面向对象的方式,把查询条件封装成对象,由条件对象决定哪些片段参与 SQL 拼装。

接下来,我们从基础回顾开始,再逐步进入动态条件、命名参数、复杂条件处理,最后用一个完整的多条件用户查询案例把知识点串起来。


2. 环境准备与项目结构

2.1 开发环境说明

本文示例基于以下环境,版本可以根据你本地的实际情况调整,核心是演示配置思路和写法:

组件说明
JDK8 及以上
Spring Boot2.x 或 3.x
构建工具Maven
数据库MySQL 5.7 / 8.0
IDEIntelliJ IDEA

需要说明的是,Spring Boot 3.x 要求 JDK 17 及以上,如果你的项目还在 JDK 8,请继续使用 Spring Boot 2.x 版本。本文代码不依赖 Spring Boot 新特性,因此两个版本都能运行。

2.2 Maven 依赖

创建一个普通的 Spring Boot Web 项目,核心依赖如下:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>

如果使用 Spring Boot 2.x,MySQL 驱动依赖坐标可以写成:

<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>

2.3 数据库表结构

为了演示需要,我们创建一张用户表,结构尽量贴近业务场景:

CREATE TABLE `user` ( `id` INT NOT NULL AUTO_INCREMENT COMMENT '主键', `name` VARCHAR(50) NOT NULL COMMENT '姓名', `age` INT DEFAULT NULL COMMENT '年龄', `status` TINYINT DEFAULT 1 COMMENT '状态:1启用 0禁用', `department` VARCHAR(100) DEFAULT NULL COMMENT '部门', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

初始化几条测试数据:

INSERT INTO `user` (`name`, `age`, `status`, `department`, `create_time`) VALUES ('张三', 25, 1, '研发部', '2024-01-10 09:00:00'), ('李四', 30, 1, '产品部', '2024-02-15 10:30:00'), ('王五', 22, 0, '研发部', '2024-03-20 14:20:00'), ('赵六', 28, 1, '测试部', '2024-04-18 16:45:00'), ('孙七', 35, 1, '运维部', '2024-05-22 08:10:00');

对应实体类:

// 文件路径:src/main/java/com/example/demo/entity/User.java package com.example.demo.entity; import java.time.LocalDateTime; public class User { private Integer id; private String name; private Integer age; private Integer status; private String department; private LocalDateTime createTime; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public Integer getStatus() { return status; } public void setStatus(Integer status) { this.status = status; } public String getDepartment() { return department; } public void setDepartment(String department) { this.department = department; } public LocalDateTime getCreateTime() { return createTime; } public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; } @Override public String toString() { return "User{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + ", status=" + status + ", department='" + department + '\'' + ", createTime=" + createTime + '}'; } }

3. 基础条件查询回顾

3.1 JdbcTemplate 基础查询

JdbcTemplate是 SpringJDBC 体系中最核心的类。使用它执行查询时,需要提供一个RowMapper来把结果集映射成对象。这是最朴素、也是理解后续内容的基础写法。

// 文件路径:src/main/java/com/example/demo/dao/UserDao.java package com.example.demo.dao; import com.example.demo.entity.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Repository; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; @Repository public class UserDao { @Autowired private JdbcTemplate jdbcTemplate; private RowMapper<User> userRowMapper = new RowMapper<User>() { @Override public User mapRow(ResultSet rs, int rowNum) throws SQLException { User user = new User(); user.setId(rs.getInt("id")); user.setName(rs.getString("name")); user.setAge(rs.getInt("age")); user.setStatus(rs.getInt("status")); user.setDepartment(rs.getString("department")); user.setCreateTime(rs.getTimestamp("create_time").toLocalDateTime()); return user; } }; public List<User> findByName(String name) { String sql = "SELECT id, name, age, status, department, create_time FROM user WHERE name = ?"; return jdbcTemplate.query(sql, new Object[]{name}, userRowMapper); } }

这里有几个细节值得注意:

  • 查询字段尽量写明确列名,不要顺手写SELECT *。原因后面最佳实践部分会展开。
  • new Object[]{name}是占位符对应的参数列表,顺序必须和 SQL 中?的顺序一致。
  • RowMapper负责把 JDBC 返回的ResultSet转换成业务对象,这一层逻辑独立出来,可以复用。

3.2 多个条件的固定查询

如果查询条件是固定的,比如“按姓名和状态查询”,SQL 直接写清楚即可:

public List<User> findByNameAndStatus(String name, Integer status) { String sql = "SELECT id, name, age, status, department, create_time FROM user WHERE name = ? AND status = ?"; return jdbcTemplate.query(sql, new Object[]{name, status}, userRowMapper); }

这种写法的优点是直观,缺点是条件完全写死。一旦用户不传姓名,或者不传状态,这个 SQL 就查不出数据了。现实业务中,筛选条件往往“可有可无”,这就引出了动态条件查询的需求。


4. 动态条件查询的三种方案

4.1 方案一:字符串手工拼接

这是最直观、也是很多新手最容易踩坑的写法。我们需要根据条件是否为空来动态添加 SQL 片段,同时记录参数。

public List<User> searchUsers(String name, Integer status, String department) { StringBuilder sql = new StringBuilder(); sql.append("SELECT id, name, age, status, department, create_time FROM user WHERE 1=1"); List<Object> params = new ArrayList<>(); if (StringUtils.hasText(name)) { sql.append(" AND name = ?"); params.add(name); } if (status != null) { sql.append(" AND status = ?"); params.add(status); } if (StringUtils.hasText(department)) { sql.append(" AND department = ?"); params.add(department); } return jdbcTemplate.query(sql.toString(), params.toArray(), userRowMapper); }

WHERE 1=1的作用是保证后续条件无论是否拼接,SQL 语法始终正确。它的执行效率不会有明显损失,因为 MySQL 的优化器会把这个恒真条件直接优化掉,不必担心性能问题。

这种方案适合条件较少、业务简单的场景,但缺点也很明显:当条件增加到五六个、七八个时,方法会变得很长,而且参数的添加顺序必须和 SQL 片段中的?顺序完全一致,一旦顺序错乱,数据就查错了。

4.2 方案二:条件对象 + 统一构建

为了解决参数顺序容易错乱的问题,我们可以把查询条件封装成一个对象,同时用一个构建器把“条件判断”和“参数收集”集中到一起。

先定义查询条件对象:

// 文件路径:src/main/java/com/example/demo/dto/UserQuery.java package com.example.demo.dto; public class UserQuery { private String name; private Integer status; private String department; private Integer minAge; private Integer maxAge; public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getStatus() { return status; } public void setStatus(Integer status) { this.status = status; } public String getDepartment() { return department; } public void setDepartment(String department) { this.department = department; } public Integer getMinAge() { return minAge; } public void setMinAge(Integer minAge) { this.minAge = minAge; } public Integer getMaxAge() { return maxAge; } public void setMaxAge(Integer maxAge) { this.maxAge = maxAge; } }

然后在 DAO 层写一个统一构建 SQL 的方法:

public List<User> searchUsers(UserQuery query) { StringBuilder sql = new StringBuilder(); sql.append("SELECT id, name, age, status, department, create_time FROM user WHERE 1=1"); List<Object> params = new ArrayList<>(); if (StringUtils.hasText(query.getName())) { sql.append(" AND name = ?"); params.add(query.getName()); } if (query.getStatus() != null) { sql.append(" AND status = ?"); params.add(query.getStatus()); } if (StringUtils.hasText(query.getDepartment())) { sql.append(" AND department = ?"); params.add(query.getDepartment()); } if (query.getMinAge() != null) { sql.append(" AND age >= ?"); params.add(query.getMinAge()); } if (query.getMaxAge() != null) { sql.append(" AND age <= ?"); params.add(query.getMaxAge()); } return jdbcTemplate.query(sql.toString(), params.toArray(), userRowMapper); }

这种写法的改进点是:条件字段集中在一个对象里,调用方不用传一堆散乱的参数;参数和 SQL 片段在同一个if块中追加,顺序天然一致,大大降低了参数错位的风险。

4.3 方案三:NamedParameterJdbcTemplate 命名参数

虽然第二种方案已经不错了,但代码里仍有大量sql.append(...)params.add(...),条件一多依然显得冗长。Spring 提供的NamedParameterJdbcTemplate支持命名参数,允许我们使用:name这样的占位符,配合Map传参,代码会清爽很多。

// 文件路径:src/main/java/com/example/demo/dao/UserDao.java(新增方法) package com.example.demo.dao; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; @Repository public class UserDao { @Autowired private NamedParameterJdbcTemplate namedParameterJdbcTemplate; public List<User> searchUsersWithNamedParams(UserQuery query) { StringBuilder sql = new StringBuilder(); sql.append("SELECT id, name, age, status, department, create_time FROM user WHERE 1=1"); MapSqlParameterSource params = new MapSqlParameterSource(); if (StringUtils.hasText(query.getName())) { sql.append(" AND name = :name"); params.addValue("name", query.getName()); } if (query.getStatus() != null) { sql.append(" AND status = :status"); params.addValue("status", query.getStatus()); } if (StringUtils.hasText(query.getDepartment())) { sql.append(" AND department = :department"); params.addValue("department", query.getDepartment()); } if (query.getMinAge() != null) { sql.append(" AND age >= :minAge"); params.addValue("minAge", query.getMinAge()); } if (query.getMaxAge() != null) { sql.append(" AND age <= :maxAge"); params.addValue("maxAge", query.getMaxAge()); } return namedParameterJdbcTemplate.query(sql.toString(), params, userRowMapper); } }

MapSqlParameterSource可以理解为一个加强版的Map,专门用来承载 SQL 命名参数。它不仅支持addValue,还支持addValue("name", value, Types.VARCHAR)这样的写法,在需要指定 JDBC 类型时有很大用处。

命名参数和?占位符相比,最大的优势是:不用关心参数顺序了。就算后面调整了 SQL 片段的顺序,只要参数名不变,代码依然正确。


5. 条件进阶:LIKE、IN、时间区间与多表关联

5.1 模糊查询 LIKE

业务系统里最常见的查询是“按名称模糊搜索”。使用命名参数时可以这样写:

if (StringUtils.hasText(query.getName())) { sql.append(" AND name LIKE :name"); params.addValue("name", "%" + query.getName() + "%"); }

这里把%拼到参数值中,而不是拼到 SQL 里,这是避免 SQL 注入的关键。如果写成AND name LIKE '%:name%',那它会被当成字符串内容处理,查询会失效。

需要说明的是,前缀模糊'abc%'可以利用索引,中缀模糊'%abc%'无法利用索引,数据量大时可能造成全表扫描。如果业务确实需要中缀模糊,可以考虑搜索引擎方案,或者控制查询范围。

5.2 IN 条件查询

按多个 ID 查询是常见的需求。NamedParameterJdbcTemplateIN集合参数有自动展开的能力,我们只需要把集合作为参数传进去:

public List<User> findByIds(List<Integer> ids) { String sql = "SELECT id, name, age, status, department, create_time FROM user WHERE id IN (:ids)"; MapSqlParameterSource params = new MapSqlParameterSource(); params.addValue("ids", ids); return namedParameterJdbcTemplate.query(sql, params, userRowMapper); }

Spring 会识别:ids对应的值是一个Collection,自动把 SQL 展开成IN (?, ?, ?),参数数量由集合大小决定。这里要特别提醒一个坑:如果ids是一个空集合,SQL 会变成IN (),在 MySQL 中会直接报语法错误。所以调用前务必判空:

if (ids == null || ids.isEmpty()) { return Collections.emptyList(); }

5.3 时间区间查询

按时间范围查询通常使用>=<的组合。开区间查询比闭区间更具扩展性,而且不容易遗漏边界。

if (query.getStartTime() != null) { sql.append(" AND create_time >= :startTime"); params.addValue("startTime", query.getStartTime()); } if (query.getEndTime() != null) { sql.append(" AND create_time < :endTime"); params.addValue("endTime", query.getEndTime()); }

这里把结束时间用<而不是<=,好处是即使传入的结束时间是2024-12-31 23:59:59,使用< 2025-01-01 00:00:00这种写法也依然能覆盖完整范围,避免因时间精度问题漏数据。

5.4 多表关联条件查询

当条件查询涉及多张表时,同一套构建 SQL 的思路依然适用。假设查询需求是:根据“用户姓名”和“用户所属部门”查询该用户最近一笔订单信息。我们把连接条件和筛选条件一起构建即可:

public List<Map<String, Object>> searchUserOrder(UserQuery query) { StringBuilder sql = new StringBuilder(); sql.append("SELECT u.name, u.department, o.order_no, o.amount "); sql.append("FROM user u "); sql.append("LEFT JOIN user_order o ON u.id = o.user_id "); sql.append("WHERE 1=1"); MapSqlParameterSource params = new MapSqlParameterSource(); if (StringUtils.hasText(query.getName())) { sql.append(" AND u.name LIKE :name"); params.addValue("name", "%" + query.getName() + "%"); } if (StringUtils.hasText(query.getDepartment())) { sql.append(" AND u.department = :department"); params.addValue("department", query.getDepartment()); } return namedParameterJdbcTemplate.queryForList(sql.toString(), params); }

多表查询时,最需要注意的是限定列名前缀。如果多个表都有namestatus字段,不加前缀会导致 SQL 列名歧义,轻则报错,重则查出错误数据。养成给列名加表别名前缀的习惯非常重要。


6. 完整实战:多条件用户查询接口

为了把前面所有知识点串起来,我们实现一个完整的用户查询接口。要求支持按姓名模糊、状态精确、部门精确、年龄区间、创建时间区间筛选,并且支持分页返回总数。

6.1 查询条件对象扩展

// 文件路径:src/main/java/com/example/demo/dto/UserQuery.java package com.example.demo.dto; import java.time.LocalDateTime; public class UserQuery { private String name; private Integer status; private String department; private Integer minAge; private Integer maxAge; private LocalDateTime startTime; private LocalDateTime endTime; private int pageNum = 1; private int pageSize = 10; public int getOffset() { return (pageNum - 1) * pageSize; } // getter / setter 省略,与前面一致 }

这里新增了pageNumpageSize,并通过getOffset()计算分页偏移量,方便在 SQL 中使用。

6.2 DAO 层实现

// 文件路径:src/main/java/com/example/demo/dao/UserDao.java package com.example.demo.dao; import com.example.demo.dto.UserQuery; import com.example.demo.entity.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.stereotype.Repository; import org.springframework.util.StringUtils; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; @Repository public class UserDao { @Autowired private JdbcTemplate jdbcTemplate; @Autowired private NamedParameterJdbcTemplate namedParameterJdbcTemplate; private RowMapper<User> userRowMapper = new RowMapper<User>() { @Override public User mapRow(ResultSet rs, int rowNum) throws SQLException { User user = new User(); user.setId(rs.getInt("id")); user.setName(rs.getString("name")); user.setAge(rs.getInt("age")); user.setStatus(rs.getInt("status")); user.setDepartment(rs.getString("department")); user.setCreateTime(rs.getTimestamp("create_time").toLocalDateTime()); return user; } }; public long countByQuery(UserQuery query) { StringBuilder sql = new StringBuilder(); sql.append("SELECT COUNT(*) FROM user WHERE 1=1"); MapSqlParameterSource params = buildParams(sql, query); Long count = namedParameterJdbcTemplate.queryForObject(sql.toString(), params, Long.class); return count == null ? 0L : count; } public List<User> findPageByQuery(UserQuery query) { StringBuilder sql = new StringBuilder(); sql.append("SELECT id, name, age, status, department, create_time FROM user WHERE 1=1"); MapSqlParameterSource params = buildParams(sql, query); sql.append(" ORDER BY id DESC LIMIT :limit OFFSET :offset"); params.addValue("limit", query.getPageSize()); params.addValue("offset", query.getOffset()); return namedParameterJdbcTemplate.query(sql.toString(), params, userRowMapper); } private MapSqlParameterSource buildParams(StringBuilder sql, UserQuery query) { MapSqlParameterSource params = new MapSqlParameterSource(); if (StringUtils.hasText(query.getName())) { sql.append(" AND name LIKE :name"); params.addValue("name", "%" + query.getName() + "%"); } if (query.getStatus() != null) { sql.append(" AND status = :status"); params.addValue("status", query.getStatus()); } if (StringUtils.hasText(query.getDepartment())) { sql.append(" AND department = :department"); params.addValue("department", query.getDepartment()); } if (query.getMinAge() != null) { sql.append(" AND age >= :minAge"); params.addValue("minAge", query.getMinAge()); } if (query.getMaxAge() != null) { sql.append(" AND age <= :maxAge"); params.addValue("maxAge", query.getMaxAge()); } if (query.getStartTime() != null) { sql.append(" AND create_time >= :startTime"); params.addValue("startTime", query.getStartTime()); } if (query.getEndTime() != null) { sql.append(" AND create_time < :endTime"); params.addValue("endTime", query.getEndTime()); } return params; } }

这里把buildParams方法抽出来,让总条数查询和分页查询共用同一套条件构建逻辑。这样能避免两个方法各自维护一份条件代码,有利于后续修改,比如增加一个新的查询字段,只需要改buildParams一处即可。

6.3 Service 层实现

// 文件路径:src/main/java/com/example/demo/service/UserService.java package com.example.demo.service; import com.example.demo.dao.UserDao; import com.example.demo.dto.UserQuery; import com.example.demo.entity.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.List; import java.util.Map; @Service public class UserService { @Autowired private UserDao userDao; public Map<String, Object> queryUserPage(UserQuery query) { long total = userDao.countByQuery(query); List<User> list = total == 0 ? List.of() : userDao.findPageByQuery(query); Map<String, Object> result = new HashMap<>(); result.put("total", total); result.put("list", list); result.put("pageNum", query.getPageNum()); result.put("pageSize", query.getPageSize()); return result; } }

total == 0时直接跳过列表查询,是一个很实用的小优化。如果总数为 0,就不需要再执行一次分页查询了,既节省一次数据库交互,也减少了无谓的数据传输。

6.4 Controller 层实现

// 文件路径:src/main/java/com/example/demo/controller/UserController.java package com.example.demo.controller; import com.example.demo.dto.UserQuery; import com.example.demo.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Map; @RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @GetMapping("/page") public Map<String, Object> page(UserQuery query) { return userService.queryUserPage(query); } }

6.5 运行与验证

启动 Spring Boot 项目后,访问:

GET http://localhost:8080/user/page?name=%E5%BC%A0&status=1&pageNum=1&pageSize=10

其中%E5%BC%A0是 URL 编码后的“张”字。预期输出如下:

{ "total": 1, "list": [ { "id": 1, "name": "张三", "age": 25, "status": 1, "department": "研发部", "createTime": "2024-01-10T09:00:00" } ], "pageNum": 1, "pageSize": 10 }

再测试不带任何条件的分页查询:

GET http://localhost:8080/user/page?pageNum=1&pageSize=3

输出应该包含 5 条数据中的前 3 条,并且total为 5。


7. 常见问题与排查思路

问题现象常见原因解决思路
执行时报SQLSyntaxErrorExceptionSQL 片段拼接漏了空格,或者AND前缺少条件分隔检查生成的 SQL 日志,观察拼接后的语句;在buildParams中加入空格约定
查询结果为空,但没有报错动态条件判断条件写反,或者参数类型不匹配确认StringUtils.hasTextnull判断的使用是否正确;打印参数确认值
IN (:ids)语法报错传入的是空集合调用前判空,空集合直接返回空结果
参数顺序错乱导致数据错误使用?占位符时,参数添加顺序和 SQL 顺序不一致改用NamedParameterJdbcTemplate命名参数
模糊查询无结果%写成了'%:name%',被当成文本内容%拼在参数值上,如params.addValue("name", "%" + query.getName() + "%")
分页数据重复或缺失分页 SQL 缺少稳定的排序字段分页查询尽量加上主键或唯一字段排序,例如ORDER BY id

8. 最佳实践与工程建议

8.1 SQL 语句列名显式化

不要写SELECT *。显式列出查询字段有三个好处:

第一,代码即文档,一眼能看出这条查询返回了哪些列。第二,当表结构发生变化时,可以快速定位需要修改的映射关系。第三,避免查询不需要的大字段,减少网络传输压力。

8.2 查询条件统一封装到条件对象

方法的参数不要超过三个。当查询条件达到四个以上时,优先考虑封装成XxxQuery对象。这样做不仅让方法签名保持简洁,也为后续扩展(比如增加排序字段、分页参数)留好了入口。

8.3 条件构建逻辑抽成共用方法

同一个查询通常需要同时统计总数和查询列表,如果这两段逻辑各自维护一套条件拼装,很容易在修改时漏改一边,导致总数和列表条件不一致。把构建 SQL 片段和参数的方法抽出来,让两边复用,是降低维护成本的关键手段。

8.4 善用 NamedParameterJdbcTemplate

对于条件多、动态拼接复杂的场景,优先选择NamedParameterJdbcTemplate。SQL 片段中只出现:参数名,参数通过MapSqlParameterSource设置,代码可读性比占位符拼接好很多,也不容易出现参数顺序错乱的问题。

8.5 提前做参数校验

对必填参数、集合参数、分页参数做前置校验,避免让 SQL 层承担不必要的判断。比如IN条件在集合为空时应该直接返回空结果,而不是把空集合传给数据库层。

8.6 关注 SQL 日志和慢查询

开发阶段就开启 SQL 日志,方便排查动态 SQL 拼接问题。生产环境建议开启数据库慢查询日志,把执行时间超过阈值的 SQL 记录下来。条件查询中特别需要留意没有命中索引的模糊查询和范围查询。


9. 总结

这一篇围绕SpringJDBC条件查询进阶,从最基础的JdbcTemplate占位符查询开始,逐步讲到动态条件拼接的三种方案:手工字符串拼接、条件对象 + 统一构建、命名参数方式。其中命名参数方式在条件较多时优势最明显,也是推荐优先掌握的技能。

实战部分实现了一个完整的多条件用户分页查询接口,覆盖了姓名模糊、状态精确、年龄区间、时间区间、分页等常见场景,并且把总数查询和列表查询的条件构建逻辑复用了起来。

下一步可以继续学习:

  • BeanPropertyRowMapper的使用场景和局限。
  • 分页查询中深分页的优化思路,比如基于游标的分页。
  • SpringJDBC中如何配合事务管理,保证多个写操作的一致性。
  • 与 MyBatis 对比,什么场景下选择哪种数据访问方式更合适。

如果你正在从JdbcTemplate的简单使用向规范化开发过渡,条件查询这块是绕不开的基础功。平时可以多写几个不同条件的查询场景,亲手感受一下命名参数、条件对象带来的可维护性提升。项目里遇到类似需求,直接拿本文的代码结构去改,会省下不少排查 SQL 拼写问题的精力。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 8:03:00

计算机信息与大数据应用国际会议CIBDA 2026投稿全攻略

如果你正在准备一篇计算机、信息或大数据方向的论文&#xff0c;打算找一个审稿周期相对可控、出版检索路径清晰、认可度又比较稳的国际会议来投&#xff0c;那CIBDA 2026确实值得认真了解一下。这个会议全称是第七届计算机信息和大数据应用国际学术会议&#xff0c;由ICPS出版…

作者头像 李华
网站建设 2026/9/8 8:02:45

用Python从零实现AI Agent:工作流编排与插件化扩展实践

AI Agent 是目前大模型应用里最值得亲手做一遍的方向。很多人已经在网页端和大模型聊天&#xff0c;也就是把大模型当成问答工具&#xff1a;输入一段文本&#xff0c;拿到一段生成结果。但到了真实业务场景&#xff0c;大模型往往需要「先规划再行动」——根据目标决定调用什么…

作者头像 李华
网站建设 2026/9/8 8:01:07

Linux下meld工具详解:文件比较、目录对比与Git集成

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 8:00:51

Kimi K3 AI编程助手:前端开发工作流优化与智能体应用实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 7:58:55

Kubernetes Pod健康探测与滚动更新零故障实践

1. 滚动更新为什么会在“一切正常”时翻车 做Kubernetes平台运维这几年&#xff0c;让我感触最深的一个主题就是Pod健康探测。我刚开始负责这块时&#xff0c;遇到过一次印象很深的故障&#xff1a;一个核心服务发版本&#xff0c;滚动更新流程跑得很顺&#xff0c;新Pod一个个…

作者头像 李华
网站建设 2026/9/8 7:58:18

信息安全毕设选题指南:从车载安全到AI应用的高价值方向拆解

这些年我参与过多届信息安全专业本科生的毕设开题与答辩&#xff0c;一个很深的感受是&#xff1a;大部分学生不是能力不够&#xff0c;而是被陈旧的题目库困住了。信息安全毕设选题年年有人问&#xff0c;真正新颖、可落地、能写清楚创新点的却很少。今年又有人拿着“DES图像加…

作者头像 李华