Using JdbcTemplate

JdbcTemplate 是 JDBC 核心包中的中心类。它处理资源的创建和释放,这可以帮助你避免常见的错误,比如忘记关闭连接。它执行核心 JDBC 工作流程的基本任务(如语句的创建和执行),让应用程序代码提供 SQL 和提取结果。JdbcTemplate 类:

  • 运行 SQL 查询
  • 更新语句和存储过程调用
  • 对 ResultSet 实例进行迭代并提取返回的参数值。
  • 捕捉 JDBC 异常,并将其转换为 org.springframework.dao包中定义的通用的、信息量更大的异常层次结构。(参见 一致的异常层次结构)。

当你为你的代码使用 JdbcTemplate 时,你只需要实现回调接口,给它们一个明确定义的契约。给定一个由 JdbcTemplate 类提供的ConnectionPreparedStatementCreator回调接口会创建一个准备好的语句,提供 SQL 和任何必要参数。CallableStatementCreator接口也是如此,它创建可调用语句。RowCallbackHandler接口从 ResultSet的每一行提取数值。

你可以通过直接实例化 DataSource引用在 DAO 实现中使用 JdbcTemplate,也可以在 Spring IoC 容器中配置它并将其作为 Bean 引用给 DAO。

:::info 数据源应该总是被配置为 Spring IoC 容器中的一个 Bean。在第一种情况下,Bean 被直接给了服务;在第二种情况下,它被给了准备好的模板(template)。 :::

这个类发出的所有 SQL 都会被记录在 DEBUG 级别,在与模板实例的全限定类名(通常是 JdbcTemplate,但如果你使用 JdbcTemplate 类的自定义子类,它可能会有所不同)相对应的类别下。

下面几节提供了一些使用 JdbcTemplate 的例子。这些例子并不是 JdbcTemplate 所暴露的所有功能的详尽清单。请参见相关的 javadoc

准备工作 - 构建 JdbcTemplate 对象

前面都没有单独的写过项目,一直都在 spring boot 环境中写的测试,这里只引入需要的类来实现这些测试,创建一个测试项目,然后引入下面的这些依赖(我使用的是 gradle 管理的依赖)

  1. // junit 测试
  2. testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.8.2'
  3. // spring 测试框架
  4. implementation group: 'org.springframework', name: 'spring-test', version: '5.3.15'
  5. // 核心和上下文在
  6. implementation group: 'org.springframework', name: 'spring-core', version: '5.3.15'
  7. implementation group: 'org.springframework', name: 'spring-context', version: '5.3.15'
  8. // aop
  9. implementation group: 'org.springframework', name: 'spring-aop', version: '5.3.15'
  10. // jdbc 包 + mysql 驱动
  11. implementation group: 'org.springframework', name: 'spring-jdbc', version: '5.3.15'
  12. implementation group: 'mysql', name: 'mysql-connector-java', version: '8.0.28'

核心和上下文在 Core/核心 中讲到过,就是在这两个依赖里面。junit 和 spring 测试框架在 Testing/测试 中讲到过。

然后准备一个 mysql 数据库,准备一个数据库 spring-read-docs,创建一个测试表:

  1. CREATE TABLE `t_user` (
  2. `id` int(11) NOT NULL AUTO_INCREMENT,
  3. `username` varchar(255) DEFAULT NULL,
  4. PRIMARY KEY (`id`)
  5. ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
  6. -- 插入一条测试语句
  7. INSERT INTO `spring-read-docs`.`t_user` (`id`, `username`) VALUES (1, '张三');

构建 JdbcTemplate 对象

  1. package cn.mrcode.study.springdocsread;
  2. import com.mysql.cj.jdbc.MysqlDataSource;
  3. import org.springframework.context.annotation.Bean;
  4. import org.springframework.context.annotation.Configuration;
  5. import org.springframework.jdbc.core.JdbcTemplate;
  6. import javax.sql.DataSource;
  7. /**
  8. * @author mrcode
  9. */
  10. @Configuration
  11. public class AppConfig {
  12. // 构建 mysql 的数据源对象
  13. @Bean
  14. public DataSource dataSource() {
  15. final MysqlDataSource dataSource = new MysqlDataSource();
  16. dataSource.setURL("jdbc:mysql://127.0.0.1:3306/spring-read-docs?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=true");
  17. dataSource.setUser("root");
  18. dataSource.setPassword("root");
  19. return dataSource;
  20. }
  21. // 构建 JdbcTemplate
  22. @Bean
  23. public JdbcTemplate jdbcTemplate(DataSource dataSource) {
  24. return new JdbcTemplate(dataSource);
  25. }
  26. }

写一个测试类,使用 junit 来执行测试用例

  1. package cn.mrcode.study.springdocsread.test;
  2. import org.junit.jupiter.api.Test;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.jdbc.core.JdbcTemplate;
  5. import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
  6. import cn.mrcode.study.springdocsread.AppConfig;
  7. /**
  8. * @author mrcode
  9. */
  10. @SpringJUnitConfig({AppConfig.class})
  11. public class DemoTest {
  12. @Autowired
  13. private JdbcTemplate jdbcTemplate;
  14. @Test
  15. public void fun1() {
  16. // 如果正常打印出 1 ,就说明环境已经构建好了
  17. final Integer count = jdbcTemplate.queryForObject("select count(*)from user", Integer.class);
  18. System.out.println(count);
  19. }
  20. }

:::tips 说明:
如果嫌麻烦,可以不用这里的项目构建,自己使用 spring boot 环境,然后像上面那样创建出 JdbcTemplate ,或则直接通过 new 的方式使用好像也是可以的 :::

准备一张表,用于后面的练习使用

  1. CREATE TABLE `t_actor` (
  2. `id` int(11) NOT NULL AUTO_INCREMENT,
  3. `first_name` varchar(255) DEFAULT NULL,
  4. `last_name` varchar(255) DEFAULT NULL,
  5. PRIMARY KEY (`id`)
  6. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

:::tips 说明:后面练习中出现的语句,有可能这里没有给出来创建表的 SQL,那么要测试的话,就直接按需增加或则减少相关表。
因为这个教程是介绍如何使用、语法之类的,并不是练习查询的 :::

查询(SELECT)

Querying (SELECT)

下面的查询得到符合条件的行数:

  1. int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);

下面的查询使用了一个绑定变量:

  1. int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject(
  2. "select count(*) from t_actor where first_name = ?", Integer.class, "Joe");

下面查询返回了一个字符串:

  1. String lastName = this.jdbcTemplate.queryForObject(
  2. "select last_name from t_actor where id = ?",
  3. String.class, 1212L);

下面的查询找到并填充了一个单一的域对象:

  1. Actor actor = jdbcTemplate.queryForObject(
  2. "select first_name, last_name from t_actor where id = ?",
  3. (resultSet, rowNum) -> {
  4. Actor newActor = new Actor();
  5. newActor.setFirstName(resultSet.getString("first_name"));
  6. newActor.setLastName(resultSet.getString("last_name"));
  7. return newActor;
  8. },
  9. 1212L);

下面的查询找到并填充了一个域对象的列表:

  1. List<Actor> actors = this.jdbcTemplate.query(
  2. "select first_name, last_name from t_actor",
  3. (resultSet, rowNum) -> {
  4. Actor actor = new Actor();
  5. actor.setFirstName(resultSet.getString("first_name"));
  6. actor.setLastName(resultSet.getString("last_name"));
  7. return actor;
  8. });

如果最后两个代码片断实际存在于同一个应用程序中,那么去除两个 RowMapper lambda 表达式中存在的重复,并将其提取为一个字段(其实是一个匿名 RowMapper 实现对象),然后可以根据需要由 DAO 引用。例如,将前面的代码片断写成下面的样子可能更好:

  1. private final RowMapper<Actor> actorRowMapper = (resultSet, rowNum) -> {
  2. Actor actor = new Actor();
  3. actor.setFirstName(resultSet.getString("first_name"));
  4. actor.setLastName(resultSet.getString("last_name"));
  5. return actor;
  6. };
  7. public List<Actor> findAllActors() {
  8. return this.jdbcTemplate.query("select first_name, last_name from t_actor", actorRowMapper);
  9. }

其实我们可以使用内置的 BeanPropertyRowMapper 来帮助我们映射成 bean 对象

  1. List<Actor> actors = jdbcTemplate.query("select first_name as firstName, last_name as lastName from t_actor", new BeanPropertyRowMapper<>(Actor.class));

用 JdbcTemplate 进行更新(INSERT、UPDATE 和 DELETE)

Updating (INSERT, UPDATE, and DELETE) with JdbcTemplate

你可以使用 update(.)方法来执行插入、更新和删除操作。参数值通常作为变量参数提供,或者作为一个对象数组提供。

下面的例子是插入一条新的数据:

  1. this.jdbcTemplate.update(
  2. "insert into t_actor (first_name, last_name) values (?, ?)",
  3. "Leonor", "Watling");

下面的例子更新了一条现有数据:

  1. this.jdbcTemplate.update(
  2. "update t_actor set last_name = ? where id = ?",
  3. "Banjo", 5276L);

下面的例子删除了一条数据:

  1. this.jdbcTemplate.update(
  2. "delete from t_actor where id = ?",
  3. Long.valueOf(actorId));

其他 JdbcTemplate 操作

Other JdbcTemplate Operations

你可以使用 execute(.)方法来运行任何任意的 SQL。因此,该方法经常被用于 DDL 语句。它被大量地重载,有一些变体,可以采取回调接口、绑定变量数组等等。下面的例子创建了一个表:

  1. this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");

下面的例子调用了一个存储过程:

  1. // 虽然调用的是 update,本质上最后还是执行一条 SQL
  2. this.jdbcTemplate.update(
  3. "call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
  4. Long.valueOf(unionId));

JdbcTemplate 最佳实践

JdbcTemplate Best Practices

JdbcTemplate 类的实例一旦配置好,就是 线程安全 的。这很重要,因为这意味着你可以配置 JdbcTemplate 的一个实例,然后安全地将这个共享引用注入到多个 DAO(或存储库)。JdbcTemplate 是有状态的,因为它维护着对 DataSource 的引用,但这个状态不是对话状态。

在使用 JdbcTemplate 类(以及相关的 NamedParameterJdbcTemplate 类)时,一个常见的做法是在 Spring 配置文件中配置一个DataSource,然后将该共享的 DataSource Bean 依赖注入到 DAO 类中。JdbcTemplate 是在 DataSource 的 setter 中创建的。这导致了类似于以下的 DAO:

  1. public class JdbcCorporateEventDao implements CorporateEventDao {
  2. private JdbcTemplate jdbcTemplate;
  3. public void setDataSource(DataSource dataSource) {
  4. this.jdbcTemplate = new JdbcTemplate(dataSource);
  5. }
  6. // 对 CorporateEventDao上的方法的 JDBC 支持的实现遵循...
  7. }

下面的例子显示了相应的 XML 配置:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xmlns:context="http://www.springframework.org/schema/context"
  5. xsi:schemaLocation="
  6. http://www.springframework.org/schema/beans
  7. https://www.springframework.org/schema/beans/spring-beans.xsd
  8. http://www.springframework.org/schema/context
  9. https://www.springframework.org/schema/context/spring-context.xsd">
  10. <bean id="corporateEventDao" class="com.example.JdbcCorporateEventDao">
  11. <property name="dataSource" ref="dataSource"/>
  12. </bean>
  13. <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
  14. <property name="driverClassName" value="${jdbc.driverClassName}"/>
  15. <property name="url" value="${jdbc.url}"/>
  16. <property name="username" value="${jdbc.username}"/>
  17. <property name="password" value="${jdbc.password}"/>
  18. </bean>
  19. <!-- 处理 dataSource 中的占位符属性 -->
  20. <context:property-placeholder location="jdbc.properties"/>
  21. </beans>

显式配置的另一个选择是使用组件扫描和注解支持依赖注入。在这种情况下,你可以用 @Repository来注解这个类(这使它成为组件扫描的候选者),并用 @Autowired来注解 DataSourcesetter方法。下面的例子展示了如何做到这一点:

  1. @Repository
  2. public class JdbcCorporateEventDao implements CorporateEventDao {
  3. private JdbcTemplate jdbcTemplate;
  4. @Autowired
  5. public void setDataSource(DataSource dataSource) {
  6. this.jdbcTemplate = new JdbcTemplate(dataSource);
  7. }
  8. // JDBC-backed implementations of the methods on the CorporateEventDao follow...
  9. }

下面的例子显示了相应的 XML 配置:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xmlns:context="http://www.springframework.org/schema/context"
  5. xsi:schemaLocation="
  6. http://www.springframework.org/schema/beans
  7. https://www.springframework.org/schema/beans/spring-beans.xsd
  8. http://www.springframework.org/schema/context
  9. https://www.springframework.org/schema/context/spring-context.xsd">
  10. <!-- 在应用程序的基础包中扫描 @Component 类,将其配置为 Bean。 -->
  11. <context:component-scan base-package="org.springframework.docs.test" />
  12. <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
  13. <property name="driverClassName" value="${jdbc.driverClassName}"/>
  14. <property name="url" value="${jdbc.url}"/>
  15. <property name="username" value="${jdbc.username}"/>
  16. <property name="password" value="${jdbc.password}"/>
  17. </bean>
  18. <context:property-placeholder location="jdbc.properties"/>
  19. </beans>

如果你使用 Spring 的 JdbcDaoSupport 类,并且你的各种支持 JDBC 的 DAO 类都是从该类扩展而来,那么你的子类就会从 JdbcDaoSupport 类继承一个 setDataSource(..)方法。你可以选择是否继承自这个类。JdbcDaoSupport 类只是作为一种方便提供(该类暴露了一些常用的方法,比如从数据源创建一个 JdbcTemplate 类、获取链接、获取数据源等快捷方法)。

无论你选择使用(或不使用)上述哪种模板初始化方式,每次要运行 SQL 时,很少有必要创建一个新的 JdbcTemplate 类实例。一旦配置好, JdbcTemplate 实例就是线程安全的。如果你的应用程序访问多个数据库,你可能需要多个 JdbcTemplate 实例,这就需要多个 DataSources,然后需要多个不同配置的 JdbcTemplate 实例。