ToB企服应用市场:ToB评测及商务社交产业平台

标题: Spring Boot 集成 JdbcTemplate(盘它!) [打印本页]

作者: 去皮卡多    时间: 2024-8-26 12:00
标题: Spring Boot 集成 JdbcTemplate(盘它!)
Spring Boot 集成 JdbcTemplate

基本介绍

JdbcTemplate 概念

JdbcTemplate 是 Spring Framework 提供的一个用于简化 JDBC(Java Database Connectivity)操纵的工具类。它封装了复杂的 JDBC API,使开发者能够更方便、快捷地执行数据库操纵,如查询、插入、更新和删除。通过利用 JdbcTemplate,我们可以制止大量的样板代码,如创建连接、处理 SQL 非常和关闭资源等。这不仅提高了开发效率,还使代码更加简便、可读。
JdbcTemplate 优势

在企业级应用开发中,数据访问层(Data Access Layer,DAL)是核心组成部门。JdbcTemplate 的优势在于:

JdbcTemplate 应用场景

JdbcTemplate 实用于以下几种场景:

NamedParameterJdbcTemplate 概念

NamedParameterJdbcTemplate 是 JdbcTemplate 的一个扩展版本,它引入了命名参数的概念,使得 SQL 语句更加易读和维护。利用 NamedParameterJdbcTemplate,可以通过参数名称而不是位置来绑定参数,制止了 SQL 中的位置参数(?)带来的混淆和维护本钱。对于复杂 SQL 操纵和需要动态构建 SQL 的场景,NamedParameterJdbcTemplate 提供了更大的灵活性。
准备工作

在利用 JdbcTemplate 和 NamedParameterJdbcTemplate 之前,首先需要进行一些基础的准备工作。这部门内容包括项目情况的配置、数据库的设置以及实体类与数据库表的映射。这些准备工作是整个项目开发的基础,确保我们在接下来的实践中能够顺利地实现各种数据库操纵。
项目情况配置

Spring Boot版本选择

Spring Boot 提供了开箱即用的功能,极大地简化了 Spring 应用的开发。在选择 Spring Boot 版本时,发起利用最新版的稳固版本,以得到最新的功能和性能优化,同时确保兼容性和安全性。
推荐版本:

Maven 依赖配置

在 Spring Boot 项目中,JdbcTemplate 和 NamedParameterJdbcTemplate 默认包罗在 spring-boot-starter-jdbc 依赖中,因此只需在项目的构建文件中添加相关依赖即可。
Maven 配置:
  1. <dependency>
  2.     <groupId>org.springframework.boot</groupId>
  3.     <artifactId>spring-boot-starter-jdbc</artifactId>
  4. </dependency>
  5. <dependency>
  6.     <groupId>mysql</groupId> <!-- 这里以MySQL为例,其他数据库请根据需求配置 -->
  7.     <artifactId>mysql-connector-java</artifactId>
  8.     <scope>runtime</scope>
  9. </dependency>
复制代码
这些依赖将会自动下载并配置所有必要的库,确保项目中可以直接利用 JdbcTemplate 和 NamedParameterJdbcTemplate。
数据库配置

Spring Boot 提供了多种方式来配置数据库连接信息。推荐利用 application.yml 文件进行配置,由于它结构清晰,便于阅读和维护。
application.yml 配置示例:
  1. spring:
  2.   datasource:
  3.     url: jdbc:mysql://localhost:3306/db_boot?useSSL=false&serverTimezone=UTC
  4.     username: root
  5.     password: root
  6.     driver-class-name: com.mysql.cj.jdbc.Driver
复制代码
实体类与数据库表映射

创建实体类

假设我们要管理一个简单的用户信息表 users,每个用户具有 id、name、email 和 age 属性。我们可以创建一个相应的实体类 User:
  1. @Data
  2. @AllArgsConstructor
  3. @NoArgsConstructor
  4. @RequiredArgsConstructor
  5. public class User {
  6.     private Long id;
  7.     private String name;
  8.     private String email;
  9.     private Integer age;
  10. }
复制代码
数据库表的设计与创建

在 MySQL 中,我们可以通过以下 SQL 语句来创建与 User 实体类对应的 users 表:
  1. CREATE TABLE users (
  2.     id BIGINT AUTO_INCREMENT PRIMARY KEY,
  3.     name VARCHAR(100) NOT NULL,
  4.     email VARCHAR(100) NOT NULL,
  5.     age INT
  6. );
复制代码
JdbcTemplate 基础用法

JdbcTemplate 注入方式

在 Spring Boot 中,JdbcTemplate 通常通过依赖注入的方式来利用。我们可以通过构造器注入或字段注入的方式将 JdbcTemplate 注入到服务类中。
通过构造器注入

构造器注入是推荐的依赖注入方式,由于它可以确保依赖项在对象创建时即被初始化。下面是一个示例,展示了怎样通过构造器注入 JdbcTemplate:
  1. @Service
  2. public class UserService {
  3.     private final JdbcTemplate jdbcTemplate;
  4.     public UserService(JdbcTemplate jdbcTemplate) {
  5.         this.jdbcTemplate = jdbcTemplate;
  6.     }
  7. }
复制代码
通过字段注入

固然构造器注入是推荐的方式,但在某些情况下,字段注入也被广泛利用,尤其是在较小的项目中。字段注入利用 @Autowired 注解来自动注入依赖项:
  1. @Service
  2. public class UserService {
  3.     @Autowired
  4.     private JdbcTemplate jdbcTemplate;
  5. }
复制代码
这种方式固然利用方便,但缺点是无法通过构造函数确保依赖项的注入。
数据库查询

JdbcTemplate 提供了丰富的方法用于执行各种类型的数据库查询操纵,包括查询单条记录、多条记录以及返回 Map 或 List 效果。
查询单条记录(queryForObject)

在 JdbcTemplate 中,queryForObject 方法用于查询单条记录并将其映射为对象。随着 Spring Framework 的发展,一些旧方法被标志为过时,并引入了新的方法以提高灵活性和易用性。
方法签名分析:
  1. @Nullable
  2. public <T> T queryForObject(String sql, Object[] args, int[] argTypes, RowMapper<T> rowMapper) throws DataAccessException {
  3.     List<T> results = (List)this.query(sql, args, argTypes, (ResultSetExtractor)(new RowMapperResultSetExtractor(rowMapper, 1)));
  4.     return DataAccessUtils.nullableSingleResult(results);
  5. }
复制代码
参数寄义:

处理逻辑如下:

⚠ 过时的 queryForObject 方法:
  1. @Deprecated
  2. @Nullable
  3. public <T> T queryForObject(String sql, @Nullable Object[] args, RowMapper<T> rowMapper) throws DataAccessException {
  4.     List<T> results = (List)this.query((String)sql, (Object[])args, (ResultSetExtractor)(new RowMapperResultSetExtractor(rowMapper, 1)));
  5.     return DataAccessUtils.nullableSingleResult(results);
  6. }
复制代码
这个方法与前一个方法的告急区别在于它没有 argTypes 参数,实用于不需要明确指定参数类型的情况。由于 JDBC 参数类型不明确大概导致一些潜在问题,因此该方法已被标志为过时。
推荐的 queryForObject 方法:
  1. @Nullable
  2. public <T> T queryForObject(String sql, RowMapper<T> rowMapper, @Nullable Object... args) throws DataAccessException {
  3.     List<T> results = (List)this.query((String)sql, (Object[])args, (ResultSetExtractor)(new RowMapperResultSetExtractor(rowMapper, 1)));
  4.     return DataAccessUtils.nullableSingleResult(results);
  5. }
复制代码
这个方法是推荐的利用方式,通过变长参数 Object... args 提供参数,并且结合了 RowMapper 以灵活地处理查询效果。
处理逻辑如下:

因此,当我们希望查询数据库中的单条记录时,可以利用上面这个 queryForObject 方法。这个方法要求 SQL 查询返回一行数据,并将效果映射为指定类型的对象。
  1. /**
  2. * 根据用户ID查找用户
  3. *
  4. * @param id 用户ID,作为查询条件
  5. * @return 匹配的用户对象,若不存在则返回null
  6. */
  7. public User findUserById(Long id) {
  8.     // 定义查询语句,使用 ? 作为占位符,防止 SQL 注入
  9.     String sql = "SELECT * FROM users WHERE id = ?";
  10.     // 使用 jdbcTemplate 的 queryForObject 方法执行查询,并将结果映射为 User 对象
  11.     // 第一个参数为 SQL 语句,第二个参数为参数对象数组,第三个参数为 RowMapper 接口的实现
  12.     // RowMapper 用于将 ResultSet 的每一行映射为一个 User 对象
  13.     return jdbcTemplate.queryForObject(sql, (rs, rowNum) ->
  14.         new User(
  15.             rs.getLong("id"),
  16.             rs.getString("name"),
  17.             rs.getString("email"),
  18.             rs.getInt("age")
  19.         ), id
  20.     );
  21. }
复制代码
查询多条记录(query)

如果查询的效果大概包罗多条记录,可以利用 query 方法。这个方法返回一个包罗所有效果的 List,每个效果可以映射为一个对象。
JdbcTemplate 的 query 方法用于执行 SQL 查询,并将效果集通过不同的方式进行处理。根据不同的需求,query 方法有多个重载版本,可以返回单个对象、列表或通过回调函数逐行处理效果集。
先来看看 query 方法(带 ResultSetExtractor)的源码:
  1. @Nullable
  2. public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
  3.     // 参数检查,确保 SQL 语句和 `ResultSetExtractor` 非空
  4.     Assert.notNull(sql, "SQL must not be null");
  5.     Assert.notNull(rse, "ResultSetExtractor must not be null");
  6.     if (this.logger.isDebugEnabled()) {
  7.         this.logger.debug("Executing SQL query [" + sql + "]");
  8.     }
  9.     class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
  10.         QueryStatementCallback() {
  11.         }
  12.         @Nullable
  13.         public T doInStatement(Statement stmt) throws SQLException {
  14.             ResultSet rs = null;
  15.             Object var3;
  16.             try {
  17.                 // 执行 SQL 查询,获取 `ResultSet`,
  18.                 rs = stmt.executeQuery(sql);
  19.                 // 通过 `ResultSetExtractor` 的 `extractData` 方法处理 `ResultSet`
  20.                 var3 = rse.extractData(rs);
  21.             } finally {
  22.                 // 闭 `ResultSet` 以释放资源
  23.                 JdbcUtils.closeResultSet(rs);
  24.             }
  25.             return var3;
  26.         }
  27.         public String getSql() {
  28.             return sql;
  29.         }
  30.     }
  31.     // 执行查询, 传入 `QueryStatementCallback` 作为回调处理逻辑,最后返回结果
  32.     return this.execute(new QueryStatementCallback(), true);
  33. }
复制代码
参数寄义:

处理逻辑:

这个版本的 query 方法非常灵活,实用于自界说 ResultSet 处理逻辑的场景。ResultSetExtractor 接口答应用户界说如那边理整个效果集。

再来看看基于上述方法的别的一个版本 query 方法(带 RowCallbackHandler):
  1. public void query(String sql, RowCallbackHandler rch) throws DataAccessException {
  2.     this.query((String)sql, (ResultSetExtractor)(new RowCallbackHandlerResultSetExtractor(rch)));
  3. }
复制代码
参数寄义:

处理逻辑:

此方法实用于需要逐行处理效果集的场景,而不是将整个效果集映射为一个对象或列表。例如,在需要在处理数据的同时执行某些操纵(如记录日志或更新状态)时,可以利用这种方式。

最后看看 query 方法(带 RowMapper):
  1. public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
  2.     return (List)result((List)this.query((String)sql, (ResultSetExtractor)(new RowMapperResultSetExtractor(rowMapper))));
  3. }
复制代码
参数寄义:

处理逻辑:

这个版本的 query 方法实用于需要将查询效果集映射为 Java 对象的场景。通过 RowMapper 接口,开发者可以自界说怎样将数据库表的每一行映射为 Java 对象,得当于 ORM 的需求。
   Note:JdbcTemplate 的 query 方法提供了多种灵活的接口来处理 SQL 查询的效果集:
    对于查询所有用户的需求,属于 ORM 的场景,不需要额外的特殊处理,可以选用带 RowMapper 的 query 方法:
  1. public List<User> findAllUsers() {
  2.     String sql = "SELECT * FROM users";
  3.     return jdbcTemplate.query(sql, (rs, rowNum) ->
  4.         new User(
  5.                 rs.getLong("id"),
  6.                 rs.getString("name"),
  7.                 rs.getString("email"),
  8.                 rs.getInt("age")
  9.         )
  10.     );
  11. }
复制代码
查询返回 Map

有时候,我们需要直接将查询效果作为 Map 或 List 返回,JdbcTemplate 提供了多种方法来实现这一需求。
queryForMap 方法用于执行 SQL 查询,并将单行效果映射为 Map<String, Object>,其中键为列名,值为相应的字段值。
  1. public Map<String, Object> findUserAsMap(Long id)  {
  2.     String sql = "SELECT * FROM users WHERE id = ?";
  3.     return jdbcTemplate.queryForMap(sql, id);
  4. }
复制代码
方法签名分析:
  1. public Map<String, Object> queryForMap(String sql, Object[] args, int[] argTypes) throws DataAccessException {
  2.     return (Map)result((Map)this.queryForObject(sql, args, argTypes, this.getColumnMapRowMapper()));
  3. }
复制代码
参数寄义:

处理逻辑:

再看看无 argTypes 参数的 queryForMap 方法:
  1. public Map<String, Object> queryForMap(String sql, @Nullable Object... args) throws DataAccessException {
  2.     return (Map)result((Map)this.queryForObject(sql, args, this.getColumnMapRowMapper()));
  3. }
复制代码
这个方法与前者类似,但不需要显式指定参数类型,更加简便,得当不需要明确类型控制的场景。
处理逻辑:

查询返回 List

queryForList 方法用于执行查询并返回一个 List,每个元素是查询效果的一行数据,可以是简单类型的列表或 Map 的列表。
方法签名分析:
  1. public <T> List<T> queryForList(String sql, Class<T> elementType) throws DataAccessException {
  2.     return this.query(sql, this.getSingleColumnRowMapper(elementType));
  3. }
复制代码
参数寄义:

该方法利用 getSingleColumnRowMapper(elementType) 来创建一个单列映射器,将效果会合单列数据映射为指定类型的对象,并返回包罗这些对象的列表。
再来看看查询多行数据的 queryForList 方法:
  1. public List<Map<String, Object>> queryForList(String sql) throws DataAccessException {
  2.     return this.query(sql, this.getColumnMapRowMapper());
  3. }
复制代码
该方法利用 getColumnMapRowMapper() 将每行效果映射为 Map<String, Object>,然后返回 List<Map<String, Object>>,每个 Map 代表一行数据。
利用示例:
  1. public List<Map<String, Object>> findAllUserAsMap() {
  2.     String sql = "SELECT * FROM users";
  3.     return jdbcTemplate.queryForList(sql);
  4. }
复制代码
数据库更新与删除

在 JdbcTemplate 中,update 方法用于执行 SQL 的更新、删除和插入操纵。它是对数据库进行写操纵的核心方法之一。该方法有多个重载版本,可以处理不同的场景,包括带参数的 SQL 语句、返回天生的键等。
update 方法(核心实现):
  1. protected int update(final PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss) throws DataAccessException {
  2.     // 日志记录
  3.     this.logger.debug("Executing prepared SQL update");
  4.     // 调用 `execute` 方法,传入 `PreparedStatementCreator` 和 `PreparedStatement` 的回调处理逻辑
  5.     // 通过 updateCount 方法返回受影响的行数。
  6.     return updateCount((Integer)this.execute(psc, (ps) -> {
  7.         boolean var9 = false;
  8.         Integer var4;
  9.         try {
  10.             var9 = true;
  11.             
  12.             // 如果 PreparedStatementSetter (pss) 不为空,则调用 pss.setValues(ps) 方法设置 SQL 语句的参数。
  13.             if (pss != null) {
  14.                 pss.setValues(ps);
  15.             }
  16.             // 执行 ps.executeUpdate() 方法,执行 SQL 更新操作,并返回受影响的行数。
  17.             int rows = ps.executeUpdate();
  18.             if (this.logger.isTraceEnabled()) {
  19.                 this.logger.trace("SQL update affected " + rows + " rows");
  20.             }
  21.             var4 = rows;
  22.             var9 = false;
  23.         } finally {
  24.             if (var9) {
  25.                 // 如果 pss 实现了 ParameterDisposer 接口,则调用 cleanupParameters 方法进行参数清理,确保资源正确释放。
  26.                 if (pss instanceof ParameterDisposer parameterDisposer) {
  27.                     parameterDisposer.cleanupParameters();
  28.                 }
  29.             }
  30.         }
  31.         if (pss instanceof ParameterDisposer parameterDisposerx) {
  32.             parameterDisposerx.cleanupParameters();
  33.         }
  34.         return var4;
  35.     }, true));
  36. }
复制代码
参数寄义:

处理逻辑:

update 方法(简化版本):
  1. public int update(PreparedStatementCreator psc) throws DataAccessException {
  2.     return this.update(psc, (PreparedStatementSetter)null);
  3. }
复制代码
这是 update 方法的简化版本,不利用 PreparedStatementSetter。直接调用核心 update 方法,pss 参数为 null。实用于不需要设置参数的简单 SQL 语句。

update 方法(处理天生的键):
  1. public int update(final PreparedStatementCreator psc, final KeyHolder generatedKeyHolder) throws DataAccessException {
  2.     // 检查 KeyHolder:确保 KeyHolder 非空。
  3.     Assert.notNull(generatedKeyHolder, "KeyHolder must not be null");
  4.     // 日志记录:记录 SQL 更新操作及生成的键。
  5.     this.logger.debug("Executing SQL update and returning generated keys");
  6.    
  7.     // 执行 SQL
  8.     return updateCount((Integer)this.execute(psc, (ps) -> {
  9.         // 执行 ps.executeUpdate() 方法,执行 SQL 更新操作,获取受影响的行数。
  10.         int rows = ps.executeUpdate();
  11.         
  12.         // 清空 KeyHolder 中的键列表,然后通过 storeGeneratedKeys 方法将生成的键存储到 KeyHolder 中。
  13.         generatedKeyHolder.getKeyList().clear();
  14.         this.storeGeneratedKeys(generatedKeyHolder, ps, 1);
  15.         if (this.logger.isTraceEnabled()) {
  16.             this.logger.trace("SQL update affected " + rows + " rows and returned " + generatedKeyHolder.getKeyList().size() + " keys");
  17.         }
  18.         // 返回结果:返回受影响的行数。
  19.         return rows;
  20.     }, true));
  21. }
复制代码
参数寄义:

处理逻辑:
   TIP:此方法实用于需要在执行插入操纵后获取天生键(如自增 ID)的场景。
  
update 方法(带 PreparedStatementSetter):
  1. public int update(String sql, @Nullable PreparedStatementSetter pss) throws DataAccessException {
  2.     return this.update((PreparedStatementCreator)(new SimplePreparedStatementCreator(sql)), (PreparedStatementSetter)pss);
  3. }
复制代码
处理逻辑:该方法担当一个 SQL 语句和可选的 PreparedStatementSetter,通过 SimplePreparedStatementCreator 创建 PreparedStatement,然后调用核心 update 方法执行 SQL 操纵。
   TIP:实用于简单的 SQL 更新、删除或插入操纵。
  
update 方法(带参数和参数类型):
  1. public int update(String sql, Object[] args, int[] argTypes) throws DataAccessException {
  2.     return this.update(sql, this.newArgTypePreparedStatementSetter(args, argTypes));
  3. }
复制代码
参数寄义:

处理逻辑:通过 newArgTypePreparedStatementSetter 方法创建 PreparedStatementSetter,设置参数及其类型,然后调用 update 方法执行 SQL 操纵。
   TIP:实用于需要明确指定参数类型的 SQL 操纵。
  
update 方法(带参数):
  1. public int update(String sql, @Nullable Object... args) throws DataAccessException {
  2.     return this.update(sql, this.newArgPreparedStatementSetter(args));
  3. }
复制代码
参数寄义:

处理逻辑:通过 newArgPreparedStatementSetter 方法创建 PreparedStatementSetter,仅设置参数(不指定类型),然后调用 update 方法执行 SQL 操纵。
   TIP:实用于无需明确指定参数类型的简单 SQL 操纵。
    Note:
  JdbcTemplate 的 update 方法提供了多种情势,以处理各种更新、删除和插入操纵:
    执行插入操纵(update)

下面的示例展示了怎样利用 update 方法插入一条新记录:
  1. public void insertUser(User user) {
  2.     String sql = "INSERT INTO users (name, email, age) VALUES (?, ?, ?)";
  3.     jdbcTemplate.update(sql, user.getName(), user.getEmail(), user.getAge());
  4. }
复制代码
该方法返回插入的行数,通常为 1。
执行更新操纵(update)

类似地,我们可以利用 update 方法更新现有的记录:
  1. public void updateUser(User user) {
  2.     String sql = "UPDATE users SET name = ?, email = ?, age = ? WHERE id = ?";
  3.     jdbcTemplate.update(sql, user.getName(), user.getEmail(), user.getAge(), user.getId());
  4. }
复制代码
该方法返回被更新的行数。
执行删除操纵(update)

删除操纵同样可以通过 update 方法来实现:
  1. public int deleteUser(Long id) {
  2.     String sql = "DELETE FROM users WHERE id = ?";
  3.     return jdbcTemplate.update(sql, id);
  4. }
复制代码
该方法返回被删除的行数。
批量操纵

在 JdbcTemplate 中,batchUpdate 方法用于执行批量更新、删除和插入操纵。通过批量操纵可以明显提高数据库操纵的效率,特殊是在处理大量数据时。
batchUpdate 方法(带 PreparedStatementCreator 和 BatchPreparedStatementSetter):
  1. public int[] batchUpdate(final PreparedStatementCreator psc, final BatchPreparedStatementSetter pss, final KeyHolder generatedKeyHolder) throws DataAccessException {
  2.     // 调用 execute 方法,传入 PreparedStatementCreator 和 PreparedStatementCallback 来执行批量操作。
  3.     // 由 BatchPreparedStatementSetter 设置批次中的每个 PreparedStatement 参数。
  4.     int[] result = (int[])this.execute(psc, this.getPreparedStatementCallback(pss, generatedKeyHolder));
  5.     Assert.state(result != null, "No result array");
  6.     return result;
  7. }
复制代码
参数寄义:

处理逻辑:
   TIP:该方法实用于需要获取天生键的批量插入操纵。
  
batchUpdate 方法(带 SQL 和 BatchPreparedStatementSetter):
  1. public int[] batchUpdate(String sql, final BatchPreparedStatementSetter pss) throws DataAccessException {
  2.     // 在调试模式下,记录 SQL 批量操作的执行情况。
  3.     if (this.logger.isDebugEnabled()) {
  4.         this.logger.debug("Executing SQL batch update [" + sql + "]");
  5.     }
  6.     // 通过 `BatchPreparedStatementSetter` 获取批次大小,如果批次大小为 0,则直接返回空的结果数组
  7.     int batchSize = pss.getBatchSize();
  8.     if (batchSize == 0) {
  9.         return new int[0];
  10.     } else {
  11.         // 调用 execute 方法,通过内部的 PreparedStatementCallback 进行批量操作。
  12.         int[] result = (int[])this.execute(sql, this.getPreparedStatementCallback(pss, (KeyHolder)null));
  13.         Assert.state(result != null, "No result array");
  14.         
  15.         // 返回受影响的行数数组,每个元素表示每批次执行的结果。
  16.         return result;
  17.     }
  18. }
复制代码
参数寄义:

处理逻辑:
   TIP:该方法实用于简单的批量更新、删除操纵,不涉及天生键的处理。
  
batchUpdate 方法(带 SQL 和参数列表):
  1. public int[] batchUpdate(String sql, List<Object[]> batchArgs) throws DataAccessException {
  2.     return this.batchUpdate(sql, batchArgs, new int[0]);
  3. }
复制代码
参数寄义:

处理逻辑:这是一个简化的 batchUpdate 方法,它调用了带有参数类型的 batchUpdate 方法,并传入空的 int[] 数组作为参数类型。
   TIP:实用于简单的批量操纵,不涉及参数类型控制。
  
batchUpdate 方法(带 SQL、参数列表和参数类型):
  1. public int[] batchUpdate(String sql, final List<Object[]> batchArgs, final int[] argTypes) throws DataAccessException {
  2.     // 如果参数列表为空,则返回空的结果数组。
  3.     // 执行批量操作,并返回受影响的行数数组。
  4.     return batchArgs.isEmpty() ? new int[0] : this.batchUpdate(sql, new BatchPreparedStatementSetter() {
  5.         // 在 `BatchPreparedStatementSetter` 的 `setValues` 方法中,根据参数和对应的类型设置 `PreparedStatement` 的参数值
  6.         public void setValues(PreparedStatement ps, int i) throws SQLException {
  7.             Object[] values = (Object[])batchArgs.get(i);
  8.             int colIndex = 0;
  9.             Object[] var5 = values;
  10.             int var6 = values.length;
  11.             for(int var7 = 0; var7 < var6; ++var7) {
  12.                 Object value = var5[var7];
  13.                 ++colIndex;
  14.                
  15.                 // 使用 `StatementCreatorUtils.setParameterValue` 方法处理不同类型的参数值
  16.                 if (value instanceof SqlParameterValue paramValue) {
  17.                     StatementCreatorUtils.setParameterValue(ps, colIndex, paramValue, paramValue.getValue());
  18.                 } else {
  19.                     int colType;
  20.                     if (argTypes.length < colIndex) {
  21.                         colType = Integer.MIN_VALUE;
  22.                     } else {
  23.                         colType = argTypes[colIndex - 1];
  24.                     }
  25.                     StatementCreatorUtils.setParameterValue(ps, colIndex, colType, value);
  26.                 }
  27.             }
  28.         }
  29.         public int getBatchSize() {
  30.             return batchArgs.size();
  31.         }
  32.     });
  33. }
复制代码
参数寄义:

处理逻辑:
   TIP:该方法实用于需要明确指定参数类型的批量操纵场景。
  
batchUpdate 方法(带自界说参数设置和批次巨细):
  1. public <T> int[][] batchUpdate(String sql, final Collection<T> batchArgs, final int batchSize, final ParameterizedPreparedStatementSetter<T> pss) throws DataAccessException {
  2.     // 在调试模式下,记录 SQL 批量操作的执行情况和批次大小。
  3.     if (this.logger.isDebugEnabled()) {
  4.         this.logger.debug("Executing SQL batch update [" + sql + "] with a batch size of " + batchSize);
  5.     }
  6.     int[][] result = (int[][])this.execute(sql, (ps) -> {
  7.         ArrayList<int[]> rowsAffected = new ArrayList<>();
  8.         boolean var15 = false;
  9.         // 结果是一个二维 `int[][]` 数组,每个子数组表示一个批次的执行结果。
  10.         int[][] var19;
  11.         try {
  12.             var15 = true;
  13.             
  14.             // 通过 `JdbcUtils.supportsBatchUpdates` 检查数据库是否支持批量更新。
  15.             boolean batchSupported = JdbcUtils.supportsBatchUpdates(ps.getConnection());
  16.             int n = 0;
  17.             Iterator<T> var8 = batchArgs.iterator();
  18.             while(true) {
  19.                 if (!var8.hasNext()) {
  20.                     int[][] result1 = new int[rowsAffected.size()][];
  21.                     for(int i = 0; i < result1.length; ++i) {
  22.                         result1[i] = rowsAffected.get(i);
  23.                     }
  24.                     var19 = result1;
  25.                     var15 = false;
  26.                     break;
  27.                 }
  28.                 T obj = var8.next();
  29.                
  30.                 // 逐个设置 `PreparedStatement` 的参数。
  31.                 pss.setValues(ps, obj);
  32.                 ++n;
  33.                
  34.                 // 如果数据库支持批量操作,则累积操作并按批次大小执行 `ps.executeBatch()`。
  35.                 if (batchSupported) {
  36.                     ps.addBatch();
  37.                     if (n % batchSize == 0 || n == batchArgs.size()) {
  38.                         if (this.logger.isTraceEnabled()) {
  39.                             int batchIdx = n % batchSize == 0 ? n / batchSize : n / batchSize + 1;
  40.                             int items = n - (n % batchSize == 0 ? n / batchSize - 1 : n / batchSize) * batchSize;
  41.                             this.logger.trace("Sending SQL batch update #" + batchIdx + " with " + items + " items");
  42.                         }
  43.                         rowsAffected.add(ps.executeBatch());
  44.                     }
  45.                 } else { // 如果不支持批量操作,则逐条执行 `ps.executeUpdate()`。
  46.                     int updateCount = ps.executeUpdate();
  47.                     rowsAffected.add(new int[]{updateCount});
  48.                 }
  49.             }
  50.         } finally {
  51.             if (var15) {
  52.                 if (pss instanceof ParameterDisposer parameterDisposer) {
  53.                     parameterDisposer.cleanupParameters();
  54.                 }
  55.             }
  56.         }
  57.         if (pss instanceof ParameterDisposer parameterDisposerx) {
  58.             parameterDisposerx.cleanupParameters();
  59.         }
  60.         return var19;
  61.     });
  62.     Assert.state(result != null, "No result array");
  63.     return result;
  64. }
复制代码
参数寄义:

处理逻辑:
   TIP:该方法实用于自界说批次巨细和参数设置的复杂批量操纵场景。
  批量插入(batchUpdate)

以下示例展示了怎样批量插入多个用户:
  1. public int[] batchAddUsers(List<User> users) {
  2.     String sql = "INSERT INTO users (name, email, age) VALUES (?, ?, ?)";
  3.     return jdbcTemplate.batchUpdate(sql, users, users.size(), (ps, user) -> {
  4.         ps.setString(1, user.getName());
  5.         ps.setString(2, user.getEmail());
  6.         ps.setInt(3, user.getAge());
  7.     });
  8. }
复制代码
该方法返回一个 int[] 数组,每个元素代表对应批次的执行效果。
批量更新与删除(batchUpdate)

批量更新和删除操纵与批量插入类似,也利用 batchUpdate 方法来实现:
  1. public int[] batchUpdateUsers(List<User> users) {
  2.     String sql = "UPDATE users SET name = ?, email = ?, age = ? WHERE id = ?";
  3.     return jdbcTemplate.batchUpdate(sql, users, users.size(), (ps, user) -> {
  4.         ps.setString(1, user.getName());
  5.         ps.setString(2, user.getEmail());
  6.         ps.setInt(3, user.getAge());
  7.         ps.setLong(4, user.getId());
  8.     });
  9. }
  10. public int[] batchDeleteUsers(List<Long> ids) {
  11.     String sql = "DELETE FROM users WHERE id = ?";
  12.     return jdbcTemplate.batchUpdate(sql, ids, ids.size(), (ps, id) -> ps.setLong(1, id));
  13. }
复制代码
这两个方法分别执行批量更新和删除操纵,返回 int[] 数组,表示每个批次的执行效果。
JdbcTemplate 进阶用法

利用 RowMapper 自界说效果映射

RowMapper 是 JdbcTemplate 中用于将 ResultSet 的每一行映射为 Java 对象的接口。通过实现 RowMapper 接口,开发者可以根据需要自界说效果集的映射逻辑,从而将数据库查询效果转换为更得当应用逻辑的对象情势。
什么是 RowMapper

RowMapper 接口位于 org.springframework.jdbc.core 包中,它只有一个方法:
  1. @FunctionalInterface
  2. public interface RowMapper<T> {
  3.     @Nullable
  4.     T mapRow(ResultSet rs, int rowNum) throws SQLException;
  5. }
复制代码
参数说明:

返回值:

通过实现这个接口,你可以在 mapRow 方法中界说怎样将 ResultSet 的一行数据映射到你界说的对象中。
自界说 RowMapper

还是之前的用户表 users,包罗以下字段:id、name、email、age。我们需要将查询效果映射为一个 User 对象。
  1. @Data
  2. @AllArgsConstructor
  3. @NoArgsConstructor
  4. @RequiredArgsConstructor
  5. public class User {
  6.     private Long id;
  7.     private String name;
  8.     private String email;
  9.     private Integer age;
  10. }
复制代码
我们实现 RowMapper 接口,将 ResultSet 中的每一行数据映射为 User 对象:
  1. public class UserRowMapper implements RowMapper {
  2.     @Override
  3.     public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
  4.         User user = new User();
  5.         user.setId(rs.getLong("id"));
  6.         user.setName(rs.getString("name"));
  7.         user.setEmail(rs.getString("email"));
  8.         user.setAge(rs.getInt("age"));
  9.         return user;
  10.     }
  11. }
复制代码
在 JdbcTemplate 中利用自界说 RowMapper

一旦 RowMapper 实现完成,我们可以在 JdbcTemplate 中利用它来执行查询并映射效果。例如:
  1. public List<User> findAllUsers() {
  2.     String sql = "SELECT * FROM users";
  3.     return jdbcTemplate.query(sql, new UserRowMapper());
  4. }
复制代码
在上面的代码中,jdbcTemplate.query 方法将执行给定的 SQL 查询,并利用 UserRowMapper 将效果会合的每一行数据映射为 User 对象,最终返回 User 对象的 List。
利用 Lambda 表达式简化 RowMapper

如果映射逻辑非常简单,例如只是将数据库字段直接映射到对象的字段上,可以利用 Lambda 表达式简化 RowMapper 的实现:
  1. public List<User> findAllUsers() {
  2.     String sql = "SELECT * FROM users";
  3.     // return jdbcTemplate.query(sql, new UserRowMapper());
  4.     return jdbcTemplate.query(sql, (rs, rowNum) -> {
  5.         User user = new User();
  6.         user.setId(rs.getLong("id"));
  7.         user.setName(rs.getString("name"));
  8.         user.setEmail(rs.getString("email"));
  9.         user.setAge(rs.getInt("age"));
  10.         return user;
  11.     });
  12. }
复制代码
这种写法更加简便,但功能上与单独界说的 UserRowMapper 类似。它更得当于映射逻辑简单、不需要在多个地方复用的场景。
扩展 RowMapper 的功能

在某些复杂场景下,大概需要扩展 RowMapper 的功能。例如,当效果集包罗关联表的数据时,可以在 RowMapper 中处理这些关联关系,并将其映射为复杂对象。
假设 User 对象中还有一个 List<Order> 字段,表示用户的订单列表,Order 是一个单独的类,代表订单信息。在 UserRowMapper 中,你可以添加额外的逻辑来查询并填充用户的订单信息:
  1. public class UserRowMapper implements RowMapper<User> {
  2.     @Override
  3.     public User mapRow(ResultSet rs, int rowNum) throws SQLException {
  4.         User user = new User();
  5.         user.setId(rs.getLong("id"));
  6.         user.setName(rs.getString("name"));
  7.         user.setEmail(rs.getString("email"));
  8.         user.setAge(rs.getInt("age"));
  9.         
  10.         // 获取用户的订单列表(假设订单数据已经加入到 ResultSet 中)
  11.         List<Order> orders = new ArrayList<>();
  12.         do {
  13.             Order order = new Order();
  14.             order.setOrderId(rs.getLong("order_id"));
  15.             order.setProduct(rs.getString("product"));
  16.             order.setPrice(rs.getBigDecimal("price"));
  17.             orders.add(order);
  18.         } while (rs.next() && rs.getLong("id") == user.getId()); // 关联逻辑,类似于 ON 条件
  19.         
  20.         user.setOrders(orders);
  21.         return user;
  22.     }
  23. }
复制代码
这种处理方式可以将多个表的数据映射为复杂的对象结构,但要留意控制查询的效率和内存斲丧。
利用 ResultSetExtractor 处理复杂查询效果

ResultSetExtractor 是 JdbcTemplate 提供的另一种用于处理查询效果的接口,与 RowMapper 不同的是,ResultSetExtractor 答应你处理整个 ResultSet,而不仅仅是逐行映射。这使得它特殊得当处理复杂的查询效果,比如需要跨多行数据进行聚合或复杂转换的情况。
什么是 ResultSetExtractor

ResultSetExtractor 是一个函数式接口,其界说如下:
  1. public interface ResultSetExtractor<T> {
  2.     @Nullable
  3.     T extractData(ResultSet rs) throws SQLException, DataAccessException;
  4. }
复制代码

ResultSetExtractor 答应你一次性处理整个 ResultSet,这在需要处理复杂的、跨多行的数据时特殊有用。
自界说 ResultSetExtractor

假设我们需要查询用户及其订单信息,并将这些信息构造成一个包罗用户和其订单列表的复杂对象。我们可以利用 ResultSetExtractor 实现这一需求。
首先,假设我们有如下的 User 和 Order 类:
  1. public class User {
  2.     private Long id;
  3.     private String name;
  4.     private String email;
  5.     private Integer age;
  6.     private List<Order> orders;
  7.     // 构造方法、getter 和 setter 方法省略
  8. }
  9. public class Order {
  10.     private Long orderId;
  11.     private String product;
  12.     private BigDecimal price;
  13.     // 构造方法、getter 和 setter 方法省略
  14. }
复制代码
然后,我们可以利用 ResultSetExtractor 将查询效果映射为一个 User 对象及其关联的 Order 列表:
  1. public class UserWithOrdersExtractor implements ResultSetExtractor {
  2.     @Override
  3.     public User extractData(ResultSet rs) throws SQLException, DataAccessException {
  4.         User user = null;
  5.         List<Order> orders = new ArrayList<>();
  6.         while (rs.next()) {
  7.             if (user == null) {
  8.                 user = new User();
  9.                 user.setId(rs.getLong("id"));
  10.                 user.setName(rs.getString("name"));
  11.                 user.setEmail(rs.getString("email"));
  12.                 user.setAge(rs.getInt("age"));
  13.             }
  14.             Order order = new Order();
  15.             order.setOrderId(rs.getLong("order_id"));
  16.             order.setProduct(rs.getString("product"));
  17.             order.setPrice(rs.getBigDecimal("price"));
  18.             orders.add(order);
  19.         }
  20.         if (user != null) {
  21.             user.setOrders(orders);
  22.         }
  23.         return user;
  24.     }
  25. }
复制代码
在这个实现中,我们利用 while 循环遍历 ResultSet,每次读取一行数据。在第一次循环时,我们初始化 User 对象,并在后续的每一行中将订单信息添加到用户的订单列表中。循环竣事后,返回包罗所有订单信息的用户对象。
利用匿名类或 Lambda 表达式简化 ResultSetExtractor

如果你只需要一次性的利用 ResultSetExtractor,可以利用匿名类或 Lambda 表达式来简化代码。例如:
  1. public User findUserWithOrders(Long userId) {
  2.     String sql = "SELECT u.id, u.name, u.email, u.age, o.order_id, o.product, o.price " +
  3.             "FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE u.id = ?";
  4.     return  jdbcTemplate.query(sql, new Object[]{userId}, rs -> {
  5.         User user = null;
  6.         List<Order> orders = new ArrayList<>();
  7.         while (rs.next()) {
  8.             if (user == null) {
  9.                 user = new User();
  10.                 user.setId(rs.getLong("id"));
  11.                 user.setName(rs.getString("name"));
  12.                 user.setEmail(rs.getString("email"));
  13.                 user.setAge(rs.getInt("age"));
  14.             }
  15.             Order order = new Order();
  16.             order.setOrderId(rs.getLong("order_id"));
  17.             order.setProduct(rs.getString("product"));
  18.             order.setPrice(rs.getBigDecimal("price"));
  19.             orders.add(order);
  20.         }
  21.         if (user != null) {
  22.             user.setOrders(orders);
  23.         }
  24.         return user;
  25.     });
  26. }
复制代码
这种写法固然简便,但也仅实用于简单的场景。如果查询逻辑复杂,发起还是单独界说一个 ResultSetExtractor 类,以便代码结构更加清晰。
ResultSetExtractor 的扩展

ResultSetExtractor 还可以用来实现更复杂的数据聚合和处理逻辑。例如,你可以在 ResultSetExtractor 中处理多个子查询效果,将数据聚合成复杂的对象结构,或将多个查询效果组合成一个复合对象。这使得它在处理复杂业务逻辑时非常有用。
举个例子,如果你需要将查询效果中的订单按日期进行分组,并盘算每组订单的总金额,你可以在 extractData 方法中实现这些逻辑,并将效果返回为一个包罗这些分组信息的对象。
利用 PreparedStatementSetter 动态设置参数

PreparedStatementSetter 是 JdbcTemplate 中用于动态设置 PreparedStatement 参数的接口。它答应开发者在执行 SQL 语句之前,通过编程的方式动态地为 SQL 语句的参数赋值。利用 PreparedStatementSetter,可以更灵活地处理不同的 SQL 参数设置场景,例如复杂的查询条件、批量操纵等。
什么是 PreparedStatementSetter

PreparedStatementSetter 接口位于 org.springframework.jdbc.core 包中,它只有一个方法:
  1. public interface PreparedStatementSetter {
  2.     void setValues(PreparedStatement ps) throws SQLException;
  3. }
复制代码
参数说明:

PreparedStatementSetter 的核心功能就是在 SQL 执行之前,为 PreparedStatement 的参数占位符(即 ?)赋值。这使得 SQL 语句在执行时能够更换掉占位符并传递现实的参数值。
利用 PreparedStatementSetter 的基本示例

假设我们有一个 SQL 查询,需要根据用户输入的条件动态设置参数:
  1. public List<User> findUsersByCriteria(String name, Integer age) {
  2.     String sql = "SELECT id, name, email, age FROM users WHERE name = ? AND age = ?";
  3.     return jdbcTemplate.query(sql, new PreparedStatementSetter() {
  4.         @Override
  5.         public void setValues(PreparedStatement ps) throws SQLException {
  6.             ps.setString(1, name);
  7.             ps.setInt(2, age);
  8.         }
  9.     }, new UserRowMapper());
  10. }
复制代码
在这个例子中,我们界说了一个匿名的 PreparedStatementSetter 实现,通过 ps.setString 和 ps.setInt 方法为 SQL 语句中的 ? 占位符设置参数。然后,利用 UserRowMapper 来将查询效果映射为 User 对象列表。
利用 Lambda 表达式简化代码

由于 PreparedStatementSetter 是一个函数式接口,可以用 Lambda 表达式来简化代码。上面的代码可以重写为:
  1. public List<User> findUsersByCriteria(String name, Integer age) {
  2.     String sql = "SELECT id, name, email, age FROM users WHERE name = ? AND age = ?";
  3.     return jdbcTemplate.query(sql, ps -> {
  4.         ps.setString(1, name);
  5.         ps.setInt(2, age);
  6.     }, new UserRowMapper());
  7. }
复制代码
这种写法不仅简便,而且在处理简单的参数设置场景时,更容易阅读和维护。
扩展 PreparedStatementSetter:处理复杂参数设置

在现实应用中,你大概需要处理更复杂的参数设置逻辑,比如根据某些条件动态设置参数值,或者处理可选参数。下面是一个更复杂的示例:
  1. public List<User> findUsersByCriteria(String name, Integer age, String email) {
  2.     String sql = "SELECT id, name, email, age FROM users WHERE 1=1";
  3.     List<Object> params = new ArrayList<>();
  4.     StringBuilder query = new StringBuilder(sql);
  5.     if (name != null) {
  6.         query.append(" AND name = ?");
  7.         params.add(name);
  8.     }
  9.     if (age != null) {
  10.         query.append(" AND age = ?");
  11.         params.add(age);
  12.     }
  13.     if (email != null) {
  14.         query.append(" AND email = ?");
  15.         params.add(email);
  16.     }
  17.     return jdbcTemplate.query(query.toString(), ps -> {
  18.         for (int i = 0; i < params.size(); i++) {
  19.             ps.setObject(i + 1, params.get(i));
  20.         }
  21.     }, new UserRowMapper());
  22. }
复制代码
在这个例子中,我们根据传入的参数条件动态构建了 SQL 查询,并将参数添加到一个列表 params 中。然后在 PreparedStatementSetter 中,我们遍历参数列表,并将参数依次设置到 PreparedStatement 中。这样可以处理不同的查询条件而无需重复编写 SQL 语句。
批量操纵中的 PreparedStatementSetter

PreparedStatementSetter 也常用于批量操纵中,特殊是共同 BatchPreparedStatementSetter 接口利用时。它答应你为每一个批次的 SQL 语句动态设置参数。
  1. public void batchUpdateUserEmails(List<User> users) {
  2.     String sql = "UPDATE users SET email = ? WHERE id = ?";
  3.     jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
  4.         @Override
  5.         public void setValues(PreparedStatement ps, int i) throws SQLException {
  6.             User user = users.get(i);
  7.             ps.setString(1, user.getEmail());
  8.             ps.setLong(2, user.getId());
  9.         }
  10.         @Override
  11.         public int getBatchSize() {
  12.             return users.size();
  13.         }
  14.     });
  15. }
复制代码
在这个示例中,我们利用 BatchPreparedStatementSetter 来处理批量更新操纵,每个批次的参数都根据当前的用户对象动态设置。
与其他 JdbcTemplate 方法的结合

PreparedStatementSetter 不仅用于 query 方法,还可以用于 update 和 batchUpdate 等方法。这使得它在动态参数设置的场景中非常通用。例如,下面是一个通过 update 方法执行插入操纵的示例:
  1. public int insertUser(String name, String email, Integer age) {
  2.     String sql = "INSERT INTO users (name, email, age) VALUES (?, ?, ?)";
  3.    
  4.     return jdbcTemplate.update(sql, ps -> {
  5.         ps.setString(1, name);
  6.         ps.setString(2, email);
  7.         ps.setInt(3, age);
  8.     });
  9. }
复制代码
利用 CallableStatementCallback 执行存储过程

在企业级应用中,存储过程(Stored Procedures)被广泛用于封装复杂的数据库逻辑。CallableStatementCallback 是 JdbcTemplate 提供的一个接口,用于执行存储过程或数据库函数,并处理效果集或输出参数。通过 CallableStatementCallback,我们可以灵活地调用数据库中的存储过程,处理复杂的输入和输出参数。
什么是 CallableStatementCallback

CallableStatementCallback 接口位于 org.springframework.jdbc.core 包中,它答应你通过回调的方式执行存储过程。接口界说如下:
  1. public interface CallableStatementCallback<T> {
  2.     @Nullable
  3.     T doInCallableStatement(CallableStatement cs) throws SQLException, DataAccessException;
  4. }
复制代码

通过实现 CallableStatementCallback,你可以自界说存储过程的执行逻辑,并处理存储过程返回的效果或输出参数。
利用 CallableStatementCallback 调用简单存储过程

假设我们在数据库中有一个简单的存储过程,用于获取用户的具体信息:
  1. CREATE PROCEDURE GetUserDetails(IN userId BIGINT, OUT userName VARCHAR(100), OUT userEmail VARCHAR(100))
  2. BEGIN
  3.     SELECT name, email INTO userName, userEmail FROM users WHERE id = userId;
  4. END;
复制代码
我们可以通过 CallableStatementCallback 调用这个存储过程,并处理输出参数:
  1. public Map<String, String> getUserDetails(Long userId) {
  2.     String sql = "{call GetUserDetails(?, ?, ?)}";
  3.     return jdbcTemplate.execute(sql, new CallableStatementCallback<Map<String, String>>() {
  4.         @Override
  5.         public Map<String, String> doInCallableStatement(CallableStatement cs) throws SQLException, DataAccessException {
  6.             // 存储过程入参
  7.             cs.setLong(1, userId);
  8.             // 存储过程出参
  9.             cs.registerOutParameter(2, Types.VARCHAR);
  10.             cs.registerOutParameter(3, Types.VARCHAR);
  11.             // 执行存储过程
  12.             cs.execute();
  13.             // 返回结果
  14.             Map<String, String> result = new HashMap<>();
  15.             result.put("userName", cs.getString(2));
  16.             result.put("userEmail", cs.getString(3));
  17.             return result;
  18.         }
  19.     });
  20. }
复制代码
在这个例子中:

处理复杂的输入和输出参数

有时候,存储过程大概需要处理更复杂的输入和输出参数,比如多个输入参数,或返回一个效果集。我们可以在 CallableStatementCallback 中处理这些复杂情况。
例如,假设我们有一个存储过程,它接收多个输入参数,并返回一个效果集:
  1. CREATE PROCEDURE GetUserOrders(IN userId BIGINT)
  2. BEGIN
  3.     SELECT order_id, product, price FROM orders WHERE user_id = userId;
  4. END;
复制代码
我们可以这样处理:
  1. public List<Order> getUserOrders(Long userId) {
  2.     String sql = "{call GetUserOrders(?)}";
  3.     return jdbcTemplate.execute(sql, new CallableStatementCallback<List<Order>>() {
  4.         @Override
  5.         public List<Order> doInCallableStatement(CallableStatement cs) throws SQLException, DataAccessException {
  6.             // 存储过程入参
  7.             cs.setLong(1, userId);
  8.             // 执行存储过程,记录是否有返回结果集
  9.             boolean hasResultSet = cs.execute();
  10.             List<Order> orders = new ArrayList<>();
  11.             if (hasResultSet) {
  12.                 ResultSet rs = cs.getResultSet();
  13.                 while (rs.next()) {
  14.                     Order order = new Order();
  15.                     order.setOrderId(rs.getLong("order_id"));
  16.                     order.setProduct(rs.getString("product"));
  17.                     order.setPrice(rs.getBigDecimal("price"));
  18.                     orders.add(order);
  19.                 }
  20.             }
  21.             return orders;
  22.         }
  23.     });
  24. }
复制代码
在这个示例中:

处理 OUT 和 INOUT 参数

在存储过程中,OUT 参数和 INOUT 参数是比较常见的。CallableStatementCallback 提供了直接的方法来处理这些参数。
例如,一个存储过程大概会利用 INOUT 参数来接收和返回值:
  1. CREATE PROCEDURE UpdateUserAge(INOUT userAge INT)
  2. BEGIN
  3.     SET userAge = userAge + 1;
  4. END;
复制代码
调用这个存储过程并处理 INOUT 参数的示例如下:
  1. public int updateUserAge(int age) {
  2.     String sql = "{call UpdateUserAge(?)}";
  3.     return jdbcTemplate.execute(sql, new CallableStatementCallback<Integer>() {
  4.         @Override
  5.         public Integer doInCallableStatement(CallableStatement cs) throws SQLException {
  6.             cs.setInt(1, age);
  7.             cs.registerOutParameter(1, Types.INTEGER);
  8.             cs.execute();
  9.             return cs.getInt(1);
  10.         }
  11.     });
  12. }
复制代码
在这个示例中:

事务管理与 JdbcTemplate

在现实开发中,事务管理至关告急,特殊是在涉及多个数据库操纵时。Spring 提供了强盛的事务管理机制,答应开发者在利用 JdbcTemplate 进行数据库操纵时轻松管理事务。事务可以确保一组数据库操纵要么全部成功,要么全部回滚,从而保持数据的同等性。
在这一部门中,我们将探究 JdbcTemplate 中的事务管理,内容包括事务的基本概念、在 JdbcTemplate 中配置事务、编程式事务与声明式事务的区别,以及事务的隔离级别与传播行为。
事务的基本概念

事务(Transaction) 是一组逻辑上的操纵单元,这些操纵要么全部成功,要么全部失败回滚。
事务具有四个基本特性,通常简称为 ACID

Spring 的事务管理框架答应开发者在应用步伐中轻松管理这些特性,无论是通过编程式还是声明式的方式。
在 JdbcTemplate 中配置事务

利用 JdbcTemplate 时,事务管理通常通过 Spring 提供的 PlatformTransactionManager 来完成。最常见的事务管理器是 DataSourceTransactionManager,用于管理基于 JDBC 的事务。
在 Spring Boot 中,配置事务管理通常非常简单,由于它已经为你自动配置了 DataSourceTransactionManager。如果你需要手动配置,以下是一个简单的配置事务管理器示例:
  1. @Configuration
  2. @EnableTransactionManagement // 开启事务管理
  3. public class AppConfig {
  4.     @Bean
  5.     public DataSourceTransactionManager transactionManager(DataSource dataSource) {
  6.         return new DataSourceTransactionManager(dataSource);
  7.     }
  8.     @Bean
  9.     public JdbcTemplate jdbcTemplate(DataSource dataSource) {
  10.         return new JdbcTemplate(dataSource);
  11.     }
  12. }
复制代码
解释

编程式事务与声明式事务

Spring 提供了两种方式来管理事务:编程式事务和声明式事务。
编程式事务答应你通过代码显式地控制事务的开始、提交和回滚。利用编程式事务通常涉及 TransactionTemplate 或 PlatformTransactionManager,它们提供了编程方式来控制事务的边界。
利用示例:
  1. @Service
  2. public class UserService {
  3.     private final JdbcTemplate jdbcTemplate;
  4.     private final PlatformTransactionManager transactionManager;
  5.     @Autowired
  6.     public UserService(JdbcTemplate jdbcTemplate, PlatformTransactionManager transactionManager) {
  7.         this.jdbcTemplate = jdbcTemplate;
  8.         this.transactionManager = transactionManager;
  9.     }
  10.     public void createUser(String name, String email) {
  11.         // 创建事务
  12.         TransactionDefinition def = new DefaultTransactionDefinition();
  13.         // 获取事务状态
  14.         TransactionStatus status = transactionManager.getTransaction(def);
  15.         try {
  16.             jdbcTemplate.update("INSERT INTO users (name, email) VALUES (?, ?)", name, email);
  17.             // 其他数据库操作...
  18.             // 操作结束,提交事务
  19.             transactionManager.commit(status);
  20.         } catch (Exception e) {
  21.             // 回滚事务
  22.             transactionManager.rollback(status);
  23.             throw e;
  24.         }
  25.     }
  26. }
复制代码
在这个示例中,我们利用 @Transactional 注解声明了 createUser 方法为一个事务方法。Spring 会自动管理事务的开始、提交和回滚。
   TIP:编程式事务优点
    
声明式事务更为常见,它答应你利用注解(或 XML 配置)来声明事务的边界,而不需要在代码中显式控制事务。
利用示例:
  1. @Service
  2. public class UserService {
  3.     private final JdbcTemplate jdbcTemplate;
  4.     @Autowired
  5.     public UserService(JdbcTemplate jdbcTemplate) {
  6.         this.jdbcTemplate = jdbcTemplate;
  7.     }
  8.     @Transactional
  9.     public void createUser(String name, String email) {
  10.         jdbcTemplate.update("INSERT INTO users (name, email) VALUES (?, ?)", name, email);
  11.         // 其他数据库操作...
  12.     }
  13. }
复制代码
在这个示例中,我们利用 @Transactional 注解声明了 createUser 方法为一个事务方法。Spring 会自动管理事务的开始、提交和回滚。
   TIP:声明式事务优点
    事务隔离级别与传播行为

在 Spring 事务管理中,事务的隔离级别和传播行为是两个非常告急的概念,它们直接影响到事务的执行方式和事务间的相互影响。
隔离级别决定了一个事务与另一个事务的隔离程度。Spring 提供了以下5个隔离级别:

利用示例:(MySQL 默认可重复读)
  1. @Transactional(isolation = Isolation.REPEATABLE_READ)
  2. public void createUser(String name, String email) {
  3.     jdbcTemplate.update("INSERT INTO users (name, email) VALUES (?, ?)", name, email);
  4. }
复制代码
在这个示例中,@Transactional 注解指定了 REPEATABLE_READ 隔离级别,确保事务在读取数据时的稳固性。

传播行为界说了当一个事务方法被另一个事务方法调用时,事务应该怎样进行。
Spring 支持的传播行为有如下7个:

利用示例:
  1. @Transactional(propagation = Propagation.REQUIRES_NEW)
  2. public void createUser(String name, String email) {
  3.     jdbcTemplate.update("INSERT INTO users (name, email) VALUES (?, ?)", name, email);
  4. }
复制代码
在这个示例中,@Transactional 注解指定了 REQUIRES_NEW 传播行为,即每次调用 createUser 方法时,都会新建一个事务,而不依赖于外部事务。
NamedParameterJdbcTemplate 详解

NamedParameterJdbcTemplate 简介与注入方式

NamedParameterJdbcTemplate 是 JdbcTemplate 的一个扩展类,告急提供了对命名参数的支持。在利用 JdbcTemplate 时,SQL 语句中的参数通常通过位置标识符 ? 来表示,这种方式在处理复杂 SQL 时大概会导致代码难以维护。而 NamedParameterJdbcTemplate 通过利用命名参数,使得 SQL 语句更加直观和易于管理。
NamedParameterJdbcTemplate 的核心特点是支持命名参数,即你可以在 SQL 语句中利用具有名称的参数,而不是利用位置占位符 ?。这使得代码更具可读性,尤其在处理多个参数时尤为有用。
命名参数的优势:

与 JdbcTemplate 类似,NamedParameterJdbcTemplate 也依赖于 DataSource 来管理数据库连接。通常情况下,我们可以通过配置类将 NamedParameterJdbcTemplate 注入到服务类中。
如果你已经有一个配置好的 JdbcTemplate,可以直接通过构造器注入 JdbcTemplate 来创建 NamedParameterJdbcTemplate:
  1. @Configuration
  2. public class AppConfig {
  3.     @Bean
  4.     public NamedParameterJdbcTemplate namedParameterJdbcTemplate(JdbcTemplate jdbcTemplate) {
  5.         return new NamedParameterJdbcTemplate(jdbcTemplate);
  6.     }
  7.     @Bean
  8.     public JdbcTemplate jdbcTemplate(DataSource dataSource) {
  9.         return new JdbcTemplate(dataSource);
  10.     }
  11. }
复制代码
在这个配置类中,我们首先界说了一个 JdbcTemplate Bean,它依赖于 DataSource。然后,我们利用这个 JdbcTemplate 创建了一个 NamedParameterJdbcTemplate,并将其作为一个独立的 Bean 注入到 Spring 容器中。
也可以直接通过 DataSource 创建 NamedParameterJdbcTemplate,这种方式不依赖现有的 JdbcTemplate:
  1. @Configuration
  2. public class AppConfig {
  3.     @Bean
  4.     public NamedParameterJdbcTemplate namedParameterJdbcTemplate(DataSource dataSource) {
  5.         return new NamedParameterJdbcTemplate(dataSource);
  6.     }
  7. }
复制代码
在这个配置类中,我们直接通过 DataSource 创建了 NamedParameterJdbcTemplate,这种方式制止了对 JdbcTemplate 的依赖,简化了配置。
在现实项目中,你可以将 NamedParameterJdbcTemplate 注入到你的服务类中,并开始利用命名参数执行数据库操纵。
以下是一个简单的利用示例:
  1. @Service
  2. public class UserService {
  3.     private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;
  4.     @Autowired
  5.     public UserService(NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
  6.         this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
  7.     }
  8.     public User findUserById(Long id) {
  9.         String sql = "SELECT id, name, email, age FROM users WHERE id = :id";
  10.         Map<String, Object> params = new HashMap<>();
  11.         params.put("id", id);
  12.         return namedParameterJdbcTemplate.queryForObject(sql, params, new BeanPropertyRowMapper<>(User.class));
  13.     }
  14. }
复制代码
在这个示例中,我们利用 NamedParameterJdbcTemplate 进行查询操纵,SQL 语句中的 :id 是一个命名参数,通过 Map<String, Object> 提供现实的参数值。queryForObject 方法用于执行查询,并返回单个 User 对象。
利用命名参数进行查询

NamedParameterJdbcTemplate 通过支持命名参数,使得 SQL 查询操纵更加直观和易于管理。我们继续探究怎样利用 NamedParameterJdbcTemplate 执行查询操纵,包括查询单条记录、多条记录,以及结合 BeanPropertySqlParameterSource 和 MapSqlParameterSource 进行查询。
查询单条记录

利用 NamedParameterJdbcTemplate 查询单条记录时,可以通过 queryForObject 方法实现。这个方法类似于 JdbcTemplate 中的 queryForObject,但它利用命名参数,使得代码更加可读。
我们希望根据用户的 id 查询单个用户的信息。可以利用如下代码实现:
  1. public User findUserById(Long id) {
  2.     String sql = "SELECT id, name, email, age FROM users WHERE id = :id";
  3.    
  4.     Map<String, Object> params = new HashMap<>();
  5.     params.put("id", id);
  6.     return namedParameterJdbcTemplate.queryForObject(sql, params, new BeanPropertyRowMapper<>(User.class));
  7. }
复制代码
解释:

在现实利用中,查询大概不会返回任何效果,此时 queryForObject 方法会抛出 EmptyResultDataAccessException。可以通过捕获该非常并返回 null 来处理这种情况:
  1. public User findUserById(Long id) {
  2.     String sql = "SELECT id, name, email, age FROM users WHERE id = :id";
  3.    
  4.     Map<String, Object> params = new HashMap<>();
  5.     params.put("id", id);
  6.     try {
  7.         return namedParameterJdbcTemplate.queryForObject(sql, params, new BeanPropertyRowMapper<>(User.class));
  8.     } catch (EmptyResultDataAccessException e) {
  9.         return null;
  10.     }
  11. }
复制代码
当查询效果为空时,捕获 EmptyResultDataAccessException 并返回 null,以制止步伐崩溃。
利用 BeanPropertySqlParameterSource

除了利用 Map 作为参数容器外,还可以利用 BeanPropertySqlParameterSource,它答应你直接将对象的属性作为 SQL 参数绑定。这种方式在对象属性较多时非常有用。
我们可以利用 BeanPropertySqlParameterSource 来简化参数绑定:
  1. public User findUserById(Long id) {
  2.     String sql = "SELECT id, name, email, age FROM users WHERE id = :id";
  3.    
  4.     User user = new User();
  5.     user.setId(id);
  6.    
  7.     SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(user);
  8.     return namedParameterJdbcTemplate.queryForObject(sql, namedParameters, new BeanPropertyRowMapper<>(User.class));
  9. }
复制代码
BeanPropertySqlParameterSource 自动将 User 对象中的属性绑定到 SQL 语句中的命名参数上。这种方式减少了手动创建参数 Map 的步骤,代码更为简便。
查询多条记录

当查询的效果大概包罗多条记录时,NamedParameterJdbcTemplate 提供了 query 方法,可以返回一个包罗所有效果的 List。每个效果可以映射为一个对象。
假设我们希望查询所有年龄大于某个值的用户,可以利用如下代码:
  1. public List<User> findUsersByAge(int age) {
  2.     String sql = "SELECT id, name, email, age FROM users WHERE age > :age";
  3.    
  4.     Map<String, Object> params = new HashMap<>();
  5.     params.put("age", age);
  6.     return namedParameterJdbcTemplate.query(sql, params, new BeanPropertyRowMapper<>(User.class));
  7. }
复制代码
解释

利用 MapSqlParameterSource

MapSqlParameterSource 是另一个常用的 SqlParameterSource 实现,它答应你利用链式调用来添加参数,代码更加简便:
  1. public List<User> findUsersByAge(int age) {
  2.     String sql = "SELECT id, name, email, age FROM users WHERE age > :age";
  3.    
  4.     SqlParameterSource namedParameters = new MapSqlParameterSource("age", age);
  5.     return namedParameterJdbcTemplate.query(sql, namedParameters, new BeanPropertyRowMapper<>(User.class));
  6. }
复制代码
MapSqlParameterSource 提供了更加简便的参数设置方式,得当参数较少的情况。
动态 SQL 查询

在现实开发中,查询条件常常是动态的。你可以利用 NamedParameterJdbcTemplate 来构建动态 SQL 查询。
假设我们需要根据多个条件动态查询用户列表:
  1. public List<User> findUsersByCriteria(String name, Integer age) {
  2.     String sql = "SELECT id, name, email, age FROM users WHERE 1=1";
  3.    
  4.     MapSqlParameterSource params = new MapSqlParameterSource();
  5.    
  6.     if (name != null) {
  7.         sql += " AND name = :name";
  8.         params.addValue("name", name);
  9.     }
  10.     if (age != null) {
  11.         sql += " AND age = :age";
  12.         params.addValue("age", age);
  13.     }
  14.     return namedParameterJdbcTemplate.query(sql, params, new BeanPropertyRowMapper<>(User.class));
  15. }
复制代码
解释

数据库更新与删除

NamedParameterJdbcTemplate 不仅可以用来执行查询操纵,还可以用于执行更新和删除操纵。通过命名参数的支持,更新和删除操纵的代码变得更加简便和易于维护。
利用命名参数进行更新

NamedParameterJdbcTemplate 提供了 update 方法,用于执行 INSERT、UPDATE 和 DELETE 等 DML(数据操纵语言)语句。这个方法支持利用命名参数来绑定 SQL 语句中的参数,使得代码更加清晰。
假设我们需要更新用户的电子邮件地址,可以利用如下代码:
  1. public int updateUserEmail(Long userId, String newEmail) {
  2.     String sql = "UPDATE users SET email = :email WHERE id = :id";
  3.    
  4.     Map<String, Object> params = new HashMap<>();
  5.     params.put("email", newEmail);
  6.     params.put("id", userId);
  7.     return namedParameterJdbcTemplate.update(sql, params);
  8. }
复制代码
解释

如果你需要更新多个字段,BeanPropertySqlParameterSource 可以简化代码,将对象属性直接绑定到 SQL 参数中。
假设我们需要更新用户的名称和电子邮件地址,可以利用如下代码:
  1. public int updateUserDetails(User user) {
  2.     String sql = "UPDATE users SET name = :name, email = :email WHERE id = :id";
  3.    
  4.     SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(user);
  5.     return namedParameterJdbcTemplate.update(sql, namedParameters);
  6. }
复制代码
解释

在现实开发中,你大概需要根据条件动态更新字段。可以结合 MapSqlParameterSource 来实现动态更新。
假设我们需要动态更新用户的名称和电子邮件地址,可以利用如下代码:
  1. public int updateUserDetailsDynamically(Long userId, String name, String email) {
  2.     String sql = "UPDATE users SET ";
  3.     MapSqlParameterSource params = new MapSqlParameterSource("id", userId);
  4.    
  5.     if (name != null) {
  6.         sql += "name = :name, ";
  7.         params.addValue("name", name);
  8.     }
  9.     if (email != null) {
  10.         sql += "email = :email, ";
  11.         params.addValue("email", email);
  12.     }
  13.     // 移除最后的逗号和空格
  14.     sql = sql.substring(0, sql.length() - 2);
  15.     sql += " WHERE id = :id";
  16.    
  17.     return namedParameterJdbcTemplate.update(sql, params);
  18. }
复制代码
解释

利用命名参数进行删除

NamedParameterJdbcTemplate 的 update 方法同样实用于删除操纵。通过命名参数,可以更灵活地控制删除的条件。
假设我们需要根据用户的 ID 删除用户记录,可以利用如下代码:
  1. public int deleteUserById(Long userId) {
  2.     String sql = "DELETE FROM users WHERE id = :id";
  3.    
  4.     Map<String, Object> params = new HashMap<>();
  5.     params.put("id", userId);
  6.     return namedParameterJdbcTemplate.update(sql, params);
  7. }
复制代码
解释

有时删除操纵大概会涉及多个条件,可以利用 MapSqlParameterSource 来处理这些条件。假设我们需要根据用户的年龄和电子邮件删除用户记录,可以利用如下代码:
  1. public int deleteUsersByAgeAndEmail(int age, String email) {
  2.     String sql = "DELETE FROM users WHERE age = :age AND email = :email";
  3.    
  4.     MapSqlParameterSource params = new MapSqlParameterSource();
  5.     params.addValue("age", age);
  6.     params.addValue("email", email);
  7.     return namedParameterJdbcTemplate.update(sql, params);
  8. }
复制代码
解释

在处理大批量数据更新或删除时,NamedParameterJdbcTemplate 的 batchUpdate 方法可以明显提高性能。这个方法支持批量处理,并且可以结合命名参数利用。
假设我们需要批量更新用户的电子邮件地址,可以利用如下代码:
  1. public int[] batchUpdateEmails(List<User> users) {
  2.     String sql = "UPDATE users SET email = :email WHERE id = :id";
  3.    
  4.     SqlParameterSource[] batchParams = SqlParameterSourceUtils.createBatch(users.toArray());
  5.    
  6.     return namedParameterJdbcTemplate.batchUpdate(sql, batchParams);
  7. }
复制代码
解释

同样的,你也可以利用 batchUpdate 方法来批量删除用户:
  1. public int[] batchDeleteUsers(List<Long> userIds) {
  2.     String sql = "DELETE FROM users WHERE id = :id";
  3.    
  4.     SqlParameterSource[] batchParams = SqlParameterSourceUtils.createBatch(userIds.toArray());
  5.    
  6.     return namedParameterJdbcTemplate.batchUpdate(sql, batchParams);
  7. }
复制代码
将用户 ID 列表转换为 SqlParameterSource 数组,然后利用 batchUpdate 方法批量执行删除操纵。
批量操纵与命名参数

NamedParameterJdbcTemplate 提供了强盛的批量操纵支持,答应你利用命名参数进行大批量的数据插入、更新和删除操纵。通过批量操纵,你可以明显提高性能,尤其是在处理大量数据时。
批量插入

在批量插入操纵中,NamedParameterJdbcTemplate 的 batchUpdate 方法答应你一次性插入多条记录,而不需要为每条记录单独执行插入操纵。这样可以减少数据库的交互次数,从而提高插入效率。
假设我们有一个 User 类,并需要批量插入多个用户记录到 users 表中:
  1. public int[] batchInsertUsers(List<User> users) {
  2.     String sql = "INSERT INTO users (id, name, email, age) VALUES (:id, :name, :email, :age)";
  3.    
  4.     SqlParameterSource[] batchParams = SqlParameterSourceUtils.createBatch(users.toArray());
  5.    
  6.     return namedParameterJdbcTemplate.batchUpdate(sql, batchParams);
  7. }
复制代码
解释

如果你需要对每条记录的插入参数进行额外的处理,可以利用 MapSqlParameterSource 来构建批量插入参数:
  1. public int[] batchInsertUsersWithMap(List<User> users) {
  2.     String sql = "INSERT INTO users (id, name, email, age) VALUES (:id, :name, :email, :age)";
  3.    
  4.     SqlParameterSource[] batchParams = users.stream()
  5.         .map(user -> new MapSqlParameterSource()
  6.             .addValue("id", user.getId())
  7.             .addValue("name", user.getName())
  8.             .addValue("email", user.getEmail())
  9.             .addValue("age", user.getAge()))
  10.         .toArray(SqlParameterSource[]::new);
  11.    
  12.     return namedParameterJdbcTemplate.batchUpdate(sql, batchParams);
  13. }
复制代码
利用 MapSqlParameterSource 可以灵活地为每条记录设置参数。这种方法答应你在构建参数时进行额外的处理,如转换或校验。
批量更新与删除

与批量插入类似,NamedParameterJdbcTemplate 的 batchUpdate 方法也支持批量更新和删除操纵。
假设我们需要批量更新用户的电子邮件地址,可以利用如下代码:
  1. public int[] batchUpdateUserEmails(List<User> users) {
  2.     String sql = "UPDATE users SET email = :email WHERE id = :id";
  3.    
  4.     SqlParameterSource[] batchParams = SqlParameterSourceUtils.createBatch(users.toArray());
  5.    
  6.     return namedParameterJdbcTemplate.batchUpdate(sql, batchParams);
  7. }
复制代码
解释

如果更新操纵需要处理复杂的逻辑,可以利用 MapSqlParameterSource 来动态构建更新参数:
  1. public int[] batchUpdateUserDetails(List<User> users) {
  2.     String sql = "UPDATE users SET name = :name, email = :email, age = :age WHERE id = :id";
  3.    
  4.     SqlParameterSource[] batchParams = users.stream()
  5.         .map(user -> new MapSqlParameterSource()
  6.             .addValue("id", user.getId())
  7.             .addValue("name", user.getName())
  8.             .addValue("email", user.getEmail())
  9.             .addValue("age", user.getAge()))
  10.         .toArray(SqlParameterSource[]::new);
  11.    
  12.     return namedParameterJdbcTemplate.batchUpdate(sql, batchParams);
  13. }
复制代码
同样的,batchUpdate 方法也可以用于批量删除操纵。假设我们需要根据用户的 ID 批量删除用户记录,可以利用如下代码:
  1. public int[] batchDeleteUsers(List<Long> userIds) {
  2.     String sql = "DELETE FROM users WHERE id = :id";
  3.    
  4.     SqlParameterSource[] batchParams = userIds.stream()
  5.         .map(id -> new MapSqlParameterSource("id", id))
  6.         .toArray(SqlParameterSource[]::new);
  7.    
  8.     return namedParameterJdbcTemplate.batchUpdate(sql, batchParams);
  9. }
复制代码
  TIP:批量操纵的留意事项
    利用 NamedParameterJdbcTemplate 执行复杂 SQL

在现实应用中,SQL 查询往往不仅仅是简单的 CRUD 操纵,大概需要根据不同的业务逻辑构建动态 SQL,或者执行复杂的查询。NamedParameterJdbcTemplate 提供了灵活的工具来处理这些场景,使得代码更加清晰易懂,并且更易于维护。
动态 SQL 的构建

动态 SQL 是指在运行时根据不同的条件构建 SQL 查询。这在处理复杂的查询条件时尤其有用。NamedParameterJdbcTemplate 可以结合 MapSqlParameterSource 或 BeanPropertySqlParameterSource 来动态地构建 SQL 查询,并绑定参数。
假设我们有一个需求,需要根据用户的名称和年龄来查询用户列表,如果某个条件为空,则不利用该条件进行过滤。可以利用如下代码:
  1. public List<User> findUsersByDynamicCriteria(String name, Integer age) {
  2.     StringBuilder sql = new StringBuilder("SELECT id, name, email, age FROM users WHERE 1=1");
  3.     MapSqlParameterSource params = new MapSqlParameterSource();
  4.    
  5.     if (name != null) {
  6.         sql.append(" AND name = :name");
  7.         params.addValue("name", name);
  8.     }
  9.    
  10.     if (age != null) {
  11.         sql.append(" AND age = :age");
  12.         params.addValue("age", age);
  13.     }
  14.    
  15.     return namedParameterJdbcTemplate.query(sql.toString(), params, new BeanPropertyRowMapper<>(User.class));
  16. }
复制代码
解释

对于更加复杂的业务逻辑,大概需要构建包罗多个 JOIN、GROUP BY 或 ORDER BY 子句的动态 SQL 查询。以下是一个示例,展示了怎样根据多个条件构建复杂的 SQL 查询:
  1. public List<User> findUsersWithOrders(String name, Integer minAge, Integer maxAge, String orderBy) {
  2.     StringBuilder sql = new StringBuilder("SELECT u.id, u.name, u.email, u.age, COUNT(o.id) as order_count FROM users u ");
  3.     sql.append("LEFT JOIN orders o ON u.id = o.user_id WHERE 1=1 ");
  4.    
  5.     MapSqlParameterSource params = new MapSqlParameterSource();
  6.    
  7.     if (name != null) {
  8.         sql.append(" AND u.name = :name");
  9.         params.addValue("name", name);
  10.     }
  11.    
  12.     if (minAge != null) {
  13.         sql.append(" AND u.age >= :minAge");
  14.         params.addValue("minAge", minAge);
  15.     }
  16.    
  17.     if (maxAge != null) {
  18.         sql.append(" AND u.age <= :maxAge");
  19.         params.addValue("maxAge", maxAge);
  20.     }
  21.    
  22.     sql.append(" GROUP BY u.id, u.name, u.email, u.age");
  23.    
  24.     if (orderBy != null) {
  25.         sql.append(" ORDER BY ").append(orderBy);
  26.     }
  27.    
  28.     return namedParameterJdbcTemplate.query(sql.toString(), params, new BeanPropertyRowMapper<>(User.class));
  29. }
复制代码
解释

在现实应用中,利用 IN 子句进行批量查询是很常见的需求。NamedParameterJdbcTemplate 提供了灵活的方式来处理这种场景。
  1. public List<User> findUsersByIds(List<Long> ids) {
  2.     String sql = "SELECT id, name, email, age FROM users WHERE id IN (:ids)";
  3.    
  4.     MapSqlParameterSource params = new MapSqlParameterSource("ids", ids);
  5.    
  6.     return namedParameterJdbcTemplate.query(sql, params, new BeanPropertyRowMapper<>(User.class));
  7. }
复制代码
:ids 命名参数通过 MapSqlParameterSource 绑定到 List<Long> 参数中。NamedParameterJdbcTemplate 会自动将列表参数转换为适当的 SQL 格式,天生类似 IN (1, 2, 3) 的 SQL 语句。
复杂查询的处理与优化

在处理复杂查询时,除了动态 SQL,还需要考虑查询性能的优化。NamedParameterJdbcTemplate 提供了一些功能来帮助你优化查询操纵。
分页查询在处理大量数据时非常有用,它可以减少每次查询返回的数据量,从而提高性能。以下是一个分页查询的示例:
  1. public List<User> findUsersWithPagination(int pageNumber, int pageSize) {
  2.     String sql = "SELECT id, name, email, age FROM users LIMIT :offset, :limit";
  3.    
  4.     int offset = (pageNumber - 1) * pageSize;
  5.     MapSqlParameterSource params = new MapSqlParameterSource();
  6.     params.addValue("offset", offset);
  7.     params.addValue("limit", pageSize);
  8.    
  9.     return namedParameterJdbcTemplate.query(sql, params, new BeanPropertyRowMapper<>(User.class));
  10. }
复制代码
解释

有时,你大概需要在一个查询中嵌套另一个查询(子查询),或者需要在一个查询中团结多个表的数据。NamedParameterJdbcTemplate 可以帮助你处理这些复杂的查询场景。
  1. public List<UserOrderSummary> findUserOrderSummaries() {
  2.     String sql = "SELECT u.id, u.name, u.email, (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) as order_count " +
  3.                  "FROM users u";
  4.    
  5.     return namedParameterJdbcTemplate.query(sql, new MapSqlParameterSource(), (rs, rowNum) -> {
  6.         UserOrderSummary summary = new UserOrderSummary();
  7.         summary.setUserId(rs.getLong("id"));
  8.         summary.setUserName(rs.getString("name"));
  9.         summary.setUserEmail(rs.getString("email"));
  10.         summary.setOrderCount(rs.getInt("order_count"));
  11.         return summary;
  12.     });
  13. }
复制代码
在这个示例中,我们利用子查询来盘算每个用户的订单数量,并将效果映射到自界说的 UserOrderSummary 对象中。
在构建复杂 SQL 查询时,除了利用 NamedParameterJdbcTemplate 进行参数绑定和动态构建外,还应留意以下几点以优化性能:

总结

JdbcTemplate vs NamedParameterJdbcTemplate

JdbcTemplate:
优点缺点简化 JDBC 编程:JdbcTemplate 大大简化了传统的 JDBC 编程,消除了诸如资源管理、非常处理等繁琐的代码。代码维护性较差:由于 SQL 语句和业务逻辑精密耦合,随着业务复杂度的增加,代码的维护性会有所低落。性能优秀:相比于其他数据访问技术(如 JPA),JdbcTemplate 更加轻量级,通常能提供更好的性能,尤其是在不需要复杂 ORM 功能的场景中。缺乏灵活性:对于复杂的对象映射和关联查询,JdbcTemplate 大概显得力不从心,需要手动处理映射逻辑。强盛的批量操纵支持:JdbcTemplate 提供了强盛的批量操纵功能,实用于处理大数据量的插入、更新和删除。 NamedParameterJdbcTemplate:
优点缺点支持命名参数:相比 JdbcTemplate,NamedParameterJdbcTemplate 支持利用命名参数,SQL 语句更加直观,参数绑定更为灵活,尤其得当复杂的 SQL 查询。性能略低:在大多数情况下,NamedParameterJdbcTemplate 的性能略低于 JdbcTemplate,这是由于额外的参数剖析和绑定过程。更好的可读性:由于支持命名参数,代码的可读性和可维护性有所提升,特殊是在有大量参数的场景中。复杂性增加:对于简单的 SQL 操纵,NamedParameterJdbcTemplate 引入的额外复杂性大概并不划算。 利用发起

利用 JdbcTemplate 和 NamedParameterJdbcTemplate 时,有一些最佳实践可以帮助提高代码的质量和体系的性能。

常见问题

常见错误与非常处理

在利用 JdbcTemplate 和 NamedParameterJdbcTemplate 进行开发时,常见的错误和非常告急会合在 SQL 语法错误、数据绑定非常和数据库连接问题上。
SQL 语法错误

问题形貌:SQL 语法错误通常发生在拼写错误、缺少关键字或不正确的 SQL 结构时。
解决方法

数据绑定非常

问题形貌:数据绑定非常通常发生在传递给 SQL 语句的参数类型不匹配时,例如传递 null 值给非空字段。
解决方法

数据库连接问题

问题形貌:数据库连接问题通常与数据库配置、连接池设置或网络问题相关,常见的错误包括连接超时、连接池耗尽等。
解决方法

性能问题的排查与解决

在现实应用中,性能问题大概来自多个方面,包括数据库查询效率、连接池配置、批量操纵等。
数据库慢查询分析

问题形貌:性能问题通常首先出现在数据库查询上,特殊是复杂查询和大数据量查询。
解决方法

连接池配置调优

问题形貌:不合理的连接池配置大概导致连接池耗尽或相应速率迟钝。
解决方法

批量操纵调优

问题形貌:批量操纵在处理大数据量时大概会由于批次巨细不妥或事务处理问题导致性能下降。
解决方法

怎样选择 JdbcTemplate 与其他数据访问技术(如JPA)

选择符合的数据访问技术至关告急。JdbcTemplate 和 JPA(Java Persistence API)是两种常见的数据访问技术,它们各有优缺点,实用于不同的应用场景。
JdbcTemplate 的实用场景

优势回顾:

实用场景:

JPA 的实用场景

优势回顾:

实用场景:

怎样选择



免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4