Java JdbcTemplate 实战指南:用 Spring 轻量完成数据库增删改查

简介

JdbcTemplateSpring JDBC 模块提供的数据库访问工具类。

它解决的是原生 JDBC 里很常见的一类重复工作:

text 复制代码
获取连接
创建 Statement
绑定 SQL 参数
遍历 ResultSet
关闭连接和结果集
处理 SQLException

原生 JDBC 能做的事情,JdbcTemplate 基本都能做。

区别在于,JdbcTemplate 把连接管理、资源释放、异常转换这些固定流程封装好了,业务代码只需要关注:

text 复制代码
执行什么 SQL
传什么参数
结果怎么映射

一句话概括:

text 复制代码
JdbcTemplate 是 Spring 提供的轻量级数据库操作工具,适合直接写 SQL,又不想写一堆 JDBC 模板代码的场景。

原生 JDBC 写法

先看一段原生 JDBC 查询代码:

java 复制代码
String sql = "select id, username, email, age from users where id = ?";

Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;

try {
    connection = dataSource.getConnection();
    statement = connection.prepareStatement(sql);
    statement.setLong(1, 1L);
    resultSet = statement.executeQuery();

    if (resultSet.next()) {
        User user = new User();
        user.setId(resultSet.getLong("id"));
        user.setUsername(resultSet.getString("username"));
        user.setEmail(resultSet.getString("email"));
        user.setAge(resultSet.getInt("age"));
        return user;
    }

    return null;
} finally {
    if (resultSet != null) {
        resultSet.close();
    }
    if (statement != null) {
        statement.close();
    }
    if (connection != null) {
        connection.close();
    }
}

真正和业务有关的只有几件事:

  • SQL 是什么
  • 参数是什么
  • 每一列怎么放进 User

其他部分基本都是固定流程。

JdbcTemplate 写法

换成 JdbcTemplate 后:

java 复制代码
String sql = "select id, username, email, age from users where id = ?";

User user = jdbcTemplate.queryForObject(sql, (rs, rowNum) -> {
    User item = new User();
    item.setId(rs.getLong("id"));
    item.setUsername(rs.getString("username"));
    item.setEmail(rs.getString("email"));
    item.setAge(rs.getInt("age"));
    return item;
}, 1L);

代码明显少很多。

连接什么时候拿、什么时候关,ResultSet 怎么关闭,异常怎么转换,都交给 JdbcTemplate 处理。

JdbcTemplate 做了什么

JdbcTemplate 大致处在这个位置:

text 复制代码
业务代码
  |
  v
JdbcTemplate
  |
  v
DataSource
  |
  v
数据库

它本身不负责连接池。

连接来自 DataSource

Spring Boot 项目里,只要配置了数据源,并引入 spring-boot-starter-jdbc,通常可以直接注入:

java 复制代码
@Repository
public class UserRepository {

    private final JdbcTemplate jdbcTemplate;

    public UserRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
}

常见能力有这些:

  • update:执行新增、修改、删除
  • queryForObject:查询一条记录或一个值
  • query:查询列表
  • batchUpdate:批量执行
  • execute:执行 DDL 或普通 SQL
  • RowMapper:把一行结果映射成一个对象
  • NamedParameterJdbcTemplate:使用命名参数写 SQL

Maven 依赖

Spring Boot 项目常用依赖:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

如果是本地演示,也可以使用 H2 数据库:

xml 复制代码
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

数据源配置

MySQL 为例:

yaml 复制代码
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

Spring Boot 会根据这些配置创建 DataSource

同时,JdbcTemplate 也会自动注册成 Bean

准备演示表

下面的示例围绕用户表展开。

sql 复制代码
DROP TABLE IF EXISTS users;

CREATE TABLE users (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  username VARCHAR(50) NOT NULL,
  email VARCHAR(100) NOT NULL,
  age INT NOT NULL,
  status VARCHAR(20) NOT NULL,
  created_at DATETIME NOT NULL
);

INSERT INTO users (username, email, age, status, created_at) VALUES
('张三', 'zhangsan@example.com', 20, 'ACTIVE', '2026-01-01 10:00:00'),
('李四', 'lisi@example.com', 25, 'ACTIVE', '2026-01-02 10:00:00'),
('王五', 'wangwu@example.com', 17, 'DISABLED', '2026-01-03 10:00:00');

实体类

java 复制代码
import java.time.LocalDateTime;

public class User {
    private Long id;
    private String username;
    private String email;
    private Integer age;
    private String status;
    private LocalDateTime createdAt;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public LocalDateTime getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(LocalDateTime createdAt) {
        this.createdAt = createdAt;
    }
}

RowMapper:结果集映射

JdbcTemplate 查询对象时,需要告诉它:

text 复制代码
ResultSet 的一行数据怎么变成 Java 对象。

这件事由 RowMapper 完成。

java 复制代码
import org.springframework.jdbc.core.RowMapper;

import java.sql.Timestamp;

private static final RowMapper<User> USER_ROW_MAPPER = (rs, rowNum) -> {
    User user = new User();
    user.setId(rs.getLong("id"));
    user.setUsername(rs.getString("username"));
    user.setEmail(rs.getString("email"));
    user.setAge(rs.getInt("age"));
    user.setStatus(rs.getString("status"));

    Timestamp createdAt = rs.getTimestamp("created_at");
    if (createdAt != null) {
        user.setCreatedAt(createdAt.toLocalDateTime());
    }

    return user;
};

rowNum 表示当前行号,从 0 开始。

update:新增数据

update 可以执行 insertupdatedelete

新增用户:

java 复制代码
public int save(User user) {
    String sql = """
            insert into users (username, email, age, status, created_at)
            values (?, ?, ?, ?, ?)
            """;

    return jdbcTemplate.update(
            sql,
            user.getUsername(),
            user.getEmail(),
            user.getAge(),
            user.getStatus(),
            user.getCreatedAt()
    );
}

返回值是受影响行数。

text 复制代码
返回 1,表示插入成功 1 行。

如果项目还在使用低版本 Java,不支持文本块,可以写成普通字符串:

java 复制代码
String sql = "insert into users (username, email, age, status, created_at) values (?, ?, ?, ?, ?)";

update:修改数据

java 复制代码
public int updateEmail(Long id, String email) {
    String sql = "update users set email = ? where id = ?";
    return jdbcTemplate.update(sql, email, id);
}

update:删除数据

java 复制代码
public int deleteById(Long id) {
    String sql = "delete from users where id = ?";
    return jdbcTemplate.update(sql, id);
}

queryForObject:查询单个值

统计用户数量:

java 复制代码
public long countAll() {
    String sql = "select count(*) from users";
    Long count = jdbcTemplate.queryForObject(sql, Long.class);
    return count == null ? 0L : count;
}

查询某个字段:

java 复制代码
public String findEmailById(Long id) {
    String sql = "select email from users where id = ?";
    return jdbcTemplate.queryForObject(sql, String.class, id);
}

queryForObject 查询单个值时,第二个参数可以写目标类型:

java 复制代码
Long.class
Integer.class
String.class
BigDecimal.class

queryForObject:查询单个对象

java 复制代码
public User findById(Long id) {
    String sql = """
            select id, username, email, age, status, created_at
            from users
            where id = ?
            """;

    return jdbcTemplate.queryForObject(sql, USER_ROW_MAPPER, id);
}

这段代码适合"数据必须存在"的场景。

如果查不到数据,queryForObject 会抛出:

text 复制代码
EmptyResultDataAccessException

如果查出了多条数据,会抛出:

text 复制代码
IncorrectResultSizeDataAccessException

所以 queryForObject 的语义很明确:

text 复制代码
期望结果必须是一条。

查询可能不存在的数据

如果查询结果可能不存在,可以用 query 返回列表,再取第一条。

java 复制代码
import java.util.List;
import java.util.Optional;

public Optional<User> findOptionalById(Long id) {
    String sql = """
            select id, username, email, age, status, created_at
            from users
            where id = ?
            """;

    List<User> users = jdbcTemplate.query(sql, USER_ROW_MAPPER, id);

    if (users.isEmpty()) {
        return Optional.empty();
    }

    return Optional.of(users.get(0));
}

这种写法不会把"查不到"当成异常。

业务层可以这样处理:

java 复制代码
User user = userRepository.findOptionalById(id)
        .orElseThrow(() -> new IllegalArgumentException("用户不存在"));

query:查询列表

查询全部用户:

java 复制代码
public List<User> findAll() {
    String sql = """
            select id, username, email, age, status, created_at
            from users
            order by id desc
            """;

    return jdbcTemplate.query(sql, USER_ROW_MAPPER);
}

按状态查询:

java 复制代码
public List<User> findByStatus(String status) {
    String sql = """
            select id, username, email, age, status, created_at
            from users
            where status = ?
            order by id desc
            """;

    return jdbcTemplate.query(sql, USER_ROW_MAPPER, status);
}

分页查询:

java 复制代码
public List<User> findPage(int page, int size) {
    int offset = (page - 1) * size;

    String sql = """
            select id, username, email, age, status, created_at
            from users
            order by id desc
            limit ? offset ?
            """;

    return jdbcTemplate.query(sql, USER_ROW_MAPPER, size, offset);
}

BeanPropertyRowMapper:自动映射

手写 RowMapper 最清楚,也最可控。

如果表字段和实体属性比较规整,可以使用 BeanPropertyRowMapper

java 复制代码
import org.springframework.jdbc.core.BeanPropertyRowMapper;

public List<User> findAllByBeanMapper() {
    String sql = "select id, username, email, age, status, created_at from users";

    return jdbcTemplate.query(
            sql,
            new BeanPropertyRowMapper<>(User.class)
    );
}

它可以处理常见的下划线转驼峰:

text 复制代码
created_at -> createdAt

不过它依赖字段名和属性名的匹配。

如果 SQL 字段比较复杂,或者结果来自多表关联,手写 RowMapper 通常更直观。

BeanPropertyRowMapper 配合别名

如果字段名和属性名不一致,可以在 SQL 里使用别名。

java 复制代码
String sql = """
        select
          id,
          username as username,
          email as email,
          created_at as created_at
        from users
        """;

对于复杂查询,别名写清楚,可以减少映射问题。

返回自增主键

新增数据后,如果需要拿到数据库生成的主键,可以使用 KeyHolder

java 复制代码
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;

import java.sql.PreparedStatement;
import java.sql.Statement;

public Long saveAndReturnId(User user) {
    String sql = """
            insert into users (username, email, age, status, created_at)
            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.getEmail());
        ps.setInt(3, user.getAge());
        ps.setString(4, user.getStatus());
        ps.setObject(5, user.getCreatedAt());
        return ps;
    }, keyHolder);

    Number key = keyHolder.getKey();
    return key == null ? null : key.longValue();
}

这种写法常用于:

text 复制代码
新增主表后,继续插入子表。

batchUpdate:批量写入

批量插入可以减少多次数据库往返。

java 复制代码
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.List;

import org.springframework.jdbc.core.BatchPreparedStatementSetter;

public int[] batchSave(List<User> users) {
    String sql = """
            insert into users (username, email, age, status, created_at)
            values (?, ?, ?, ?, ?)
            """;

    return jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
        @Override
        public void setValues(PreparedStatement ps, int i) throws SQLException {
            User user = users.get(i);
            ps.setString(1, user.getUsername());
            ps.setString(2, user.getEmail());
            ps.setInt(3, user.getAge());
            ps.setString(4, user.getStatus());
            ps.setObject(5, user.getCreatedAt());
        }

        @Override
        public int getBatchSize() {
            return users.size();
        }
    });
}

返回值是每条 SQL 影响的行数。

还有一种更短的写法:

java 复制代码
public int[] batchUpdateStatus(List<Long> ids, String status) {
    String sql = "update users set status = ? where id = ?";

    List<Object[]> args = ids.stream()
            .map(id -> new Object[]{status, id})
            .toList();

    return jdbcTemplate.batchUpdate(sql, args);
}

如果项目使用 Java 8,可以改成:

java 复制代码
List<Object[]> args = ids.stream()
        .map(id -> new Object[]{status, id})
        .collect(Collectors.toList());

execute:执行普通 SQL

execute 常用于执行不返回结果集的 SQL。

java 复制代码
jdbcTemplate.execute("""
        create table if not exists operation_logs (
          id bigint primary key auto_increment,
          content varchar(200) not null
        )
        """);

实际业务里,建表更常交给迁移工具,比如 FlywayLiquibase

execute 更适合临时初始化、测试场景或少量管理类 SQL。

NamedParameterJdbcTemplate

JdbcTemplate 默认使用 ? 占位符。

参数少时很清楚:

java 复制代码
String sql = "select * from users where status = ? and age >= ?";

参数一多,可读性就会下降。

NamedParameterJdbcTemplate 支持命名参数:

java 复制代码
String sql = """
        select id, username, email, age, status, created_at
        from users
        where status = :status
          and age >= :minAge
        order by id desc
        """;

完整示例:

java 复制代码
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;

public class UserQueryRepository {

    private final NamedParameterJdbcTemplate namedJdbcTemplate;

    public UserQueryRepository(NamedParameterJdbcTemplate namedJdbcTemplate) {
        this.namedJdbcTemplate = namedJdbcTemplate;
    }

    public List<User> findByCondition(String status, int minAge) {
        String sql = """
                select id, username, email, age, status, created_at
                from users
                where status = :status
                  and age >= :minAge
                order by id desc
                """;

        MapSqlParameterSource params = new MapSqlParameterSource()
                .addValue("status", status)
                .addValue("minAge", minAge);

        return namedJdbcTemplate.query(sql, params, USER_ROW_MAPPER);
    }
}

IN 查询

NamedParameterJdbcTemplate 处理 IN 条件很方便。

java 复制代码
public List<User> findByIds(List<Long> ids) {
    String sql = """
            select id, username, email, age, status, created_at
            from users
            where id in (:ids)
            """;

    MapSqlParameterSource params = new MapSqlParameterSource()
            .addValue("ids", ids);

    return namedJdbcTemplate.query(sql, params, USER_ROW_MAPPER);
}

如果使用普通 JdbcTemplateIN 参数需要自己拼出对应数量的 ?,代码会更啰嗦。

事务控制

JdbcTemplate 可以直接配合 @Transactional 使用。

常见写法是:

text 复制代码
Repository 负责 SQL
Service 负责事务和业务流程

示例:

java 复制代码
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Transactional
    public Long register(User user) {
        Long userId = userRepository.saveAndReturnId(user);

        userRepository.insertRegisterLog(userId, "用户注册成功");

        return userId;
    }
}

如果 insertRegisterLog 抛出运行时异常,前面的用户新增也会回滚。

完整实战 Demo:用户 Repository

下面是一份较完整的 Repository 示例,包含新增、返回主键、查询、分页、批量更新、删除。

java 复制代码
package com.example.demo.repository;

import com.example.demo.entity.User;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;

import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.List;
import java.util.Optional;

@Repository
public class UserRepository {

    private final JdbcTemplate jdbcTemplate;

    public UserRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    private static final RowMapper<User> USER_ROW_MAPPER = (rs, rowNum) -> {
        User user = new User();
        user.setId(rs.getLong("id"));
        user.setUsername(rs.getString("username"));
        user.setEmail(rs.getString("email"));
        user.setAge(rs.getInt("age"));
        user.setStatus(rs.getString("status"));

        Timestamp createdAt = rs.getTimestamp("created_at");
        if (createdAt != null) {
            user.setCreatedAt(createdAt.toLocalDateTime());
        }

        return user;
    };

    public Long saveAndReturnId(User user) {
        String sql = """
                insert into users (username, email, age, status, created_at)
                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.getEmail());
            ps.setInt(3, user.getAge());
            ps.setString(4, user.getStatus());
            ps.setObject(5, user.getCreatedAt());
            return ps;
        }, keyHolder);

        Number key = keyHolder.getKey();
        return key == null ? null : key.longValue();
    }

    public int updateEmail(Long id, String email) {
        String sql = "update users set email = ? where id = ?";
        return jdbcTemplate.update(sql, email, id);
    }

    public int deleteById(Long id) {
        String sql = "delete from users where id = ?";
        return jdbcTemplate.update(sql, id);
    }

    public Optional<User> findById(Long id) {
        String sql = """
                select id, username, email, age, status, created_at
                from users
                where id = ?
                """;

        try {
            User user = jdbcTemplate.queryForObject(sql, USER_ROW_MAPPER, id);
            return Optional.ofNullable(user);
        } catch (EmptyResultDataAccessException e) {
            return Optional.empty();
        }
    }

    public List<User> findByStatus(String status) {
        String sql = """
                select id, username, email, age, status, created_at
                from users
                where status = ?
                order by id desc
                """;

        return jdbcTemplate.query(sql, USER_ROW_MAPPER, status);
    }

    public List<User> findPage(int page, int size) {
        int offset = (page - 1) * size;

        String sql = """
                select id, username, email, age, status, created_at
                from users
                order by id desc
                limit ? offset ?
                """;

        return jdbcTemplate.query(sql, USER_ROW_MAPPER, size, offset);
    }

    public int[] batchUpdateStatus(List<Long> ids, String status) {
        String sql = "update users set status = ? where id = ?";

        return jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
            @Override
            public void setValues(PreparedStatement ps, int i) throws SQLException {
                ps.setString(1, status);
                ps.setLong(2, ids.get(i));
            }

            @Override
            public int getBatchSize() {
                return ids.size();
            }
        });
    }
}

Controller 示例

配合 Spring MVC 可以很快写出一个简单接口。

java 复制代码
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.util.List;

@RestController
@RequestMapping("/users")
public class UserController {

    private final UserRepository userRepository;

    public UserController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @PostMapping
    public Long create(@RequestBody User user) {
        user.setStatus("ACTIVE");
        user.setCreatedAt(LocalDateTime.now());
        return userRepository.saveAndReturnId(user);
    }

    @GetMapping("/{id}")
    public User detail(@PathVariable Long id) {
        return userRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("用户不存在"));
    }

    @GetMapping
    public List<User> list() {
        return userRepository.findByStatus("ACTIVE");
    }
}

参数绑定和 SQL 注入

JdbcTemplate 支持参数绑定:

java 复制代码
String sql = "select * from users where email = ?";
User user = jdbcTemplate.queryForObject(sql, USER_ROW_MAPPER, email);

这里的 email 不会直接拼到 SQL 字符串里,而是作为参数传给数据库驱动。

这种写法比字符串拼接更稳妥:

java 复制代码
String sql = "select * from users where email = '" + email + "'";

常见原则:

text 复制代码
值用参数绑定
表名、字段名、排序字段不能直接接收外部输入

如果排序字段来自前端,可以做白名单映射:

java 复制代码
public String resolveSortColumn(String sort) {
    if ("age".equals(sort)) {
        return "age";
    }
    if ("createdAt".equals(sort)) {
        return "created_at";
    }
    return "id";
}

异常处理

JdbcTemplate 会把底层 SQLException 转成 Spring 的 DataAccessException 体系。

常见异常有:

异常 常见原因
EmptyResultDataAccessException queryForObject 没查到数据
IncorrectResultSizeDataAccessException 期望一条,实际多条
DuplicateKeyException 唯一键冲突
BadSqlGrammarException SQL 语法错误、表名字段名错误
DataIntegrityViolationException 非空约束、外键约束、字段长度等问题

在业务层通常不需要捕获所有数据库异常。

更常见的做法是:

text 复制代码
Repository 层处理"查不到"这种正常分支
全局异常处理统一处理其他数据库异常

JdbcTemplate 和 MyBatis 的区别

JdbcTemplateMyBatis 都是直接操作 SQL 的方案。

差异大致可以这样理解:

对比点 JdbcTemplate MyBatis
SQL 位置 多数写在 Java 代码里 常写在 XML 或注解里
映射能力 轻量,常用 RowMapper 更完整,支持复杂映射
学习成本 较低 中等
动态 SQL 手写字符串或命名参数 动态 SQL 支持更成熟
适合场景 简单 CRUD、报表查询、小型模块 复杂 SQL、较多数据访问层代码

JdbcTemplate 的优势是直接、轻量、接近 JDBC。

如果项目里 SQL 很复杂、映射关系很多,MyBatis 通常会更省维护成本。

如果只是少量 SQL、内部工具、简单后台、数据修复脚本接口,JdbcTemplate 用起来很顺手。

常见使用建议

Repository 层统一放 SQL

SQL 通常放在 RepositoryDao 层。

java 复制代码
@Repository
public class UserRepository {
}

Service 层负责业务流程,少直接拼 SQL。

复杂映射优先手写 RowMapper

BeanPropertyRowMapper 很省事,但它更适合简单对象。

多表关联、字段别名、枚举转换、时间转换、金额转换等场景,手写 RowMapper 会更清楚。

查询可能为空时返回 Optional

对于按 ID 查询、按唯一字段查询这类方法,可以返回 Optional

java 复制代码
public Optional<User> findByEmail(String email) {
    String sql = """
            select id, username, email, age, status, created_at
            from users
            where email = ?
            """;

    List<User> users = jdbcTemplate.query(sql, USER_ROW_MAPPER, email);

    if (users.isEmpty()) {
        return Optional.empty();
    }

    return Optional.of(users.get(0));
}

方法签名本身就表达了:

text 复制代码
这个用户可能存在,也可能不存在。

大批量数据注意分批

batchUpdate 适合批量操作,但不是一次塞得越多越好。

如果一次处理几十万条数据,通常需要拆批:

text 复制代码
每 500 条或 1000 条提交一批。

具体大小要看数据库、连接池、SQL 复杂度和网络情况。

动态条件的参数绑定

动态查询经常会写成这样:

java 复制代码
String sql = "select * from users where 1 = 1";

if (status != null) {
    sql += " and status = '" + status + "'";
}

这种写法不利于参数绑定。

更合适的是保留参数列表:

java 复制代码
StringBuilder sql = new StringBuilder("""
        select id, username, email, age, status, created_at
        from users
        where 1 = 1
        """);

List<Object> args = new ArrayList<>();

if (status != null) {
    sql.append(" and status = ?");
    args.add(status);
}

if (minAge != null) {
    sql.append(" and age >= ?");
    args.add(minAge);
}

List<User> users = jdbcTemplate.query(
        sql.toString(),
        USER_ROW_MAPPER,
        args.toArray()
);

如果动态条件很多,NamedParameterJdbcTemplate 会更容易维护。

常用方法汇总

方法 作用 常见场景
update(sql, args...) 执行新增、修改、删除 insert/update/delete
queryForObject(sql, Class<T>, args...) 查询单个值 统计、单字段
queryForObject(sql, RowMapper<T>, args...) 查询单个对象 按主键查询且必须存在
query(sql, RowMapper<T>, args...) 查询列表 列表页、条件查询
batchUpdate(sql, batchArgs) 批量执行 批量插入、批量更新
execute(sql) 执行普通 SQL DDL、初始化脚本
NamedParameterJdbcTemplate 命名参数 SQL 多条件查询、IN 查询
RowMapper<T> 单行结果映射对象 自定义实体映射
BeanPropertyRowMapper<T> 自动属性映射 简单表和简单实体

总结

JdbcTemplate 的定位很清楚:

text 复制代码
保留 SQL 的直接控制感,同时减少 JDBC 模板代码。

它适合这些场景:

  • SQL 数量不多
  • 查询逻辑比较直接
  • 不想引入完整 ORM 或 Mapper 框架
  • 需要快速写内部工具、管理后台、数据处理逻辑
  • 希望直接控制 SQL 和参数

实际使用时,重点放在几件事上:

  • 用参数绑定,不拼接外部输入
  • RowMapper 管好结果映射
  • queryOptional 表达可能查不到的数据
  • NamedParameterJdbcTemplate 处理多参数和 IN 查询
  • @Transactional 管住多个 SQL 的一致性

掌握这些内容后,JdbcTemplate 已经可以覆盖大多数轻量数据库访问场景。

相关推荐
未秃头的程序猿1 小时前
别再让大模型单打独斗了!Java 多 Agent 协作实战:任务拆解+结果聚合
java·后端·ai编程
右耳朵猫AI1 小时前
Java & JVM技术周刊 2026年第20周
java·开发语言·jvm
人道领域1 小时前
【LeetCode刷题日记】538.把二叉搜索树转换为累加树
java·开发语言·后端·算法·leetcode
铁皮哥1 小时前
【后端开发】什么是守护线程,和普通线程有什么区别?
java·开发语言·数据库·人工智能·python·spring·intellij-idea
西凉的悲伤1 小时前
Spring Boot + ShardingSphere 介绍
java·spring boot·后端·shardingsphere·分库分表
Lsk_Smion1 小时前
力扣实训 _ [33].搜索旋转排序数组 _ [92].翻转链表Ⅱ
java·数据结构·算法
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第86题】【Mysql篇】第16题:MySQL 中锁的种类与行锁实现原理?
java·开发语言·数据库·mysql·面试
code2roc1 小时前
SpringBoot整合Milvus向量数据库
数据库·spring boot·milvus·向量化
苏渡苇1 小时前
Seata 番外篇:使用 docker-compose 部署 Seata Server(TC)及 K8S 部署 Seata 高可用
spring boot·docker·微服务·容器·kubernetes·seata·springcloud