尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Spring Boot整合JdbcTemplate:告别MyBatis繁琐,轻量数据访问实战
Spring Boot 整合 JdbcTemplate绕开 MyBatis 的繁琐也能把数据访问写得明明白白先聊聊我自己的选型经历。早几年做项目团队一上来就上 MyBatis生成 XML、配置 mapper、管理 resultMap一套流程下来小项目光搭架子就耗掉半天。后来维护一个遗留系统里面大量 SQL 是纯 JDBC 风格压根没有 ORM业务逻辑又复杂硬套 MyBatis 反而别扭最后我用 Spring Boot 整合 JdbcTemplate 把那一堆数据库操作重写了一遍整个 DAO 层代码量直接缩了一半而且 SQL 的掌控力还是在自己手里。这篇文章就把这套整合过程完整拆开从环境准备、核心 API 使用、多数据源配置到性能优化和常见坑全部基于我实际跑过的代码来写。无论你是刚接触 Spring Boot 的新手还是被 MyBatis 的映射规则折腾到头疼的老手只要想在项目里快速、轻量地把数据库操作跑起来这篇都能直接参考着动手。1. 整合思路为什么偏偏是 JdbcTemplateJdbcTemplate 是 Spring 对 JDBC 的轻量封装核心价值一句话概括它把“获取连接、创建 Statement、处理 ResultSet、释放资源”这些重复到令人麻木的样板代码全部吃掉但同时又把 SQL 的执行权完整交回给你。相比 MyBatis 的“半自动 ORM”相比 Spring Data JPA 的“全自动抽象”JdbcTemplate 处在两者之间的微妙位置没有 SQL 生成器没有一级二级缓存没有懒加载但正因为没有这些机制它在很多场景下更快、更可控、更容易排查问题。选择 JdbcTemplate 的典型场景我总结下来大概是这三类表结构复杂、SQL 多变需要精细控制每一条 SQLORM 的“自动生成 SQL”反而碍事。项目本身数据访问层不重不想引入 MyBatis 的 XML 配置体系也不想被 JPA 的实体状态管理搞迷糊。团队里新成员多JdbcTemplate 的学习曲线比 MyBatis 和 JPA 平缓太多掌握 JDBC 基础就能上手。Spring Boot 对 JdbcTemplate 的支持非常彻底只要在 classpath 里引入spring-boot-starter-jdbc再配置好数据源Spring Boot 的自动配置机制具体是JdbcTemplateAutoConfiguration就会自动创建JdbcTemplate、NamedParameterJdbcTemplate这两个 Bean你拿来就能注入使用不需要写一行Bean配置。这种“零配置”体验是 Spring Boot 整合 JdbcTemplate 最舒服的地方。2. 环境准备依赖、配置、建表一次到位2.1 最小依赖组合和版本选择从 Spring Boot 2.x 到目前的 3.x整合 JdbcTemplate 的依赖都没变化差异主要在 JDK 版本和包名3.x 需要 JDK 17javax.*换成了jakarta.*。我建议新项目直接上 Spring Boot 3.x老项目保持原版本即可核心代码差异不大。Maven 依赖就三个核心dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency第一行spring-boot-starter-jdbc是主角它会传递引入spring-jdbc、spring-tx事务支持以及 HikariCP 连接池。第二行是 MySQL 驱动具体依赖名称随版本有差异Spring Boot 2.x 管理的是mysql:mysql-connector-javaSpring Boot 3.x 改为com.mysql:mysql-connector-j两者本质是同一个东西的坐标迁移。注意如果不小心在 Spring Boot 3.x 里用了mysql:mysql-connector-java依赖能拉下来但运行时会报驱动类加载异常排查起来非常坑。建议用spring-boot-dependencies统一管理的版本不要手动写版本号。2.2 application.yml 配置数据源的关键参数Spring Boot 的数据源配置高度集中在spring.datasource下。最基本的写法spring: datasource: url: jdbc:mysql://localhost:3306/demo_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver hikari: minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000几个参数我得单独说明一下不然你照着写容易踩坑useSSLfalse本地开发连 MySQL 8.x 如果不开 SSL不关这个参数会频繁报 SSL 握手警告虽然不影响功能但日志很烦。serverTimezoneAsia/ShanghaiMySQL 8.x 的连接器默认时区和中国差了 8 小时不加这个参数Timestamp查询结果会出现诡异的时间偏移。allowPublicKeyRetrievaltrue新版 MySQL 驱动用caching_sha2_password认证时首次连接可能需要这个参数否则会报Public Key Retrieval is not allowed。driver-class-name其实可以省略Spring Boot 会根据 URL 自动推断驱动类但写出来更明确尤其在多数据源场景下必须显式写。HikariCP 参数里我实际使用中比较在意maximum-pool-size。很多错误教程建议设成 100、200在数据库连接数本来就紧张的场景直接打满数据库。合理的经验值是maximum-pool-size (CPU核数 × 2) 有效磁盘数一般 10 到 20 足够大多数业务系统使用。2.3 建表与数据准备为了后面演示 API 方便这里先建一张非常通用的用户表CREATE TABLE user_info ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, email VARCHAR(100) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;对应的实体类Data public class UserInfo { private Long id; private String username; private String password; private String email; private LocalDateTime createTime; }这里用了DataLombok项目里需要额外加lombok依赖。如果你不想用 Lombok手写 getter/setter 也完全没问题JdbcTemplate 不关心这些。3. JdbcTemplate 核心 API 实战增删改查全拆解3.1 注入方式与基础结构Spring Boot 自动配置的JdbcTemplate和NamedParameterJdbcTemplate可以直接注入到任何 Spring 管理的 Bean 中。我习惯的做法是在 Service 层直接注入JdbcTemplate而不是再包一层 DAO。这样代码更扁平但要注意职责边界复杂 SQL 还是要沉淀到专门的 DAO 类Service public class UserService { private final JdbcTemplate jdbcTemplate; public UserService(JdbcTemplate jdbcTemplate) { this.jdbcTemplate jdbcTemplate; } }3.2 插入数据从update()到GeneratedKeyHolder插入单条记录最简单的写法public int insertUser(UserInfo user) { String sql INSERT INTO user_info (username, password, email) VALUES (?, ?, ?); return jdbcTemplate.update(sql, user.getUsername(), user.getPassword(), user.getEmail()); }update()方法返回受影响行数通常用来判断执行是否成功。但这里有个实战中几乎一定遇到的问题插入后你需要自增主键 ID 回填到实体对象里否则接下来要做关联操作还得再查一次。JdbcTemplate 提供了GeneratedKeyHolderpublic int insertUserWithId(UserInfo user) { String sql INSERT INTO user_info (username, password, email) VALUES (?, ?, ?); KeyHolder keyHolder new GeneratedKeyHolder(); jdbcTemplate.update(connection - { PreparedStatement ps connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); ps.setString(1, user.getUsername()); ps.setString(2, user.getPassword()); ps.setString(3, user.getEmail()); return ps; }, keyHolder); Number key keyHolder.getKey(); if (key ! null) { user.setId(key.longValue()); } return user.getId() ! null ? 1 : 0; }这里有个容易忽略的细节prepareStatement时如果不传Statement.RETURN_GENERATED_KEYSMySQL 驱动不会返回自增主键keyHolder.getKey()会一直是null。这个参数是 JDBC 规范层面的和 JdbcTemplate 封不封装没关系很多封装框架内部帮我们做了用裸 JDBC 封装时必须自己记着。3.3 查询单条记录queryForObject与 RowMapper查单条最直观的方法public UserInfo getUserById(Long id) { String sql SELECT id, username, password, email, create_time FROM user_info WHERE id ?; return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper(UserInfo.class), id); }BeanPropertyRowMapper能把结果集按 JavaBean 属性名自动映射注意说的是“属性名”即实体类里的createTime对应数据库的create_time这种经典的下划线转驼峰映射BeanPropertyRowMapper默认是不支持的它会去找create_time属性而不是createTime属性结果就是抛异常或者字段为null。所以实际项目里我更推荐手写一个RowMapperSQL 里直接用别名对齐实体属性名private static final RowMapperUserInfo USER_ROW_MAPPER (rs, rowNum) - { UserInfo user new UserInfo(); user.setId(rs.getLong(id)); user.setUsername(rs.getString(username)); user.setPassword(rs.getString(password)); user.setEmail(rs.getString(email)); user.setCreateTime(rs.getTimestamp(create_time).toLocalDateTime()); return user; }; public UserInfo getUserById(Long id) { String sql SELECT id, username, password, email, create_time FROM user_info WHERE id ?; return jdbcTemplate.queryForObject(sql, USER_ROW_MAPPER, id); }这种方式瞧着多一点代码但好处非常明显映射逻辑完全掌握在自己手里类型转换冲突、字段隔离改名、嵌套对象组装都能在这个RowMapper里做。查多列、多表关联查询、DTO 属性名和表字段完全不一致时这种手写RowMapper是收拾烂摊子的最佳方案。如果表字段和实体属性刚好完全一致都不带下划线用BeanPropertyRowMapper是很省事的但表名、字段命名稍有不规范它的表现就会让新人大呼“为什么查出来全是 null”。3.4 查询列表query()与空结果集处理查列表基本同上只是用query()替换queryForObject()public ListUserInfo listUsers() { String sql SELECT id, username, password, email, create_time FROM user_info ORDER BY id DESC; return jdbcTemplate.query(sql, USER_ROW_MAPPER); }注意query()查询结果为空时返回的是空列表不会抛异常。而queryForObject()在结果为空时抛EmptyResultDataAccessException实际业务里查单条要提前判断“查不到怎么办”我通常的做法有两种捕获EmptyResultDataAccessException返回null。改用query(...).stream().findFirst()列表为空时返回Optional.empty()。第一种更直接第二种少一次异常栈的开销。如果这个方法会被高频调用比如按主键查缓存建议用第二种。public UserInfo getUserByIdQuietly(Long id) { String sql SELECT id, username, password, email, create_time FROM user_info WHERE id ?; ListUserInfo list jdbcTemplate.query(sql, USER_ROW_MAPPER, id); return list.isEmpty() ? null : list.get(0); }3.5 更新和删除update()的变体更新操作和插入完全同一个套路public int updateEmail(Long id, String newEmail) { String sql UPDATE user_info SET email ? WHERE id ?; return jdbcTemplate.update(sql, newEmail, id); } public int deleteUser(Long id) { String sql DELETE FROM user_info WHERE id ?; return jdbcTemplate.update(sql, id); }注意update()返回的是影响行数不是主键。删除时影响 0 行不代表 SQL 执行失败只是条件没匹配到记录业务层需要根据这个返回值决定是否抛出“数据不存在”之类的异常。3.6 命名参数NamedParameterJdbcTemplate让 SQL 不再数问号用?占位符时参数一多数问号真的是噩梦。比如一个更新 SQL 有七八个字段中间漏了一个参数运行时直接Incorrect column count或者参数错位排查非常痛苦。NamedParameterJdbcTemplate解决的就是这个痛点它允许 SQL 里用:paramName形式的命名参数参数通过Map或SqlParameterSource传入Service public class UserService { private final NamedParameterJdbcTemplate namedJdbcTemplate; public UserService(NamedParameterJdbcTemplate namedJdbcTemplate) { this.namedJdbcTemplate namedJdbcTemplate; } public int insertUserByNamed(UserInfo user) { String sql INSERT INTO user_info (username, password, email) VALUES (:username, :password, :email); MapSqlParameterSource params new MapSqlParameterSource() .addValue(username, user.getUsername()) .addValue(password, user.getPassword()) .addValue(email, user.getEmail()); return namedJdbcTemplate.update(sql, params); } }NamedParameterJdbcTemplate本身就是对JdbcTemplate的包装底层会把命名参数展开成?占位符再交给JdbcTemplate执行。所以事务、连接池、RowMapper 这些用法完全一致只是换了个入口。我现在的项目基本都是直接注入NamedParameterJdbcTemplate可读性提升了一个档次强烈建议新代码直接用这个。3.7 批量操作batchUpdate的正确打开方式批量插入常见两种做法效果天差地别写一个for循环调一万次update()。性能差每次都要走完整的 SQL 解析、执行、事务提交如果没加外层事务一万条数据跑起来肉眼可见的慢。用batchUpdate()一次提交一批JDBC 底层走批量执行网络和数据库开销都压缩得很低。public int[] batchInsertUsers(ListUserInfo users) { String sql INSERT INTO user_info (username, password, email) VALUES (?, ?, ?); return jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { Override public void setValues(PreparedStatement ps, int i) throws SQLException { UserInfo user users.get(i); ps.setString(1, user.getUsername()); ps.setString(2, user.getPassword()); ps.setString(3, user.getEmail()); } Override public int getBatchSize() { return users.size(); } }); }实现BatchPreparedStatementSetter时要注意setValues里的索引i是从 0 开始的而users.get(i)也是从 0 开始这两个从对应关系上不会错位。但如果你的users是个很大的集合一次性batchUpdate会撑爆内存或数据库单次事务的大小稳妥的做法是每 500 条一组分片提交。关于返回的int[]数组里的每个元素代表每条语句影响的行数。一般只要没有抛异常就可以认为批量执行成功不用太纠结具体值。3.8 查询结果集映射的更多写法除了手写RowMapper和BeanPropertyRowMapper还有一个常用变体是ColumnMapRowMapper直接把每一行封装成MapString, Object适合不需要强类型实体、临时取数据的场景public ListMapString, Object listUserMaps() { return jdbcTemplate.queryForList(SELECT * FROM user_info); }这种写法方便灵活但拿到手的是String到Object的松散结构类型转换全靠自觉适合报表、导出、临时调查这类场景不适合核心业务逻辑。4. 进阶玩法多数据源、事务、查询超时控制4.1 多数据源配置从“双库操作”到“读改写分离”JdbcTemplate 整合里最常遇到的进阶需求是多数据源。我曾经在一个项目里同时操作 MySQL业务主库和 PostgreSQL报表库同一个 Service 方法里先更新 MySQL 再查询 PostgreSQL。Spring Boot 默认只自动配置一个DataSource多数据源时要用Primary指定主数据源并对第二个数据源手动构建。Configuration public class DataSourceConfig { Bean Primary ConfigurationProperties(prefix spring.datasource.primary) public DataSource primaryDataSource() { return DataSourceBuilder.create().build(); } Bean ConfigurationProperties(prefix spring.datasource.secondary) public DataSource secondaryDataSource() { return DataSourceBuilder.create().build(); } Bean Primary public JdbcTemplate primaryJdbcTemplate(Qualifier(primaryDataSource) DataSource ds) { return new JdbcTemplate(ds); } Bean public JdbcTemplate secondaryJdbcTemplate(Qualifier(secondaryDataSource) DataSource ds) { return new JdbcTemplate(ds); } }注意ConfigurationProperties(prefix spring.datasource.primary)意味着我们不再用spring.datasource.url统一配置而是拆成两个独立配置块spring: datasource: primary: url: jdbc:mysql://localhost:3306/main_db... username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver secondary: url: jdbc:postgresql://localhost:5432/report_db... username: report_user password: 123456 driver-class-name: org.postgresql.Driver然后使用时通过Qualifier指定你要操作的数据源Service public class MixedService { private final JdbcTemplate primaryJdbcTemplate; private final JdbcTemplate secondaryJdbcTemplate; public MixedService(Qualifier(primaryJdbcTemplate) JdbcTemplate primaryJdbcTemplate, Qualifier(secondaryJdbcTemplate) JdbcTemplate secondaryJdbcTemplate) { this.primaryJdbcTemplate primaryJdbcTemplate; this.secondaryJdbcTemplate secondaryJdbcTemplate; } }这种方式清晰直白适合数据源数量固定且少的场景。还有一个更动态的方案是继承AbstractRoutingDataSource在运行时通过ThreadLocal或AOP切面动态决定走哪个数据源适合做读写分离一个写库、多个读库的场景但架构复杂度明显上升如果只是双库操作没必要上路由那套。4.2 事务管理Transactional的正确用法JdbcTemplate 本身没有事务能力但 Spring Boot 的DataSourceTransactionManager已经自动配置好了。只要在 Service 方法上加Transactional就能把多个数据库操作包进同一个事务Transactional(rollbackFor Exception.class) public void transferUser(Long fromUserId, Long toUserId, int amount) { String reduceSql UPDATE user_account SET balance balance - ? WHERE id ?; String addSql UPDATE user_account SET balance balance ? WHERE id ?; jdbcTemplate.update(reduceSql, amount, fromUserId); // 模拟中间异常 if (amount 10000) { throw new RuntimeException(转账金额超限); } jdbcTemplate.update(addSql, amount, toUserId); }加上rollbackFor Exception.class非常关键。Spring 默认只在运行时异常RuntimeException和Error时才回滚如果你抛的是自定义Exception而不指定rollbackFor事务不会回滚数据就产生奇怪的中间状态。多数据源环境下Transactional会默认绑定Primary数据源对应的事务管理器如果第二个数据源的操作也需要事务就要指定不同的事务管理器或使用JtaTransactionManager实现分布式事务。这个复杂度比较高建议能避免就避免实在需要分布式事务再考虑 Seata 这类方案而不是在 Spring 层面硬凑。金玉良言Transactional失效的几大经典坑——方法必须被 Spring 代理调用同类内部调用直接失效、方法必须public、异常必须传播出方法边界、数据库引擎必须支持事务MyISAM 表结构加了也没用。每个我都踩过。4.3 查询超时控制防止慢 SQL 拖垮整个连接池JdbcTemplate 提供了queryForObject(sql, requiredType, queryTimeout)之类的重载方法也可以给JdbcTemplate设置全局超时JdbcTemplate jdbcTemplate new JdbcTemplate(dataSource); jdbcTemplate.setQueryTimeout(5); // 单位秒实际项目里如果有个别 SQL 特别容易慢我建议不要全局设置会影响所有查询而是在执行前用jdbcTemplate.setQueryTimeout(5)临时调整执行后马上恢复默认值。更优雅的方式是使用 Spring 的TransactionTemplate或 AOP 对特定 Service 方法做超时控制。关于超时要注意setQueryTimeout最终是设置 JDBC 驱动的Statement.setQueryTimeoutMySQL 驱动在部分网络异常场景下可能不会严格执行这个超时所以它更多是“尽力而为”的兜底措施不能替代数据库层面的max_execution_time优化。5. 常见问题与排查技巧我踩过的坑都在这5.1 中文乱码问题现象是数据库里保存的中文变成了??或者读出来一片乱码。这个问题基本绕不开三个层面数据库表字符集建表时用ENGINEInnoDB DEFAULT CHARSETutf8mb4utf8mb4 才是真正的完整中文支持老的utf8utf8mb3对四字节表情符号会报错。JDBC 连接串useUnicodetruecharacterEncodingutf8要带上现在新版 MySQL 驱动对 utf8mb4 支持更完善但老项目迁移后容易漏配置。应用层编码Spring Boot 默认使用UTF-8一般不用改。如果还有乱码检查server.servlet.encoding相关配置和请求响应头里的字符集。排查顺序建议先直接在数据库客户端执行 SQL 确认存储的数据是否正常再挨个检查连接串和表字符集最后才是应用层编码。数据库层面已经乱掉的时候改应用层代码永远救不回来。5.2 连接泄漏导致连接池耗尽症状是系统跑一段时间后接口响应越来越慢数据库连接池报HikariPool-1 - Connection is not available, request timed out。绝大多数原因是代码里没有正确释放连接。用JdbcTemplate的好处是不需要手动管连接但它依赖的DataSource如果被直接注入并用原生 JDBC 方式操作忘记在finally里关闭连接就会泄漏。另外Spring的Transactional方法中如果出现异常链路不清、事务传播配置混乱也会导致连接不释放。排查方法打开 HikariCP 的日志级别调spring.datasource.hikari.pool-name和日志框架对com.zaxxer.hikari的 DEBUG 级别再看连接池里active连接数量。出现异常峰值时把线程栈 dump 出来jstack找到持有连接但卡住的线程问题代码基本就现形了。5.3 SQL 注入与参数绑定的边界JdbcTemplate 的update(sql, args...)和query(sql, mapper, args...)都支持参数绑定?占位符的值是驱动层面预编译绑定不会被拼接到 SQL 字符串里天然防注入。真正危险的是自己拼 SQL// 危险直接把参数拼进 SQL String sql SELECT * FROM user_info WHERE username username ;这种代码即便用了 JdbcTemplate 也没有任何防护。凡涉及外部输入前端传参、配置文件取值、接口返回数据必须走占位符。如果参数是可变的排序字段、表名等结构对象?无法绑定结构需要在代码侧做白名单校验比如排序字段只能匹配预设字符串集合。5.4 Spring Boot 版本太高导致的依赖冲突这个热词很多人碰到过。Spring Boot 3.x 要求 JDK 17如果项目错用 JDK 8各种类加载异常层出不穷。另外 3.x 把javax.servlet、javax.persistence等迁到了jakarta.*命名空间老项目迁移时所有import javax.annotation.Resource要批量替换成jakarta.annotation.Resource。我的建议是新项目直接上 Boot 3.x 配 JDK 17老项目如果用了大量第三方库部分老库还没适配jakarta命名空间慎重升级升级前列个第三方依赖兼容性清单。5.5 慢 SQL 的定位和优化JdbcTemplate 没有内置慢 SQL 日志但可以在 Spring Boot 配置里开启 Spring 的 JdbcTemplate 日志看 SQL 执行时间logging: level: org.springframework.jdbc.core.JdbcTemplate: DEBUG这个日志会打出执行的 SQL 和参数列表但不会直接显示耗时。更实用的做法是用 MySQL 自身的慢查询日志slow_query_logON和long_query_time1单位秒把超过 1 秒的 SQL 全部记录出来再结合EXPLAIN分析执行计划。索引没走、全表扫描这类问题EXPLAIN的type字段一眼就能看出ALL和ref、const的差别。真正影响 JdbcTemplate 性能的往往不是封装层而是 SQL 本身。JdbcTemplate 的每次query都会走一遍 JDBC 驱动和数据库执行SQL 写得烂换任何框架都救不回来。5.6 空结果集和类型转换的几个隐蔽坑public Integer countUsers() { String sql SELECT COUNT(1) FROM user_info WHERE status ?; return jdbcTemplate.queryForObject(sql, Integer.class, 1); }当COUNT结果超过Integer.MAX_VALUE或者如果表里 records 为亿万级别Integer会不够用建议用Long.class。另一个类型坑是queryForObject(sql, LocalDate.class)某些 MySQL 驱动版本返回java.sql.Date不能直接转换为LocalDate需要rs.getDate(...).toLocalDate()手转。这类问题通常不会立即报错而是特定数据量上来之后突然抛类型转换异常。处理原则是实体类字段类型尽量和数据库返回类型一一对应拿不准就先查出来用Object接再手动转换。6. 性能优化建议让 JdbcTemplate 再快一点6.1 合理设置 fetchSizeJdbcTemplate默认是一次把所有ResultSet拉到内存里大结果集几万行以上会导致内存陡增。在 MySQL 驱动下更稳妥的做法是设置fetchSize为较小的值比如 500让驱动分批读取private void queryLargeData() { String sql SELECT * FROM huge_table; jdbcTemplate.setFetchSize(500); try { jdbcTemplate.query(sql, rs - { while (rs.next()) { // 逐行处理 } return null; }); } finally { jdbcTemplate.setFetchSize(0); // 0 表示使用默认值 } }注意MySQL 驱动对fetchSize的支持和连接事务状态有关某些旧版驱动在AUTOCOMMIT模式下不生效。遇到这种情况可以考虑Statement.setFetchSize加上按需分段查询或者用游标机制处理超大结果集。6.2 批处理分片不要一把梭前面说过batchUpdate这里再展开讲。一次性batchUpdate一万条数据数据库可能直接压力拉满而且一旦中间某条失败整批回滚还是部分回滚、日志怎么记都不好控制。我常用的分片策略public int batchInsertInSlice(ListUserInfo users, int sliceSize) { String sql INSERT INTO user_info (username, password, email) VALUES (?, ?, ?); int total 0; for (int i 0; i users.size(); i sliceSize) { ListUserInfo slice users.subList(i, Math.min(i sliceSize, users.size())); int[] result jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { Override public void setValues(PreparedStatement ps, int index) throws SQLException { UserInfo u slice.get(index); ps.setString(1, u.getUsername()); ps.setString(2, u.getPassword()); ps.setString(3, u.getEmail()); } Override public int getBatchSize() { return slice.size(); } }); total Arrays.stream(result).sum(); } return total; }分片大小我一般先按 500 起步实测数据库耗时、网络往返都正常就保持如果单批执行时间变长调小到 200。不同数据库、不同服务器配置最合适的值不一样没有万能参数跑个压测对比是王道。6.3 复用 RowMapper 而不是每次都 newRowMapper是线程安全的执行时没有内部状态所以完全可以定义为static final字段复用而不是每次查询都new一个。这在对象创建开销上微不足道但代码整洁度和可维护性提升明显也避免了一不留神在RowMapper里捕获外部可变状态导致的并发隐患。6.4 配合 Redis 做缓存别让 JdbcTemplate 背锅JdbcTemplate 本身不带缓存这是它轻量的代价也是优势。频繁执行的读操作我一般是 Service 层加一层 Redis 缓存初次从 JdbcTemplate 查完写缓存后续直接走 Redis。但要注意缓存穿透和缓存击穿的问题高并发下给热点 key 加锁、给空结果也设置短过期时间这样 JdbcTemplate 层也不会被高频请求打到崩溃。7. 应用场景扩展除了常规 CRUD 还能做什么7.1 动态 SQL 拼装与条件查询面向搜索引擎或者管理后台的筛选列表查询条件是动态的有几十种组合。用 JdbcTemplate 拼 SQL 时用StringBuilder配合 List 参数收集public ListUserInfo searchUsers(String username, String email, LocalDate startDate, LocalDate endDate) { StringBuilder sql new StringBuilder(SELECT id, username, password, email, create_time FROM user_info WHERE 11); ListObject params new ArrayList(); if (StringUtils.hasText(username)) { sql.append( AND username LIKE ?); params.add(% username %); } if (StringUtils.hasText(email)) { sql.append( AND email ?); params.add(email); } if (startDate ! null) { sql.append( AND create_time ?); params.add(startDate.atStartOfDay()); } if (endDate ! null) { sql.append( AND create_time ?); params.add(endDate.plusDays(1).atStartOfDay()); } sql.append( ORDER BY id DESC LIMIT 100); return jdbcTemplate.query(sql.toString(), USER_ROW_MAPPER, params.toArray()); }使用NamedParameterJdbcTemplateMapSqlParameterSource时“收集参数”会变成“收集 key-value”代码可读性更好。动态 SQL 拼装时WHERE 11这种技巧看着不优雅但确实能省去判断“前面是否已有条件”的逻辑实际项目里非常实用。7.2 复杂结果集一对多查询怎么映射JdbcTemplate 做简单 CRUD 很舒服做一对多关联查询时没有 MyBatis 的collection那么方便但也能处理。比如查询用户和他的多个角色public ListUserWithRolesDTO listUsersWithRoles() { String sql SELECT u.id AS user_id, u.username, r.id AS role_id, r.role_name FROM user_info u LEFT JOIN user_role ur ON u.id ur.user_id LEFT JOIN role r ON ur.role_id r.id ORDER BY u.id; MapLong, UserWithRolesDTO userMap new LinkedHashMap(); jdbcTemplate.query(sql, rs - { Long userId rs.getLong(user_id); UserWithRolesDTO dto userMap.computeIfAbsent(userId, id - { UserWithRolesDTO d new UserWithRolesDTO(); d.setId(id); d.setUsername(rs.getString(username)); d.setRoles(new ArrayList()); return d; }); dto.getRoles().add(new RoleDTO(rs.getLong(role_id), rs.getString(role_name))); }); return new ArrayList(userMap.values()); }核心思路是用Map按主键分组合并子表记录手动实现类似 MyBatis 的嵌套结果映射。SQL 里排序必须先按主表主键排序这样分组合并时顺序才稳定。7.3 数据导入导出与报表查询报表系统是 JdbcTemplate 的强项。查询结果用queryForList拿ListMapString, Object然后直接转成 JSON 输出给前端报表组件或者配合 EasyExcel 写导出。连接线上库做报表查询时建议用独立的数据源指向只读从库避免报表查询拖垮主库性能。7.4 生成环境中的批量更新策略除了批量插入批量更新也常用。例如批量给用户重置密码public int batchUpdatePassword(ListLong userIds, String newPassword) { String sql UPDATE user_info SET password ? WHERE id ?; return Arrays.stream(jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { Override public void setValues(PreparedStatement ps, int i) throws SQLException { ps.setString(1, newPassword); ps.setLong(2, userIds.get(i)); } Override public int getBatchSize() { return userIds.size(); } })).sum(); }批量更新建议也按 500 条分片并放入事务保证一批操作的原子性。8. 从一个真实项目里提炼的整合经验8.1 项目背景与取舍我之前做一个中小型后台管理系统表有三十多张业务逻辑大多集中在订单、用户、报表模块。一开始同事建议直接上 MyBatis Plus但我评估后发现系统并不需要太多复杂的动态 SQL大部分查询就是单表加几个条件表关系也比较直白没必要引入一套代码生成器和繁重的 BaseMapper 继承体系。最终选了 Spring Boot JdbcTemplate整个数据访问层用一个公共的BaseDao封装公共操作各业务模块自己写 SQL 和 RowMapper开发效率不高吗反而是同行里完成最快的。后来接了一个第二数据源做统计报表也因为是 JdbcTemplate 的配置模式扩展起来几分钟就搞定了。8.2 团队协作中的数据访问层规范用 JdbcTemplate 最怕的是每个开发者各自为政同一个查询写十种 SQL。我们在项目规范里定了几个硬性要求所有 SQL 统一走NamedParameterJdbcTemplate禁止直接用?占位符。所有 RowMapper 集中在一个XxxMapper工具类里禁止散落在 Service 里。表名前缀统一所有涉及多表查询的 DTO 单独建类不直接把实体类拉出来硬凑字段。SQL 中禁止SELECT *必须显式列字段。这些规范看着不起眼但在项目规模变大、人员流动之后它决定了你接手代码时是“痛不欲生”还是“一目了然”。8.3 JdbcTemplate 与 MyBatis 的对比什么项目选哪个每次聊到 JdbcTemplate就会被追问“和 MyBatis 比哪个好”。我的观点是没有绝对的好坏只有场景匹配度。维度JdbcTemplateMyBatis / MyBatis Plus学习成本极低会 JDBC 就能上手中等需要理解 mapper、XML、缓存机制SQL 可控性完全可控SQL 全手写可控但复杂动态 SQL 在 XML 里调试更绕开发效率简单 CRUD 需要手写 SQL略繁琐BaseMapper 和代码生成器加持下 CRUD 开发极快性能表现封装薄接近原生 JDBC轻度封装性能差距很小适合场景数据访问需求清晰、团队规模小、原生 SQL 熟练需求多变、迭代快、ORM 习惯强的团队如果你的系统有几十张表、实体关系复杂、通用 CRUD 占大头MyBatis Plus 确实省事但如果你像我一样喜欢精确控制 SQL、系统表结构稳定、又不想花精力维护 XML 映射文件和繁琐的注解JdbcTemplate 是更轻快的那把刀。9. 踩坑总结与最后的经验心得写到最后把这些年用 JdbcTemplate 的核心体会浓缩成几条给准备动手整合的朋友第一占位符参数一定要按顺序核对。用?风格写多个条件时参数顺序错一个是 JdbcTemplate 新手最容易犯的错而且不少错误要在真实数据量下才暴露。使用NamedParameterJdbcTemplate能系统性规避这个问题新代码都该默认用它。第二不要随意在 SQL 字符串里拼接关键字。动态拼 SQL 时ORDER BY字段、表名这类结构参数如果来自外部输入必须做白名单校验。这不算 JdbcTemplate 的问题是所有数据访问方案的共同底线。第三分页和超大结果集要提前规划。JdbcTemplate 没有内置的分页拦截MySQL 用LIMIT ? OFFSET ?PostgreSQL 用LIMIT ? OFFSET ?也有但参数绑定方式略有差异。大结果集必须配合fetchSize或分页不能在内存里处理几十万行数据。第四事务边界要清晰。Transactional加在 Service 层的方法上控制好粒度。太粗了锁表时间长太细了每个方法都要写一遍事务注解容易漏。事务不回滚问题排查时先确认异常类型、再确认是否走 Bean 代理、再看rollbackFor有没有配置。第五JdbcTemplate 真的不难难的是 SQL 功底。封装帮你解决了资源管理帮你统一了异常处理但 SQL 优化、索引设计、分表分库这些核心能力任何时候都绕不开。在 Spring Boot 项目里整合 JdbcTemplate本质上是选了一条“轻封装、重 SQL”的数据访问路线。你不需要理解 MyBatis 的拦截器与二级缓存机制不需要配置一堆 mapper 映射文件只需要专注于把每条 SQL 写对写快把参数传递清楚。这种朴素直接的风格在大量真实业务开发里往往比花哨的 ORM 方案更耐糙。按上面这套配置和代码去搭很快你就能体验到“原来数据库操作可以这么干净”的爽快感。
RELATED

相关推荐

AI论文写作工具实测:千笔AI写作与文途AI对比指南

AI论文写作工具实测:千笔AI写作与文途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/10 7:14:32
SEO优化软件功能全解析:从关键词研究到站点体检的实战指南

SEO优化软件功能全解析:从关键词研究到站点体检的实战指南

做SEO这么多年,我接触过不少优化软件,从免费的浏览器插件到一年好几万的企业级平台都用过。后台私信里问得最多的一个问题就是:SEO优化软件到底有哪些功能?是不是真能一键把排名做到首页?先说结论:没有任何…

📅 2026/9/10 7:14:32
PostHog GitHub Actions 密钥管理实战:组织级集中管控与 gh CLI 操作指南

PostHog GitHub Actions 密钥管理实战:组织级集中管控与 gh CLI 操作指南

PostHog GitHub Actions 密钥管理实战:组织级集中管控与 gh CLI 操作指南 【免费下载链接】posthog :hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags…

📅 2026/9/10 7:14:32
MORE NEWS

更多资讯

📰

AI代理安全加固:用E2B沙箱和Firecracker微虚拟机隔离OpenClaw风险

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

📰

分子动力学模拟矿物表面润湿性:从建模到接触角计算全攻略

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

📰

车企MBD落地为何偏爱创紫Ganzlab?——从Simulink迁移到HIL测试的国产替代实践

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

📰

Claude in Chrome:浏览器Agent的自动批准与安全分类器详解

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

📰

AI编程工具选型实战:团队协作效率的四大隐形成本解析

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

📰

考虑多容量电动汽车接入的配电网承载能力评估附Matlab代码

✅作者简介:热爱科研的Matlab仿真开发者,擅长毕业设计辅导、数学建模、数据处理、算法改进、程序设计科研仿真。🍎 往期回顾关注个人主页:完整代码获取 定制创新 论文复现私信🍊个人信条:做科研&#xff0c…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬