首页 / Spring Boot 入门教程 / 数据访问概览与 JDBC

Spring Boot 入门教程

数据访问概览与 JDBC

本教程共 48 篇 · 第 26 篇 · 更新于 2026-08-13 · 约 6 分钟阅读

Spring BootJDBCJdbcTemplate数据源HikariCP事务DataSource

本节目标:看懂 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 自带连接池选择算法,按顺序挑:

  1. HikariCP(性能最好,默认首选)
  2. Tomcat 连接池
  3. Commons DBCP2
  4. 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 是池里没空闲连接时,调用方最多等多久,等不到就抛异常,防止请求无限挂起。

Note

classpath 上有多个嵌入式数据库(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建表、删表等 DDLvoid
update增、删、改受影响行数
query多条记录查询List
queryForObject单条记录或单值查询对象
batchUpdate批量增删改int[]

建表、初始化这种一次性操作用 executejdbcTemplate.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,看看对象和表怎么自动映射。