数据访问概览与 JDBC
本教程共 48 篇 · 第 26 篇 · 更新于 2026-08-13 · 约 6 分钟阅读
本节目标:看懂 Spring 数据访问的层次,会配置数据源,用 JdbcTemplate 完成增删改查,并理解事务的基本用法。
数据访问的三种姿势
Java 访问关系型数据库,绕不开 JDBC。JDBC 是 Java 官方的数据库访问标准,定义了连接数据库、执行 SQL 的接口。直接用 JDBC 写代码很痛苦:要手动管理连接、处理异常、转换结果集。
Spring 在 JDBC 之上做了三层封装:
| 方案 | 封装程度 | 适合场景 |
|---|---|---|
| JdbcTemplate | 轻封装 | 少量 SQL,想自己控制 |
| Spring Data JDBC | 中封装 | 简单实体,不想写 SQL |
| Spring Data JPA | 重封装 | 复杂领域模型,对象思维 |
官方文档的原话是:从直接用 JDBC,到 JdbcTemplate,再到 Hibernate 这类 ORM,Spring 全支持。选哪种看项目复杂度,没有绝对答案。
三种方案也不冲突。同一个项目里,核心业务用 JPA,报表统计用 JdbcTemplate 写原生 SQL,很常见。工具是拿来组合的,不是选了一个就绑死。
数据源:连接从哪来
DataSource 是 JDBC 标准里的连接工厂。数据库连接很贵:建立一次要经过网络握手、身份认证、分配内存,几十毫秒起步。每个请求都新建连接,一半时间耗在连接上,性能扛不住。
所以生产环境都用连接池。可以把它想成图书馆的借书台:提前备好一批书(连接),读者(请求)来借,用完还回去,别人接着借。
Spring Boot 自带连接池选择算法,按顺序挑:
- HikariCP(性能最好,默认首选)
- Tomcat 连接池
- Commons DBCP2
- Oracle UCP
只要引入 spring-boot-starter-jdbc,HikariCP 就自动在 classpath 里了,你什么都不用配。想强制指定某个池,用 spring.datasource.type 属性。
配置数据源
开发时用 H2 内存数据库最省事。H2 是嵌入式数据库,Spring Boot 能自动配置,连 URL 都不用写,只要加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
H2 的默认连接是 jdbc:h2:mem:testdb,数据存在内存里,应用一停就清空。开发期图省事无所谓,但要清楚:内存库不提供持久化,需要的数据要在启动时初始化(第 30 章讲)。
连接真实数据库(比如 MySQL)时,在 application.yml 里写连接信息:
spring:
datasource:
url: "jdbc:mysql://localhost:3306/shop"
username: "root"
password: "123456"
Note只要设置了
spring.datasource.url,Spring Boot 就按真实数据库配置,不再自动配嵌入式数据库。驱动类大多能从 URL 推断出来,不用写driver-class-name。
连接池参数用 spring.datasource.hikari.* 调:
spring:
datasource:
hikari:
connection-timeout: 20000 # 借不到连接最多等 20 秒
maximum-pool-size: 10 # 池里最多 10 个连接
minimum-idle: 2 # 保持至少 2 个空闲连接
两个参数值得多说一句。maximum-pool-size 不是越大越好:连接多了,数据库端负载也高。起步值一般按「核数 × 2 + 有效磁盘数」估算。connection-timeout 是池里没空闲连接时,调用方最多等多久,等不到就抛异常,防止请求无限挂起。
Noteclasspath 上有多个嵌入式数据库(H2、HSQLDB 同时引入)时,用
spring.datasource.embedded-database-connection指定用哪个。测试场景想让每个测试上下文用独立内存库,把spring.datasource.generate-unique-name设为true。
JdbcTemplate:写第一段数据访问
JdbcTemplate 由 Spring Boot 自动配置,直接注入就能用:
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class ProductRepository {
private final JdbcTemplate jdbcTemplate;
public ProductRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
// 增、删、改都用 update,返回受影响的行数
public int save(String name, BigDecimal price) {
return jdbcTemplate.update(
"insert into product (name, price) values (?, ?)",
name, price);
}
public int deleteById(Long id) {
return jdbcTemplate.update("delete from product where id = ?", id);
}
}
? 是占位符,参数按顺序传入。这比拼字符串安全得多,能防 SQL 注入,初学者要养成习惯。
JdbcTemplate 的常用方法就五个,覆盖九成场景:
| 方法 | 用途 | 返回值 |
|---|---|---|
execute | 建表、删表等 DDL | void |
update | 增、删、改 | 受影响行数 |
query | 多条记录查询 | List |
queryForObject | 单条记录或单值查询 | 对象 |
batchUpdate | 批量增删改 | int[] |
建表、初始化这种一次性操作用 execute:jdbcTemplate.execute("create table product (id bigint, name varchar(50), price decimal(10,2))")。批量导入用 batchUpdate,一次提交多条,比循环 update 快一个数量级。
查询与 RowMapper
查询要处理结果集。RowMapper 负责把一行数据变成 Java 对象:
public Product findById(Long id) {
return jdbcTemplate.queryForObject(
"select * from product where id = ?",
(rs, rowNum) -> new Product(
rs.getLong("id"),
rs.getString("name"),
rs.getBigDecimal("price")),
id);
}
public List<Product> findAll() {
return jdbcTemplate.query(
"select * from product",
(rs, rowNum) -> new Product(
rs.getLong("id"),
rs.getString("name"),
rs.getBigDecimal("price")));
}
queryForObject 查单条,查不到会抛异常;query 查列表。lambda 里的 rs 就是 JDBC 的 ResultSet,按列名取值。
Tip字段多的实体可以换
BeanPropertyRowMapper,它按列名和属性名自动映射,少写样板代码。列名用下划线、属性用驼峰时注意:JPA 有命名策略,会自动做下划线映射;裸 JdbcTemplate 没有自动蛇形映射,自己写 SQL 时列名要写对。
命名参数:SQL 更好读
SQL 里 ? 多了容易数错顺序。NamedParameterJdbcTemplate 用名字代替位置:
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
@Repository
public class ProductRepository {
private final NamedParameterJdbcTemplate template;
public ProductRepository(NamedParameterJdbcTemplate template) {
this.template = template;
}
public int updatePrice(Long id, BigDecimal price) {
MapSqlParameterSource params = new MapSqlParameterSource();
params.addValue("id", id);
params.addValue("price", price);
return template.update(
"update product set price = :price where id = :id",
params);
}
}
updatePrice(1L, new BigDecimal("99")) 这样的调用,一眼能看出哪个值绑哪个参数,可读性好很多。
参数多时还有更省事的绑法:BeanPropertySqlParameterSource 直接把对象属性当命名参数。实体字段名和 SQL 里的参数名对上,一个对象全搞定:
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
public int updatePrice(Product product) {
return template.update(
"update product set price = :price where id = :id",
new BeanPropertySqlParameterSource(product));
}
Product 要有 getId()、getPrice(),SQL 里 :id、:price 就自动取到值。字段一多,这种绑法的手感优势就出来了。
JdbcClient:新一代写法
Spring Framework 6.1 起(Spring Boot 4.x 主推)有了 JdbcClient,把 JdbcTemplate 的链式风格再封装一层。它是官方文档在 JDBC 章节第一个提到的类,值得学:
import org.springframework.jdbc.core.simple.JdbcClient;
@Repository
public class ProductRepository {
private final JdbcClient jdbcClient;
public ProductRepository(JdbcClient jdbcClient) {
this.jdbcClient = jdbcClient;
}
public List<Product> findCheaperThan(BigDecimal maxPrice) {
return jdbcClient.sql("select * from product where price <= :price")
.param("price", maxPrice)
.query(Product.class)
.list();
}
public int updateName(Long id, String name) {
return jdbcClient.sql("update product set name = :name where id = :id")
.param("id", id)
.param("name", name)
.update();
}
}
.sql() 写 SQL,.param() 绑参数,.query() 查、.update() 改,一路点下去,代码像在读书。
想拿单条结果,.query(Product.class).single();只要一列的值,.query(String.class).list()。.query(Product.class) 这种形式依赖 Spring 的实体映射器,要求列名和属性名对得上,Java 17 以上还能自动识别 record 类型。
Note版本差异:JdbcClient 是 Spring Framework 6.1(对应 Spring Boot 3.2)引入的,Spring Boot 4.x 完全支持。网上大量 2.x 教程里只有 JdbcTemplate,看到 JdbcClient 不认识很正常,它没有替换谁,只是多了一个更顺手的选项。
事务:要么全成功,要么全不成功
转账场景:扣钱和加钱两步,中间失败就出大问题。事务保证这组操作原子性——要么全成功,要么全部回滚。
Spring 用 @Transactional 声明事务边界,最省心:
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class TransferService {
private final AccountRepository accountRepository;
public TransferService(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
accountRepository.deduct(fromId, amount);
// 这里抛异常,上面的扣款也会回滚
accountRepository.add(toId, amount);
}
}
方法内任意一步抛异常,整个事务回滚。运行时异常默认触发回滚,受检异常不触发——想让它也回滚,用 @Transactional(rollbackFor = Exception.class)。
只读操作可以标注只读事务,给数据库优化留空间:
@Transactional(readOnly = true)
public List<Product> findCheap() {
return productRepository.findCheaperThan(new BigDecimal("100"));
}
事务之间还能互相嵌套,这涉及传播行为(propagation):默认 REQUIRED 表示「有事务就加入,没有就新建」,REQUIRES_NEW 表示「无论如何开新事务」。入门先记默认值,面试问到再展开。
Warning
@Transactional要加在 public 方法上,且通过 Spring 代理调用才生效。同类里this.方法()自调用会绕过代理,事务失效,这是面试高频坑。
小结
数据访问从 JDBC 出发,有三条主线:JdbcTemplate 轻量直接,NamedParameterJdbcTemplate 参数清晰,JdbcClient 是 4.x 时代的新宠。数据源交给 HikariCP 连接池,事务用 @Transactional 声明即可。
下一节进入 Spring Data JPA,看看对象和表怎么自动映射。