
先说一个很常见的场景在后台管理系统中列表查询页面几乎都带筛选条件用户可能按姓名查、按状态查、按时间区间查也可能什么都不选直接查全部。如果查询条件少用if判断拼 SQL 还算能忍可一旦条件多起来代码里全是if (xxx ! null)拼接不仅看着累后续加一个字段还要小心翼翼改好几处稍不注意就漏了空格或者多了一个AND运行起来直接报 SQL 语法错误。这一篇就围绕SpringJDBC的条件查询进阶来展开重点解决动态条件如何拼、如何防止 SQL 注入、如何写出结构清晰且可维护的查询代码。适合正在使用JdbcTemplate做数据访问、想从“能跑”进阶到“写得规范”的 Java 开发者。1. 条件查询的痛点与解决思路1.1 直接拼接 SQL 有哪些问题先看一段很常见的写法String sql SELECT * FROM user WHERE 11; if (StringUtils.hasText(name)) { sql AND name name ; } if (status ! null) { sql AND status status; }这段代码在功能上没错但它有几个明显问题第一SQL 注入风险。name如果是用户输入的内容直接拼进 SQL 字符串里攻击者可以构造 OR 11这类参数轻则查询范围被扩大重则配合其他语句造成数据损坏。第二可读性差。条件一多这个if块会越来越长SQL 字符串和 Java 代码混杂在一起阅读成本很高。第三维护成本高。每新增一个查询条件都需要在字符串拼接处小心修改一旦漏掉空格、多写一个AND排查起来非常消耗时间。1.2 SpringJDBC 解决方案的整体思路SpringJDBC并没有像 MyBatis 那样提供 XML 映射文件也没有像 MyBatis-Plus 那样提供现成的QueryWrapper但它提供的JdbcTemplate和NamedParameterJdbcTemplate足够支撑我们写出灵活、安全、可维护的动态条件查询。核心思路有三点使用?占位符或者:name命名参数代替字符串拼接从语法层面规避 SQL 注入。将 SQL 主体和条件部分的构建分开让代码结构更清晰。借用面向对象的方式把查询条件封装成对象由条件对象决定哪些片段参与 SQL 拼装。接下来我们从基础回顾开始再逐步进入动态条件、命名参数、复杂条件处理最后用一个完整的多条件用户查询案例把知识点串起来。2. 环境准备与项目结构2.1 开发环境说明本文示例基于以下环境版本可以根据你本地的实际情况调整核心是演示配置思路和写法组件说明JDK8 及以上Spring Boot2.x 或 3.x构建工具Maven数据库MySQL 5.7 / 8.0IDEIntelliJ IDEA需要说明的是Spring Boot 3.x 要求 JDK 17 及以上如果你的项目还在 JDK 8请继续使用 Spring Boot 2.x 版本。本文代码不依赖 Spring Boot 新特性因此两个版本都能运行。2.2 Maven 依赖创建一个普通的 Spring Boot Web 项目核心依赖如下dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency如果使用 Spring Boot 2.xMySQL 驱动依赖坐标可以写成dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency2.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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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 RowMapperUser userRowMapper new RowMapperUser() { 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 ListUser 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 ListUser 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 ListUser searchUsers(String name, Integer status, String department) { StringBuilder sql new StringBuilder(); sql.append(SELECT id, name, age, status, department, create_time FROM user WHERE 11); ListObject 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 11的作用是保证后续条件无论是否拼接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 ListUser searchUsers(UserQuery query) { StringBuilder sql new StringBuilder(); sql.append(SELECT id, name, age, status, department, create_time FROM user WHERE 11); ListObject 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 ListUser searchUsersWithNamedParams(UserQuery query) { StringBuilder sql new StringBuilder(); sql.append(SELECT id, name, age, status, department, create_time FROM user WHERE 11); 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 查询是常见的需求。NamedParameterJdbcTemplate对IN集合参数有自动展开的能力我们只需要把集合作为参数传进去public ListUser findByIds(ListInteger 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 ListMapString, 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 11); 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); }多表查询时最需要注意的是限定列名前缀。如果多个表都有name或status字段不加前缀会导致 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 省略与前面一致 }这里新增了pageNum和pageSize并通过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 RowMapperUser userRowMapper new RowMapperUser() { 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 11); MapSqlParameterSource params buildParams(sql, query); Long count namedParameterJdbcTemplate.queryForObject(sql.toString(), params, Long.class); return count null ? 0L : count; } public ListUser findPageByQuery(UserQuery query) { StringBuilder sql new StringBuilder(); sql.append(SELECT id, name, age, status, department, create_time FROM user WHERE 11); 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 MapString, Object queryUserPage(UserQuery query) { long total userDao.countByQuery(query); ListUser list total 0 ? List.of() : userDao.findPageByQuery(query); MapString, 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 MapString, Object page(UserQuery query) { return userService.queryUserPage(query); } }6.5 运行与验证启动 Spring Boot 项目后访问GET http://localhost:8080/user/page?name%E5%BC%A0status1pageNum1pageSize10其中%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?pageNum1pageSize3输出应该包含 5 条数据中的前 3 条并且total为 5。7. 常见问题与排查思路问题现象常见原因解决思路执行时报SQLSyntaxErrorExceptionSQL 片段拼接漏了空格或者AND前缺少条件分隔检查生成的 SQL 日志观察拼接后的语句在buildParams中加入空格约定查询结果为空但没有报错动态条件判断条件写反或者参数类型不匹配确认StringUtils.hasText和null判断的使用是否正确打印参数确认值IN (:ids)语法报错传入的是空集合调用前判空空集合直接返回空结果参数顺序错乱导致数据错误使用?占位符时参数添加顺序和 SQL 顺序不一致改用NamedParameterJdbcTemplate命名参数模糊查询无结果把%写成了%:name%被当成文本内容将%拼在参数值上如params.addValue(name, % query.getName() %)分页数据重复或缺失分页 SQL 缺少稳定的排序字段分页查询尽量加上主键或唯一字段排序例如ORDER BY id8. 最佳实践与工程建议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 拼写问题的精力。