Using JdbcTemplate
JdbcTemplate 是 JDBC 核心包中的中心类。它处理资源的创建和释放,这可以帮助你避免常见的错误,比如忘记关闭连接。它执行核心 JDBC 工作流程的基本任务(如语句的创建和执行),让应用程序代码提供 SQL 和提取结果。JdbcTemplate 类:
- 运行 SQL 查询
- 更新语句和存储过程调用
- 对 ResultSet 实例进行迭代并提取返回的参数值。
- 捕捉 JDBC 异常,并将其转换为
org.springframework.dao
包中定义的通用的、信息量更大的异常层次结构。(参见 一致的异常层次结构)。
当你为你的代码使用 JdbcTemplate 时,你只需要实现回调接口,给它们一个明确定义的契约。给定一个由 JdbcTemplate 类提供的Connection
,PreparedStatementCreator
回调接口会创建一个准备好的语句,提供 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 管理的依赖)
// junit 测试
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.8.2'
// spring 测试框架
implementation group: 'org.springframework', name: 'spring-test', version: '5.3.15'
// 核心和上下文在
implementation group: 'org.springframework', name: 'spring-core', version: '5.3.15'
implementation group: 'org.springframework', name: 'spring-context', version: '5.3.15'
// aop
implementation group: 'org.springframework', name: 'spring-aop', version: '5.3.15'
// jdbc 包 + mysql 驱动
implementation group: 'org.springframework', name: 'spring-jdbc', version: '5.3.15'
implementation group: 'mysql', name: 'mysql-connector-java', version: '8.0.28'
核心和上下文在 Core/核心 中讲到过,就是在这两个依赖里面。junit 和 spring 测试框架在 Testing/测试 中讲到过。
然后准备一个 mysql 数据库,准备一个数据库 spring-read-docs
,创建一个测试表:
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
-- 插入一条测试语句
INSERT INTO `spring-read-docs`.`t_user` (`id`, `username`) VALUES (1, '张三');
构建 JdbcTemplate 对象
package cn.mrcode.study.springdocsread;
import com.mysql.cj.jdbc.MysqlDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
/**
* @author mrcode
*/
@Configuration
public class AppConfig {
// 构建 mysql 的数据源对象
@Bean
public DataSource dataSource() {
final MysqlDataSource dataSource = new MysqlDataSource();
dataSource.setURL("jdbc:mysql://127.0.0.1:3306/spring-read-docs?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=true");
dataSource.setUser("root");
dataSource.setPassword("root");
return dataSource;
}
// 构建 JdbcTemplate
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
写一个测试类,使用 junit 来执行测试用例
package cn.mrcode.study.springdocsread.test;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import cn.mrcode.study.springdocsread.AppConfig;
/**
* @author mrcode
*/
@SpringJUnitConfig({AppConfig.class})
public class DemoTest {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
public void fun1() {
// 如果正常打印出 1 ,就说明环境已经构建好了
final Integer count = jdbcTemplate.queryForObject("select count(*)from user", Integer.class);
System.out.println(count);
}
}
:::tips
说明:
如果嫌麻烦,可以不用这里的项目构建,自己使用 spring boot 环境,然后像上面那样创建出 JdbcTemplate ,或则直接通过 new 的方式使用好像也是可以的
:::
准备一张表,用于后面的练习使用
CREATE TABLE `t_actor` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`first_name` varchar(255) DEFAULT NULL,
`last_name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
:::tips
说明:后面练习中出现的语句,有可能这里没有给出来创建表的 SQL,那么要测试的话,就直接按需增加或则减少相关表。
因为这个教程是介绍如何使用、语法之类的,并不是练习查询的
:::
查询(SELECT)
Querying (SELECT)
下面的查询得到符合条件的行数:
int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);
下面的查询使用了一个绑定变量:
int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject(
"select count(*) from t_actor where first_name = ?", Integer.class, "Joe");
下面查询返回了一个字符串:
String lastName = this.jdbcTemplate.queryForObject(
"select last_name from t_actor where id = ?",
String.class, 1212L);
下面的查询找到并填充了一个单一的域对象:
Actor actor = jdbcTemplate.queryForObject(
"select first_name, last_name from t_actor where id = ?",
(resultSet, rowNum) -> {
Actor newActor = new Actor();
newActor.setFirstName(resultSet.getString("first_name"));
newActor.setLastName(resultSet.getString("last_name"));
return newActor;
},
1212L);
下面的查询找到并填充了一个域对象的列表:
List<Actor> actors = this.jdbcTemplate.query(
"select first_name, last_name from t_actor",
(resultSet, rowNum) -> {
Actor actor = new Actor();
actor.setFirstName(resultSet.getString("first_name"));
actor.setLastName(resultSet.getString("last_name"));
return actor;
});
如果最后两个代码片断实际存在于同一个应用程序中,那么去除两个 RowMapper
lambda 表达式中存在的重复,并将其提取为一个字段(其实是一个匿名 RowMapper 实现对象),然后可以根据需要由 DAO 引用。例如,将前面的代码片断写成下面的样子可能更好:
private final RowMapper<Actor> actorRowMapper = (resultSet, rowNum) -> {
Actor actor = new Actor();
actor.setFirstName(resultSet.getString("first_name"));
actor.setLastName(resultSet.getString("last_name"));
return actor;
};
public List<Actor> findAllActors() {
return this.jdbcTemplate.query("select first_name, last_name from t_actor", actorRowMapper);
}
其实我们可以使用内置的 BeanPropertyRowMapper 来帮助我们映射成 bean 对象
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(.)
方法来执行插入、更新和删除操作。参数值通常作为变量参数提供,或者作为一个对象数组提供。
下面的例子是插入一条新的数据:
this.jdbcTemplate.update(
"insert into t_actor (first_name, last_name) values (?, ?)",
"Leonor", "Watling");
下面的例子更新了一条现有数据:
this.jdbcTemplate.update(
"update t_actor set last_name = ? where id = ?",
"Banjo", 5276L);
下面的例子删除了一条数据:
this.jdbcTemplate.update(
"delete from t_actor where id = ?",
Long.valueOf(actorId));
其他 JdbcTemplate 操作
Other JdbcTemplate Operations
你可以使用 execute(.)
方法来运行任何任意的 SQL。因此,该方法经常被用于 DDL 语句。它被大量地重载,有一些变体,可以采取回调接口、绑定变量数组等等。下面的例子创建了一个表:
this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
下面的例子调用了一个存储过程:
// 虽然调用的是 update,本质上最后还是执行一条 SQL
this.jdbcTemplate.update(
"call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
Long.valueOf(unionId));
JdbcTemplate 最佳实践
JdbcTemplate Best Practices
JdbcTemplate 类的实例一旦配置好,就是 线程安全 的。这很重要,因为这意味着你可以配置 JdbcTemplate 的一个实例,然后安全地将这个共享引用注入到多个 DAO(或存储库)。JdbcTemplate 是有状态的,因为它维护着对 DataSource 的引用,但这个状态不是对话状态。
在使用 JdbcTemplate 类(以及相关的 NamedParameterJdbcTemplate 类)时,一个常见的做法是在 Spring 配置文件中配置一个DataSource,然后将该共享的 DataSource Bean 依赖注入到 DAO 类中。JdbcTemplate 是在 DataSource 的 setter 中创建的。这导致了类似于以下的 DAO:
public class JdbcCorporateEventDao implements CorporateEventDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
// 对 CorporateEventDao上的方法的 JDBC 支持的实现遵循...
}
下面的例子显示了相应的 XML 配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<bean id="corporateEventDao" class="com.example.JdbcCorporateEventDao">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!-- 处理 dataSource 中的占位符属性 -->
<context:property-placeholder location="jdbc.properties"/>
</beans>
显式配置的另一个选择是使用组件扫描和注解支持依赖注入。在这种情况下,你可以用 @Repository
来注解这个类(这使它成为组件扫描的候选者),并用 @Autowired
来注解 DataSource
的 setter
方法。下面的例子展示了如何做到这一点:
@Repository
public class JdbcCorporateEventDao implements CorporateEventDao {
private JdbcTemplate jdbcTemplate;
@Autowired
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
// JDBC-backed implementations of the methods on the CorporateEventDao follow...
}
下面的例子显示了相应的 XML 配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<!-- 在应用程序的基础包中扫描 @Component 类,将其配置为 Bean。 -->
<context:component-scan base-package="org.springframework.docs.test" />
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<context:property-placeholder location="jdbc.properties"/>
</beans>
如果你使用 Spring 的 JdbcDaoSupport 类,并且你的各种支持 JDBC 的 DAO 类都是从该类扩展而来,那么你的子类就会从 JdbcDaoSupport 类继承一个 setDataSource(..)
方法。你可以选择是否继承自这个类。JdbcDaoSupport 类只是作为一种方便提供(该类暴露了一些常用的方法,比如从数据源创建一个 JdbcTemplate 类、获取链接、获取数据源等快捷方法)。
无论你选择使用(或不使用)上述哪种模板初始化方式,每次要运行 SQL 时,很少有必要创建一个新的 JdbcTemplate 类实例。一旦配置好, JdbcTemplate 实例就是线程安全的。如果你的应用程序访问多个数据库,你可能需要多个 JdbcTemplate 实例,这就需要多个 DataSources,然后需要多个不同配置的 JdbcTemplate 实例。