使用JDBC核心类来控制基本的JDBC处理和错误处理
本节介绍了如何使用JDBC核心类来控制基本的JDBC处理,包括错误处理。它包括以下主题:
使用 JdbcTemplate
JdbcTemplate 是 JDBC 核心包中的核心类。它负责资源的创建和释放,这有助于你避免一些常见的错误,比如忘记关闭连接。它执行 JDBC 核心工作流的基本任务(如语句的创建和执行),将提供 SQL 语句和提取结果的工作留给应用程序代码来处理。JdbcTemplate 类:
- 运行SQL查询
- 更新语句和存储过程调用
- 遍历
ResultSet实例并提取返回的参数值 - 捕捉JDBC异常,并将其转换为
org.springframework.dao包中定义的通用、更具信息量的异常层次结构。(参见一致的异常层次结构。)
当你在代码中使用JdbcTemplate时,只需要实现回调接口,并为这些接口提供一个明确定义的契约即可。JdbcTemplate类提供了一个Connection,PreparedStatementCreator回调接口可以利用这个Connection来创建预编译语句(prepared statement),并提供SQL语句以及任何必要的参数。CallableStatementCreator接口也是如此,它用于创建可调用语句(callable statements)。而RowCallbackHandler接口则用于从ResultSet的每一行中提取数值。
你可以在DAO实现中使用JdbcTemplate,通过直接使用DataSource引用进行实例化;或者,你也可以在Spring IoC容器中配置它,然后将其作为bean引用提供给DAOs。
DataSource 应始终作为 Bean 配置在 Spring IoC 容器中。在第一种情况下,该 Bean 直接被提供给服务;在第二种情况下,它被提供给已准备的模板。
该类发出的所有SQL操作都会以DEBUG级别进行日志记录,日志类别与模板实例的完全限定类名相对应(通常是JdbcTemplate,但如果您使用了JdbcTemplate类的自定义子类,那么日志类别可能会有所不同)。
以下部分提供了一些JdbcTemplate使用的示例。这些示例并不涵盖JdbcTemplate所提供的所有功能的全部列表。关于JdbcTemplate的完整功能,请参阅相应的javadoc。
查询(SELECT)
以下查询可以获取关系(relation)中的行数:
- Java
- Kotlin
int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);
val rowCount = jdbcTemplate.queryForObject<Int>("select count(*) from t_actor")!!
以下查询使用了一个绑定变量:
- Java
- Kotlin
int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject(
"select count(*) from t_actor where first_name = ?", Integer.class, "Joe");
val countOfActorsNamedJoe = jdbcTemplate.queryForObject<Int>(
"select count(*) from t_actor where first_name = ?", arrayOf("Joe"))!!
以下查询用于查找一个String:
- Java
- Kotlin
String lastName = this.jdbcTemplate.queryForObject(
"select last_name from t_actor where id = ?",
String.class, 1212L);
val lastName = this.jdbcTemplate.queryForObject<String>(
"select last_name from t_actor where id = ?",
arrayOf(1212L))!!
以下查询可以找到并填充一个单独的域对象:
- Java
- Kotlin
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);
val actor = jdbcTemplate.queryForObject(
"select first_name, last_name from t_actor where id = ?",
arrayOf(1212L)) { rs, _ ->
Actor(rs.getString("first_name"), rs.getString("last_name"))
}
以下查询会查找并填充一个域名对象列表:
- Java
- Kotlin
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;
});
val actors = jdbcTemplate.query("select first_name, last_name from t_actor") { rs, _ ->
Actor(rs.getString("first_name"), rs.getString("last_name"))
如果最后两段代码确实存在于同一个应用程序中,那么就有必要消除两个RowMapper lambda表达式中的重复代码,将它们提取出来放入一个单独的字段中,这样DAO方法就可以根据需要引用这个字段了。例如,上述代码片段可以更好地写成如下形式:
- Java
- Kotlin
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);
}
val actorMapper = RowMapper<Actor> { rs: ResultSet, rowNum: Int ->
Actor(rs.getString("first_name"), rs.getString("last_name"))
}
fun findAllActors(): List<Actor> {
return jdbcTemplate.query("select first_name, last_name from t_actor", actorMapper)
}
使用 JdbcTemplate 进行更新(INSERT、UPDATE 和 DELETE)
你可以使用update(..)方法来执行插入、更新和删除操作。参数值通常以可变参数的形式提供,或者也可以以对象数组的形式提供。
以下示例插入了一条新记录:
- Java
- Kotlin
this.jdbcTemplate.update(
"insert into t_actor (first_name, last_name) values (?, ?)",
"Leonor", "Watling");
jdbcTemplate.update(
"insert into t_actor (first_name, last_name) values (?, ?)",
"Leonor", "Watling")
以下示例更新现有的条目:
- Java
- Kotlin
this.jdbcTemplate.update(
"update t_actor set last_name = ? where id = ?",
"Banjo", 5276L);
jdbcTemplate.update(
"update t_actor set last_name = ? where id = ?",
"Banjo", 5276L)
以下示例删除了一条记录:
- Java
- Kotlin
this.jdbcTemplate.update(
"delete from t_actor where id = ?",
Long.valueOf(actorId));
jdbcTemplate.update("delete from t_actor where id = ?", actorId.toLong())
其他 JdbcTemplate 操作
你可以使用execute(..)方法来运行任何任意的SQL语句。因此,该方法通常被用于执行DDL(数据定义语言)语句。该方法有大量的重载版本,可以接受回调接口、绑定变量数组等参数。以下示例创建了一个表格:
- Java
- Kotlin
this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
jdbcTemplate.execute("create table mytable (id integer, name varchar(100))")
以下示例调用了一个存储过程:
- Java
- Kotlin
this.jdbcTemplate.update(
"call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
Long.valueOf(unionId));
jdbcTemplate.update(
"call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
unionId.toLong())
更复杂的存储过程支持将在后面介绍。
JdbcTemplate最佳实践
一旦配置完成,JdbcTemplate 类的实例就是线程安全的。这一点很重要,因为这意味着你可以配置一个 JdbcTemplate 的单一样本,然后安全地将这个共享引用注入到多个 DAO(或仓库)中。JdbcTemplate 是有状态的,因为它维护着一个对 DataSource 的引用,但这种状态不是会话状态(conversational state)。
在使用JdbcTemplate类(以及相关的NamedParameterJdbcTemplate类)时,常见的做法是在Spring配置文件中配置一个DataSource,然后通过依赖注入将该共享的DataSource bean注入到DAO类中。JdbcTemplate是在DataSource的setter方法或构造函数中创建的。这样生成的DAO类如下所示:
- Java
- Kotlin
public class JdbcCorporateEventDao implements CorporateEventDao {
private final JdbcTemplate jdbcTemplate;
public JdbcCorporateEventDao(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
// JDBC-backed implementations of the methods on the CorporateEventDao follow...
}
class JdbcCorporateEventDao(dataSource: DataSource): CorporateEventDao {
private val jdbcTemplate = JdbcTemplate(dataSource)
// JDBC-backed implementations of the methods on the CorporateEventDao follow...
}
以下示例展示了相应的配置:
- Java
- Kotlin
- Xml
@Bean
JdbcCorporateEventDao corporateEventDao(DataSource dataSource) {
return new JdbcCorporateEventDao(dataSource);
}
@Bean(destroyMethod = "close")
BasicDataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}
@Bean
fun corporateEventDao(dataSource: DataSource) = JdbcCorporateEventDao(dataSource)
@Bean(destroyMethod = "close")
fun dataSource() = BasicDataSource().apply {
driverClassName = "org.hsqldb.jdbcDriver"
url = "jdbc:hsqldb:hsql://localhost:"
username = "sa"
password = ""
}
<bean id="corporateEventDao" class="org.example.jdbc.JdbcCorporateEventDao">
<constructor-arg ref="dataSource"/>
</bean>
<bean id="dataSource" class="org.apache.commons.dbcp2.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"/>
一种替代显式配置的方法是使用组件扫描和注解支持来进行依赖注入。在这种情况下,你可以在类上添加@Repository注解(这会使该类成为组件扫描的候选对象)。以下示例展示了如何操作:
@Repository
public class JdbcCorporateEventRepository implements CorporateEventRepository {
private JdbcTemplate jdbcTemplate;
// Implicitly autowire the DataSource constructor parameter
public JdbcCorporateEventRepository(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
// JDBC-backed implementations of the methods on the CorporateEventRepository follow...
}
以下示例展示了相应的配置:
- Java
- Kotlin
- Xml
@Configuration
@ComponentScan("org.example.jdbc")
public class JdbcCorporateEventRepositoryConfiguration {
@Bean(destroyMethod = "close")
BasicDataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}
}
@Configuration
@ComponentScan("org.example.jdbc")
class JdbcCorporateEventRepositoryConfiguration {
@Bean(destroyMethod = "close")
fun dataSource() = BasicDataSource().apply {
driverClassName = "org.hsqldb.jdbcDriver"
url = "jdbc:hsqldb:hsql://localhost:"
username = "sa"
password = ""
}
}
<!-- Scans within the base package of the application for @Component classes to configure as beans -->
<context:component-scan base-package="org.example.jdbc" />
<bean id="dataSource" class="org.apache.commons.dbcp2.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"/>
如果你使用Spring的JdbcDaoSupport类,并且你的各种基于JDBC的DAO类都继承自它,那么你的子类就会从JdbcDaoSupport类中继承一个setDataSource(..)方法。你可以选择是否要继承这个类。JdbcDaoSupport类的提供仅仅是为了方便而已。
无论您选择使用(或不使用)上述哪种模板初始化样式,每次运行SQL时都创建一个新的JdbcTemplate实例都是不必要的。一旦配置完成,JdbcTemplate实例就是线程安全的。如果您的应用程序访问多个数据库,那么您可能需要多个JdbcTemplate实例,这就需要多个DataSource,进而也需要多个配置不同的JdbcTemplate实例。
使用 NamedParameterJdbcTemplate
NamedParameterJdbcTemplate 类通过使用命名参数来支持 JDBC 语句的编程,而不仅仅是使用传统的占位符(‘?’)参数来进行 JDBC 语句的编程。NamedParameterJdbcTemplate 类封装了一个 JdbcTemplate,并将其大部分工作委托给被封装的 JdbcTemplate 来完成。本节仅描述 NamedParameterJdbcTemplate 类中与 JdbcTemplate 本身不同的部分——即,通过使用命名参数来编程 JDBC 语句。以下示例展示了如何使用 NamedParameterJdbcTemplate:
- Java
- Kotlin
// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int countOfActorsByFirstName(String firstName) {
String sql = "select count(*) from t_actor where first_name = :first_name";
SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName);
return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}
private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)
fun countOfActorsByFirstName(firstName: String): Int {
val sql = "select count(*) from t_actor where first_name = :first_name"
val namedParameters = MapSqlParameterSource("first_name", firstName)
return namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Int::class.java)!!
}
请注意在赋值给 sql 变量的值中使用的命名参数表示法,以及被插入到 namedParameters 变量(类型为 MapSqlParameterSource)中的相应值。
或者,您可以使用基于Map的方式将命名参数及其对应的值传递给NamedParameterJdbcTemplate实例。NamedParameterJdbcOperations类提供的其余方法以及由NamedParameterJdbcTemplate类实现的方法也遵循类似的模式,在这里不再赘述。
以下示例展示了基于Map的用法:
- Java
- Kotlin
// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int countOfActorsByFirstName(String firstName) {
String sql = "select count(*) from t_actor where first_name = :first_name";
Map<String, String> namedParameters = Collections.singletonMap("first_name", firstName);
return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}
// some JDBC-backed DAO class...
private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)
fun countOfActorsByFirstName(firstName: String): Int {
val sql = "select count(*) from t_actor where first_name = :first_name"
val namedParameters = mapOf("first_name" to firstName)
return namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Int::class.java)!!
}
与NamedParameterJdbcTemplate相关的一个不错特性(并且也存在于同一个Java包中)是SqlParameterSource接口。在之前的代码片段中,你已经看到了这个接口的一个实现示例(MapSqlParameterSource类)。SqlParameterSource是NamedParameterJdbcTemplate的命名参数值来源。MapSqlParameterSource类是一个简单的实现,它实际上是对java.util.Map的适配器,其中键是参数名,值是参数值。
另一种SqlParameterSource的实现是BeanPropertySqlParameterSource类。这个类封装了一个任意的JavaBean(即,遵循JavaBean规范的类的实例),并使用被封装的JavaBean的属性作为命名参数值的来源。
以下示例展示了一个典型的JavaBean:
- Java
- Kotlin
public class Actor {
private Long id;
private String firstName;
private String lastName;
public String getFirstName() {
return this.firstName;
}
public String getLastName() {
return this.lastName;
}
public Long getId() {
return this.id;
}
// setters omitted...
}
data class Actor(val id: Long, val firstName: String, val lastName: String)
以下示例使用NamedParameterJdbcTemplate来返回前面示例中所示类的成员数量:
- Java
- Kotlin
// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int countOfActors(Actor exampleActor) {
// notice how the named parameters match the properties of the above 'Actor' class
String sql = "select count(*) from t_actor where first_name = :firstName and last_name = :lastName";
SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(exampleActor);
return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}
// some JDBC-backed DAO class...
private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)
private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)
fun countOfActors(exampleActor: Actor): Int {
// notice how the named parameters match the properties of the above 'Actor' class
val sql = "select count(*) from t_actor where first_name = :firstName and last_name = :lastName"
val namedParameters = BeanPropertySqlParameterSource(exampleActor)
return namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Int::class.java)!!
}
请记住,NamedParameterJdbcTemplate 类封装了一个经典的 JdbcTemplate 模板。如果您需要访问被封装的 JdbcTemplate 实例,以使用仅存在于 JdbcTemplate 类中的功能,可以使用 getJdbcOperations() 方法通过 JdbcOperations 接口来访问该 JdbcTemplate。
另请参阅 JdbcTemplate Best Practices,以获取在应用程序中使用 NamedParameterJdbcTemplate 类的指南。
统一的JDBC查询/更新操作:JdbcClient
从6.1版本开始,NamedParameterJdbcTemplate的命名参数语句和普通JdbcTemplate的位置参数语句可以通过一个统一的客户端API来使用,该API提供了流畅的交互模型。
例如,使用位置参数时:
private JdbcClient jdbcClient = JdbcClient.create(dataSource);
public int countOfActorsByFirstName(String firstName) {
return this.jdbcClient.sql("select count(*) from t_actor where first_name = ?")
.param(firstName)
.query(Integer.class).single();
}
例如,使用命名参数时:
private JdbcClient jdbcClient = JdbcClient.create(dataSource);
public int countOfActorsByFirstName(String firstName) {
return this.jdbcClient.sql("select count(*) from t_actor where first_name = :firstName")
.param("firstName", firstName)
.query(Integer.class).single();
}
RowMapper 的功能也同样可用,且具有灵活的结果解析能力:
List<Actor> actors = this.jdbcClient.sql("select first_name, last_name from t_actor")
.query((rs, rowNum) -> new Actor(rs.getString("first_name"), rs.getString("last_name")))
.list();
除了使用自定义的RowMapper之外,你也可以指定一个类来进行映射。例如,假设Actor类具有firstName和lastName属性(作为记录类的字段)、自定义构造函数、Bean属性或普通字段:
List<Actor> actors = this.jdbcClient.sql("select first_name, last_name from t_actor")
.query(Actor.class)
.list();
需要返回单个对象的结果:
Actor actor = this.jdbcClient.sql("select first_name, last_name from t_actor where id = ?")
.param(1212L)
.query(Actor.class)
.single();
使用 java.util.Optional 的结果:
Optional<Actor> actor = this.jdbcClient.sql("select first_name, last_name from t_actor where id = ?")
.param(1212L)
.query(Actor.class)
.optional();
而对于更新语句(update statement):
this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (?, ?)")
.param("Leonor").param("Watling")
.update();
或者是一个带有命名参数的更新语句:
this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (:firstName, :lastName)")
.param("firstName", "Leonor").param("lastName", "Watling")
.update();
除了使用单独命名的参数外,您还可以指定一个参数源对象——例如,一个记录类(record class)、一个具有Bean属性的类,或者一个提供firstName和lastName属性的简单字段持有者(field holder),就像上面提到的Actor类那样:
this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (:firstName, :lastName)")
.paramSource(new Actor("Leonor", "Watling"))
.update();
上述参数及查询结果的自动Actor类映射是通过隐式的SimplePropertySqlParameterSource和SimplePropertyRowMapper策略实现的,这些策略也可以直接使用。它们可以作为BeanPropertySqlParameterSource以及BeanPropertyRowMapper/DataClassRowMapper的通用替代品,同时也可以与JdbcTemplate和NamedParameterJdbcTemplate一起使用。
JdbcClient 是一个灵活但简化的 JDBC 查询/更新语句封装层。诸如批量插入和存储过程调用等高级功能通常需要额外的定制:如果 JdbcClient 不支持这些功能,可以考虑使用 Spring 的 SimpleJdbcInsert 和 SimpleJdbcCall 类,或者直接使用 JdbcTemplate。
使用 SQLExceptionTranslator
SQLExceptionTranslator 是一个接口,需要由那些能够将 SQLException 与 Spring 自带的 org.springframework.dao.DataAccessException 之间进行转换的类来实现。org.springframework.dao.DataAccessException 对数据访问策略是无关紧要的(即其不依赖于具体的数据访问方式)。实现方式可以是通用的(例如,对于 JDBC 使用 SQLState 代码进行转换),也可以是专有的(例如,使用 Oracle 的错误代码进行转换),以便实现更高的精确度。这种异常转换机制被用于常见的 JdbcTemplate 和 JdbcTransactionManager 入口点中;这些类在处理异常时不会直接抛出 SQLException,而是会抛出 DataAccessException。
从6.0版本开始,默认的异常转换器是SQLExceptionSubclassTranslator,它通过一些额外的检查来识别JDBC 4的SQLException子类,并在无法识别的情况下通过SQLStateSQLExceptionTranslator进行SQLState的内部检测。这通常对于常见的数据库访问来说是足够的,不需要特定于供应商的检测方式。为了保持向后兼容性,可以考虑使用下面描述的SQLErrorCodeSQLExceptionTranslator,并且可以自定义错误代码的映射。
SQLErrorCodeSQLExceptionTranslator 是 SQLExceptionTranslator 的实现版本,当类路径(classpath)的根目录下存在名为 sql-error-codes.xml 的文件时,会默认使用这一实现。该实现使用了特定供应商提供的错误代码。与基于 SQLState 或 SQLException 子类的转换方式相比,它的精度更高。错误代码的转换是基于一个名为 SQLErrorCodes 的 JavaBean 类中存储的代码来完成的。这个类是由 SQLErrorCodesFactory 创建并填充的,顾名思义,SQLErrorCodesFactory 是一个根据名为 sql-error-codes.xml 的配置文件内容来生成 SQLErrorCodes 对象的工厂。该配置文件中记录了供应商提供的错误代码,这些代码是根据从 DatabaseMetaData 中获取的 DatabaseProductName 来确定的。实际使用的数据库所对应的错误代码也会被用来进行转换。
SQLErrorCodeSQLExceptionTranslator 按照以下顺序应用匹配规则:
-
任何由子类实现的自定义翻译。通常情况下,会使用提供的具体
SQLErrorCodeSQLExceptionTranslator,因此此规则不适用。只有当你实际提供了子类实现时,该规则才适用。 -
任何作为
SQLErrorCodes类的customSqlExceptionTranslator属性提供的SQLExceptionTranslator接口的自定义实现。 -
会在
CustomSQLErrorCodesTranslation类的实例列表中(该列表是作为SQLErrorCodes类的customTranslations属性提供的)进行匹配查找。 -
应用错误代码匹配。
-
使用回退翻译器。
SQLExceptionSubclassTranslator是默认的回退翻译器。如果此翻译器不可用,下一个回退翻译器是SQLStateSQLExceptionTranslator。
默认情况下,使用SQLErrorCodesFactory来定义错误代码和自定义异常翻译。这些错误代码会从类路径中的sql-error-codes.xml文件中查找,而对应的SQLErrorCodes实例则会根据所使用数据库的元数据中的数据库名称来定位。
你可以扩展SQLErrorCodeSQLExceptionTranslator,如下例所示:
- Java
- Kotlin
public class CustomSQLErrorCodesTranslator extends SQLErrorCodeSQLExceptionTranslator {
protected DataAccessException customTranslate(String task, String sql, SQLException sqlEx) {
if (sqlEx.getErrorCode() == -12345) {
return new DeadlockLoserDataAccessException(task, sqlEx);
}
return null;
}
}
class CustomSQLErrorCodesTranslator : SQLErrorCodeSQLExceptionTranslator() {
override fun customTranslate(task: String, sql: String?, sqlEx: SQLException): DataAccessException? {
if (sqlEx.errorCode == -12345) {
return DeadlockLoserDataAccessException(task, sqlEx)
}
return null
}
}
在前面的例子中,特定的错误代码(-12345)被翻译了,而其他错误则由默认的翻译器实现来处理。要使用这个自定义翻译器,你必须通过setExceptionTranslator方法将其传递给JdbcTemplate,并且你必须在所有需要这个翻译器的数据访问处理中都使用这个JdbcTemplate。以下示例展示了如何使用这个自定义翻译器:
- Java
- Kotlin
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
// create a JdbcTemplate and set data source
this.jdbcTemplate = new JdbcTemplate();
this.jdbcTemplate.setDataSource(dataSource);
// create a custom translator and set the DataSource for the default translation lookup
CustomSQLErrorCodesTranslator tr = new CustomSQLErrorCodesTranslator();
tr.setDataSource(dataSource);
this.jdbcTemplate.setExceptionTranslator(tr);
}
public void updateShippingCharge(long orderId, long pct) {
// use the prepared JdbcTemplate for this update
this.jdbcTemplate.update("update orders" +
" set shipping_charge = shipping_charge * ? / 100" +
" where id = ?", pct, orderId);
}
// create a JdbcTemplate and set data source
private val jdbcTemplate = JdbcTemplate(dataSource).apply {
// create a custom translator and set the DataSource for the default translation lookup
exceptionTranslator = CustomSQLErrorCodesTranslator().apply {
this.dataSource = dataSource
}
}
fun updateShippingCharge(orderId: Long, pct: Long) {
// use the prepared JdbcTemplate for this update
this.jdbcTemplate!!.update("update orders" +
" set shipping_charge = shipping_charge * ? / 100" +
" where id = ?", pct, orderId)
}
自定义翻译器会接收一个数据源,以便在sql-error-codes.xml中查找错误代码。
运行语句
执行SQL语句所需的代码非常少。你需要一个DataSource和一个JdbcTemplate,以及JdbcTemplate提供的便捷方法。以下示例展示了创建一个新表所需包含的内容,这个类虽然简单,但功能却是完整的:
- Java
- Kotlin
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
public class ExecuteAStatement {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public void doExecute() {
this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
}
}
import javax.sql.DataSource
import org.springframework.jdbc.core.JdbcTemplate
class ExecuteAStatement(dataSource: DataSource) {
private val jdbcTemplate = JdbcTemplate(dataSource)
fun doExecute() {
jdbcTemplate.execute("create table mytable (id integer, name varchar(100))")
}
}
运行查询
一些查询方法会返回单个值。要从某一行中检索计数或特定值,可以使用queryForObject(..)。该方法会将返回的JDBC Type转换为作为参数传递的Java类。如果类型转换无效,则会抛出InvalidDataAccessApiUsageException异常。以下示例包含两个查询方法,一个用于查询int类型的数据,另一个用于查询String类型的数据:
- Java
- Kotlin
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
public class RunAQuery {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public int getCount() {
return this.jdbcTemplate.queryForObject("select count(*) from mytable", Integer.class);
}
public String getName() {
return this.jdbcTemplate.queryForObject("select name from mytable", String.class);
}
}
import javax.sql.DataSource
import org.springframework.jdbc.core.JdbcTemplate
class RunAQuery(dataSource: DataSource) {
private val jdbcTemplate = JdbcTemplate(dataSource)
val count: Int
get() = jdbcTemplate.queryForObject("select count(*) from mytable")!!
val name: String?
get() = jdbcTemplate.queryForObject("select name from mytable")
}
除了单条结果查询方法外,还有几种方法会返回一个列表,其中每个条目对应查询返回的一行数据。最通用的方法是queryForList(..),它返回一个List,列表中的每个元素都是一个Map,每个Map的键是列名,值是该列对应的行数据。如果你在之前的例子中添加一个方法来检索所有行的列表,可能如下所示:
- Java
- Kotlin
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public List<Map<String, Object>> getList() {
return this.jdbcTemplate.queryForList("select * from mytable");
}
private val jdbcTemplate = JdbcTemplate(dataSource)
fun getList(): List<Map<String, Any>> {
return jdbcTemplate.queryForList("select * from mytable")
}
返回的列表将类似于以下内容:
[{name=Bob, id=1}, {name=Mary, id=2}]
更新数据库
以下示例更新了具有某个主键的列:
- Java
- Kotlin
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
public class ExecuteAnUpdate {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public void setName(int id, String name) {
this.jdbcTemplate.update("update mytable set name = ? where id = ?", name, id);
}
}
import javax.sql.DataSource
import org.springframework.jdbc.core.JdbcTemplate
class ExecuteAnUpdate(dataSource: DataSource) {
private val jdbcTemplate = JdbcTemplate(dataSource)
fun setName(id: Int, name: String) {
jdbcTemplate.update("update mytable set name = ? where id = ?", name, id)
}
}
在前面的例子中,SQL语句中包含了用于行参数的占位符。你可以将参数值作为可变参数(varargs)传递,或者也可以将它们作为一个对象数组传递。因此,你应该明确地将基本数据类型包装在相应的包装类中,或者使用自动装箱(auto-boxing)功能。
获取自动生成的键
update() 这个便捷方法支持检索由数据库生成的主键。这一支持是 JDBC 3.0 标准的一部分。详情请参见规范的第 13.6 章。该方法的首个参数是一个 PreparedStatementCreator,通过它来指定所需的插入语句。另一个参数是 KeyHolder,在更新操作成功返回后,该对象会包含生成的主键。目前没有一种标准的统一方式来创建合适的 PreparedStatement(这就是为什么方法的签名是这样的)。以下示例可以在 Oracle 上运行,但在其他平台上可能无法运行:
- Java
- Kotlin
final String INSERT_SQL = "insert into my_test (name) values(?)";
final String name = "Rob";
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(INSERT_SQL, new String[] { "id" });
ps.setString(1, name);
return ps;
}, keyHolder);
// keyHolder.getKey() now contains the generated key
val INSERT_SQL = "insert into my_test (name) values(?)"
val name = "Rob"
val keyHolder = GeneratedKeyHolder()
jdbcTemplate.update({
it.prepareStatement (INSERT_SQL, arrayOf("id")).apply { setString(1, name) }
}, keyHolder)
// keyHolder.getKey() now contains the generated key