Spring Framework:使用JDBC进行数据访问

https://docs.spring.io/spring-framework/reference/data-access/jdbc.html

Spring框架JDBC抽象提供的价值或许最好通过下表中概述的操作序列来展示。该表显示了哪些操作由Spring负责,哪些操作是你的职责。
在这里插入图片描述
在这里插入图片描述

Spring框架负责处理所有低级细节,这些细节可能会使JDBC成为一个繁琐的API。

选择JDBC数据库访问的方法

你可以在几种方法中选择一种作为JDBC数据库访问的基础。除了三种不同风格的JdbcTemplate,还有SimpleJdbcInsertSimpleJdbcCall方法优化了数据库元数据,而RDBMS对象风格则导致了更面向对象的方法。一旦你开始使用这些方法之一,仍然可以混合搭配,以包含来自不同方法的特性。

  • JdbcTemplate是经典且最受欢迎的Spring JDBC方法。这种“最低级别”的方法和其他所有方法都在底层使用了JdbcTemplate
  • NamedParameterJdbcTemplate包装了JdbcTemplate,提供了命名参数而不是传统的JDBC ?占位符。当SQL语句有多个参数时,这种方法提供了更好的文档和易用性。
  • SimpleJdbcInsertSimpleJdbcCall优化了数据库元数据,以限制所需的配置量。这种方法简化了编码,使你只需提供表或过程的名称以及与列名匹配的参数映射。这只有在数据库提供足够的元数据时才有效。如果数据库不提供这些元数据,你必须提供参数的显式配置。
  • RDBMS对象——包括MappingSqlQuerySqlUpdateStoredProcedure——要求你在初始化数据访问层期间创建可重用且线程安全的对象。这种方法允许你定义查询字符串,声明参数,并编译查询。一旦完成这些操作,就可以多次调用execute(…​)update(…​)findObject(…​)方法,使用不同的参数值。

层次结构

Spring框架的JDBC抽象框架由四个不同的包组成:

  • coreorg.springframework.jdbc.core包包含了JdbcTemplate类及其各种回调接口,以及许多相关类。一个名为org.springframework.jdbc.core.simple的子包包含了SimpleJdbcInsertSimpleJdbcCall类。另一个名为org.springframework.jdbc.core.namedparam的子包包含了NamedParameterJdbcTemplate类及相关的支持类。
  • datasourceorg.springframework.jdbc.datasource包包含了一个便于访问DataSource的实用工具类,以及各种简单的DataSource实现,你可以在Jakarta EE容器之外用于测试和运行未经修改的JDBC代码。一个名为org.springframework.jdbc.datasource.embedded的子包提供了通过使用Java数据库引擎(如HSQL、H2和Derby)创建嵌入式数据库的支持。
  • objectorg.springframework.jdbc.object包包含了表示RDBMS查询、更新和存储过程的类,这些类是线程安全的、可重用的对象。这种风格导致了更面向对象的方法,尽管查询返回的对象自然与数据库断开连接。这种更高级别的JDBC抽象依赖于org.springframework.jdbc.core包中的低级别抽象。
  • supportorg.springframework.jdbc.support包提供了SQLException转换功能和一些实用工具类。在JDBC处理过程中抛出的异常被转换为org.springframework.dao包中定义的异常。这意味着使用Spring JDBC抽象层的代码不需要实现JDBC或RDBMS特定的错误处理。所有转换后的异常都是未检查的,这为你提供了捕获可以从中恢复的异常的选项,同时让其他异常传播给调用者。

使用JDBC核心类控制基本的JDBC处理和错误处理

使用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。

DataSource应该始终在Spring IoC容器中配置为一个bean。在第一种情况下,bean直接提供给服务;在第二种情况下,它提供给准备好的模板。

该类发出的所有SQL都在与模板实例的完全限定类名相对应的类别下(通常是JdbcTemplate,但如果使用JdbcTemplate类的自定义子类,则可能不同)以DEBUG级别进行记录。

以下部分提供了一些JdbcTemplate使用示例。这些示例并不是JdbcTemplate暴露的所有功能的详尽列表。

查询(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

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表达式中的重复并将它们提取到一个单独的字段中是有意义的,然后根据需要由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);
}

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

可以使用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操作

可以使用execute(..)方法运行任意SQL。因此,该方法通常用于DDL语句。它有大量重载的变体,接受回调接口、绑定变量数组等。以下示例创建一个表:

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

以下示例调用一个存储过程:

this.jdbcTemplate.update(
		"call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
		Long.valueOf(unionId));

JdbcTemplate 最佳实践

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

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

public class JdbcCorporateEventDao implements CorporateEventDao {

	private JdbcTemplate jdbcTemplate;

	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">

	<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>

	<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">

	<!-- Scans within the base package of the application for @Component classes to configure as beans -->
	<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类仅作为方便提供。

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

使用NamedParameterJdbcTemplate

NamedParameterJdbcTemplate类增加了使用命名参数编程JDBC语句的支持,而不是仅使用经典的占位符('?')参数来编程JDBC语句。NamedParameterJdbcTemplate类包装了一个JdbcTemplate,并将其大部分工作委托给被包装的JdbcTemplate。本节仅描述NamedParameterJdbcTemplate类与JdbcTemplate本身不同的领域——即使用命名参数编程JDBC语句。以下示例显示了如何使用NamedParameterJdbcTemplate

// 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);
}

注意在sql变量的值中使用了命名参数表示法,以及插入到namedParameters变量(类型为MapSqlParameterSource)中的相应值。

或者,可以使用基于Map的风格将命名参数及其对应的值传递给NamedParameterJdbcTemplate实例。NamedParameterJdbcOperations暴露的其余方法和由NamedParameterJdbcTemplate类实现的方法遵循类似的模式,此处未涵盖。

以下示例显示了基于Map的风格的使用:

// 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);
}

NamedParameterJdbcTemplate相关的一个很好的特性是SqlParameterSource接口(存在于相同的Java包中)。已经在之前的代码片段之一中看到了这个接口的一个实现示例(MapSqlParameterSource类)。SqlParameterSource是命名参数值的来源,用于NamedParameterJdbcTemplateMapSqlParameterSource类是一个简单实现,它是围绕java.util.Map的适配器,其中键是参数名,值是参数值。

另一个SqlParameterSource实现是BeanPropertySqlParameterSource类。这个类包装了一个任意的JavaBean(即,遵循JavaBean约定的类的实例),并使用被包装的JavaBean的属性作为命名参数值的来源。

以下示例显示了一个典型的JavaBean:

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...
}

以下示例使用NamedParameterJdbcTemplate返回前一个示例中所示类的成员数量:

// 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);
}

记住,NamedParameterJdbcTemplate类包装了一个经典的JdbcTemplate模板。如果需要访问被包装的JdbcTemplate实例以访问仅在JdbcTemplate类中存在的功能,可以使用getJdbcOperations()方法通过JdbcOperations接口访问被包装的JdbcTemplate

统一JDBC查询/更新操作:JdbcClient

自6.1版本起,NamedParameterJdbcTemplate的命名参数语句和常规JdbcTemplate的位置参数语句可以通过具有流畅交互模型的统一客户端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具有firstNamelastName属性作为记录类、自定义构造函数、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();

对于更新语句:

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();

可以指定一个参数源对象,而不是单独的命名参数——例如,具有bean属性的记录类或普通字段持有者,它提供firstNamelastName属性,就像上面的Actor类:

this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (:firstName, :lastName)")
		.paramSource(new Actor("Leonor", "Watling")
		.update();

上面提到的参数和查询结果的自动Actor类映射是通过隐式的SimplePropertySqlParameterSourceSimplePropertyRowMapper策略提供的,这些策略也可以直接使用。它们可以作为BeanPropertySqlParameterSourceBeanPropertyRowMapper/DataClassRowMapper的通用替代品,也适用于JdbcTemplateNamedParameterJdbcTemplate本身。

JdbcClient是一个灵活但简化的JDBC查询/更新语句的外观。高级功能,如批量插入和存储过程调用,通常需要额外的定制:对于JdbcClient中不可用的任何此类功能,请考虑使用Spring的SimpleJdbcInsertSimpleJdbcCall类或直接使用JdbcTemplate

使用SQLExceptionTranslator

SQLExceptionTranslator是一个接口,需要由能够在SQLExceptions和Spring自己的org.springframework.dao.DataAccessException之间进行转换的类实现,后者对数据访问策略不可知。实现可以是通用的(例如,使用JDBC的SQLState代码)或专有的(例如,使用Oracle错误代码)以获得更高的精确度。这种异常转换机制被用于常见的JdbcTemplateJdbcTransactionManager入口点之后,这些入口点不传播SQLException,而是传播DataAccessException

自6.0版本起,默认的异常转换器是SQLExceptionSubclassTranslator,它通过一些额外的检查来检测JDBC 4 SQLException子类,并在必要时通过SQLStateSQLExceptionTranslator回退到SQLState自省。这对于常见的数据库访问通常是足够的,不需要特定于供应商的检测。为了向后兼容,请考虑使用下面描述的SQLErrorCodeSQLExceptionTranslator,可能还需要自定义错误代码映射。

SQLErrorCodeSQLExceptionTranslatorSQLExceptionTranslator的实现,当类路径根目录下存在名为sql-error-codes.xml的文件时,默认使用这个实现。这个实现使用特定的供应商代码。它比SQLStateSQLException子类转换更精确。错误代码转换基于一个名为SQLErrorCodes的JavaBean类型类中保存的代码。这个类由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,如下例所示:

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;
	}
}

在上述示例中,特定的错误代码(-12345)被翻译,而其他错误则留给默认的翻译器实现来处理。要使用这个自定义翻译器,必须通过setExceptionTranslator方法将其传递给JdbcTemplate,并且必须在所有需要此翻译器的数据访问处理中使用这个JdbcTemplate。以下示例展示了如何使用这个自定义翻译器:

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);
}

定义翻译器传递了一个数据源,以便在sql-error-codes.xml中查找错误代码。

执行语句(Running Statements)

运行SQL语句所需的代码量非常少。你需要一个DataSource和一个JdbcTemplate,包括JdbcTemplate提供的便捷方法。下面的例子展示了如何编写一个最小但功能完备的类,用于创建一个新的表:

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))");
	}
}

运行查询(Running Queries)

某些查询方法返回单个值。要检索一行的计数或特定值,请使用queryForObject(..)。后者将返回的JDBC Type 转换为作为参数传递的Java类。如果类型转换无效,则会抛出InvalidDataAccessApiUsageException。以下示例包含两个查询方法,一个用于int,另一个查询String

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);
	}
}

除了返回单个结果的查询方法外,还有几个方法返回一个列表,其中每个条目对应查询返回的一行。最常用的方法是queryForList(..),它返回一个List,其中每个元素是一个Map,包含每列的一个条目,使用列名作为键。如果在前面的示例中添加一个方法来检索所有行的列表,可能如下所示:

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");
}

返回的列表将类似于以下内容:

[{name=Bob, id=1}, {name=Mary, id=2}]

更新数据库

以下示例以主键更新了某个列:

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);
	}
}

在上述示例中,SQL语句有行参数的占位符。可以将参数值作为可变参数传递,或者作为对象数组传递。因此,应该显式地将基本类型包装在基本类型的包装类中,或者使用自动装箱。

检索自动生成的键

update()便捷方法支持检索由数据库生成的主键。此支持是JDBC 3.0标准的一部分。该方法的第一个参数是一个PreparedStatementCreator,通过它来指定所需的插入语句。另一个参数是KeyHolder,在更新成功返回时包含生成的键。没有标准的统一方法创建适当的PreparedStatement(这就是为什么方法签名是这样的)。以下示例适用于Oracle,但可能不适用于其他平台:

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

控制数据库连接

使用DataSource

Spring 通过 DataSource 获取数据库连接。DataSource 是 JDBC 规范的一部分,是一个通用的连接工厂。它允许容器或框架将连接池和事务管理问题从应用程序代码中隐藏起来。作为开发者,不需要了解如何连接到数据库的细节。这是设置数据源的管理员的责任。在开发和测试代码时,很可能同时扮演这两个角色,但不一定需要知道生产数据源是如何配置的。

当使用Spring的JDBC层时,可以从JNDI获取数据源,或者可以使用第三方提供的连接池实现来配置自己的数据源。传统的选择是Apache Commons DBCP和C3P0,它们提供了bean风格的DataSource类;对于现代的JDBC连接池,可以考虑使用HikariCP,它提供了构建器风格的API。

应该仅将DriverManagerDataSourceSimpleDriverDataSource类(包含在Spring分发包中)用于测试目的!这些变体不提供连接池,并且在多个请求同时需要连接时性能较差。

以下部分使用了Spring的DriverManagerDataSource实现。稍后将介绍其他几种DataSource变体。

要配置DriverManagerDataSource

  • 使用DriverManagerDataSource获取连接,就像通常获取JDBC连接一样。
  • 指定JDBC驱动程序的完全限定类名,以便DriverManager可以加载驱动程序类。
  • 提供一个URL,该URL在不同的JDBC驱动程序之间有所不同。
  • 提供用户名和密码以连接到数据库。

以下示例展示了如何在Java中配置DriverManagerDataSource

DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername("sa");
dataSource.setPassword("");

以下示例展示了相应的XML配置:

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
	<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"/>

接下来的两个示例展示了DBCP和C3P0的基本连接和配置。

以下示例展示了DBCP配置:

<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"/>

下示例展示了C3P0配置:

<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
	<property name="driverClass" value="${jdbc.driverClassName}"/>
	<property name="jdbcUrl" value="${jdbc.url}"/>
	<property name="user" value="${jdbc.username}"/>
	<property name="password" value="${jdbc.password}"/>
</bean>

<context:property-placeholder location="jdbc.properties"/>

使用DataSourceUtils

DataSourceUtils类是一个方便而强大的辅助类,提供了从JNDI获取连接并在必要时关闭连接的静态方法。它支持使用DataSourceTransactionManager进行线程绑定的JDBC连接,但也支持JtaTransactionManagerJpaTransactionManager

请注意,JdbcTemplate 的使用意味着通过DataSourceUtils进行连接访问,它在每个JDBC操作背后隐含地参与了一个正在进行的事务。

实现SmartDataSource

SmartDataSource接口应由能够提供关系数据库连接的类实现。它扩展了DataSource接口,允许使用它的类查询在给定操作后是否应关闭连接。当你知道需要重用连接时,这种用法是高效的。

扩展AbstractDataSource

AbstractDataSource是Spring的DataSource实现的抽象基类。它实现了所有DataSource实现共有的代码。如果编写自己的DataSource实现,应该扩展AbstractDataSource类。

使用SingleConnectionDataSource

SingleConnectionDataSource类是SmartDataSource接口的一个实现,它包装了一个在每次使用后都不关闭的单个连接。这不支持多线程。

如果有客户端代码基于连接池的假设调用close (例如在使用持久性工具时),你应该将suppressClose属性设置为true。此设置返回一个包装物理连接的抑制关闭代理。请注意,你不能再将其转换为本地Oracle Connection或类似的对象。

SingleConnectionDataSource主要是一个测试类。它通常使得在应用服务器外部,结合简单的JNDI环境,轻松测试代码。与DriverManagerDataSource相比,它始终重用相同的连接,避免了过多创建物理连接。

使用DriverManagerDataSource

DriverManagerDataSource类是标准DataSource接口的一个实现,它通过bean属性配置普通的JDBC驱动程序,并在每次调用时返回一个新的Connection

这个实现对于Jakarta EE容器之外的测试和独立环境很有用,可以作为Spring IoC容器中的DataSource bean,或者与简单的JNDI环境结合使用。假设连接池的Connection.close()调用会关闭连接,因此任何感知到DataSource的持久性代码都应该正常工作。然而,即使在测试环境中,使用JavaBean风格的连接池(如commons-dbcp)也非常简单,几乎总是比使用DriverManagerDataSource更可取。

使用TransactionAwareDataSourceProxy

TransactionAwareDataSourceProxy是目标DataSource的代理。该代理包装目标DataSource以增加对Spring管理的事务的认知。在这方面,它类似于Jakarta EE服务器提供的事务性JNDI DataSource

除非在必须调用已有代码并传递标准JDBC DataSource接口实现的情况下,否则很少需要使用这个类。在这种情况下,仍然可以正常使用这段代码,同时让这段代码参与Spring管理的事务。通常更推荐编写自己的新代码时使用更高级别的资源管理抽象,如JdbcTemplateDataSourceUtils

使用DataSourceTransactionManager / JdbcTransactionManager

DataSourceTransactionManager类是针对单个JDBC DataSourcePlatformTransactionManager实现。它将指定DataSource的JDBC连接绑定到当前执行线程,可能允许每个DataSource有一个线程绑定的连接。

应用程序代码需要通过DataSourceUtils.getConnection(DataSource)检索JDBC连接,而不是使用Java EE的标准DataSource.getConnection。它抛出非检查型的org.springframework.dao异常,而不是检查型的SQLExceptions。所有框架类(如JdbcTemplate)都隐式使用这种策略。如果不与事务管理器一起使用,查找策略的行为与DataSource.getConnection完全相同,因此可以在任何情况下使用。

DataSourceTransactionManager类支持保存点(PROPAGATION_NESTED)、自定义隔离级别,以及适当应用的JDBC语句查询超时时间。为了支持后者,应用程序代码必须使用JdbcTemplate或者为每个创建的语句调用DataSourceUtils.applyTransactionTimeout(..)方法。

在单资源情况下,可以使用DataSourceTransactionManager代替JtaTransactionManager,因为它不需要容器支持JTA事务协调器。只要你坚持使用所需的连接查找模式,切换这些事务管理器只是配置问题。请注意,JTA不支持保存点或自定义隔离级别,并且具有不同的超时机制,但在JDBC资源和JDBC提交/回滚管理方面表现出类似的行为。

对于JTA风格的实际资源连接的延迟检索,Spring为目标连接池提供了相应的DataSource代理类:LazyConnectionDataSourceProxy。这对于没有实际语句执行的潜在空事务特别有用(在这种情况下从不获取实际资源),也适用于路由DataSource前面,意味着要考虑事务同步的只读标志和/或隔离级别(例如IsolationLevelDataSourceRouter)。

LazyConnectionDataSourceProxy还为只读事务期间使用的只读连接池提供特殊支持,避免了在从主连接池获取连接时,在每次事务开始和结束时切换JDBC连接的只读标志的开销(这取决于JDBC驱动程序,可能会很昂贵)。

从5.3版本开始,Spring提供了一个扩展的JdbcTransactionManager变体,它在提交/回滚时增加了异常转换功能(与JdbcTemplate保持一致)。DataSourceTransactionManager只会抛出TransactionSystemException(类似于JTA),而JdbcTransactionManager会将数据库锁定失败等转换为相应的DataAccessException子类。请注意,应用程序代码需要为此类异常做好准备,而不是仅预期TransactionSystemException。在那种情况下,JdbcTransactionManager是推荐的选择。

在异常行为方面,JdbcTransactionManager大致相当于JpaTransactionManagerR2dbcTransactionManager,作为彼此的直接配套/替代品。另一方面,DataSourceTransactionManager相当于JtaTransactionManager,可以直接替代它。

JDBC批处理操作

大多数JDBC驱动程序在你对同一个预编译语句进行多次调用批处理时,会提供改进的性能。通过将更新分组为批次,你可以减少往返数据库的次数。

使用JdbcTemplate进行基本的批处理操作

你可以通过实现一个特殊接口BatchPreparedStatementSetter的两个方法来完成JdbcTemplate的批处理,并将该实现作为第二个参数传递给batchUpdate方法调用。你可以使用getBatchSize方法来提供当前批次的大小。你可以使用setValues方法来设置预编译语句的参数值。这个方法会被调用你在getBatchSize调用中指定的次数。下面的例子基于列表中的条目更新t_actor表,整个列表被用作批处理:

public class JdbcActorDao implements ActorDao {

	private JdbcTemplate jdbcTemplate;

	public void setDataSource(DataSource dataSource) {
		this.jdbcTemplate = new JdbcTemplate(dataSource);
	}

	public int[] batchUpdate(final List<Actor> actors) {
		return this.jdbcTemplate.batchUpdate(
				"update t_actor set first_name = ?, last_name = ? where id = ?",
				new BatchPreparedStatementSetter() {
					public void setValues(PreparedStatement ps, int i) throws SQLException {
						Actor actor = actors.get(i);
						ps.setString(1, actor.getFirstName());
						ps.setString(2, actor.getLastName());
						ps.setLong(3, actor.getId().longValue());
					}
					public int getBatchSize() {
						return actors.size();
					}
				});
	}

	// ... additional methods
}

如果你处理一系列更新或从文件中读取,你可能有一个首选的批处理大小,但最后一批可能没有那么多条目。在这种情况下,你可以使用InterruptibleBatchPreparedStatementSetter接口,它允许你在输入源耗尽时中断批处理。isBatchExhausted方法让你可以发出批处理结束的信号。

使用对象列表进行批处理操作

JdbcTemplateNamedParameterJdbcTemplate都提供了一种替代的批更新方式。你不需要实现特殊的批处理接口,而是在调用中将所有参数值作为一个列表提供。框架会遍历这些值并使用内部预编译语句设置器。API根据你是否使用命名参数而有所不同。对于命名参数,你需要提供一个SqlParameterSource数组,每个批次的成员对应一个条目。你可以使用SqlParameterSourceUtils.createBatch方便方法来创建这个数组,传入一个bean风格对象的数组(带有与参数对应的getter方法)、String键的Map实例(包含相应参数作为值),或者两者的混合。

以下示例展示了使用命名参数进行批更新:

public class JdbcActorDao implements ActorDao {

	private NamedParameterTemplate namedParameterJdbcTemplate;

	public void setDataSource(DataSource dataSource) {
		this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
	}

	public int[] batchUpdate(List<Actor> actors) {
		return this.namedParameterJdbcTemplate.batchUpdate(
				"update t_actor set first_name = :firstName, last_name = :lastName where id = :id",
				SqlParameterSourceUtils.createBatch(actors));
	}

	// ... additional methods
}

对于使用经典 ? 占位符的SQL语句,你需要传入一个包含对象数组的列表,该对象数组必须为SQL语句中的每个占位符提供一个条目,并且它们的顺序必须与SQL语句中定义的顺序相同。

以下示例与前一个示例相同,除了它使用传统的JDBC ? 占位符:

public class JdbcActorDao implements ActorDao {

	private JdbcTemplate jdbcTemplate;

	public void setDataSource(DataSource dataSource) {
		this.jdbcTemplate = new JdbcTemplate(dataSource);
	}

	public int[] batchUpdate(final List<Actor> actors) {
		List<Object[]> batch = new ArrayList<>();
		for (Actor actor : actors) {
			Object[] values = new Object[] {
					actor.getFirstName(), actor.getLastName(), actor.getId()};
			batch.add(values);
		}
		return this.jdbcTemplate.batchUpdate(
				"update t_actor set first_name = ?, last_name = ? where id = ?",
				batch);
	}

	// ... additional methods
}

我们之前描述的所有批更新方法都返回一个int数组,包含每个批处理条目影响的行数。这个计数是由JDBC驱动程序报告的。如果计数不可用,JDBC驱动程序将返回-2

在这种情况下,自动设置底层PreparedStatement上的值时,需要从给定的Java类型派生出每个值的相应JDBC类型。虽然这通常运作良好,但可能会出现问题(例如,包含null值的Map)。Spring默认情况下会调用ParameterMetaData.getParameterType,这在你的JDBC驱动程序中可能会很昂贵。你应该使用最新版本的驱动程序,并且如果你遇到性能问题(如在Oracle 12c、JBoss和PostgreSQL上报告的),考虑将spring.jdbc.getParameterType.ignore属性设置为true(作为JVM系统属性或通过SpringProperties机制)。

或者,你可以考虑显式指定相应的JDBC类型,可以通过BatchPreparedStatementSetter(如前所示),通过基于List<Object[]>调用的显式类型数组,通过在自定义MapSqlParameterSource实例上调用registerSqlType,或者通过BeanPropertySqlParameterSource,即使对于null值也能从Java声明的属性类型派生出SQL类型。

使用多个批次进行批处理操作

前面的批更新示例处理的批次太大,以至于你希望将它们拆分为几个较小的批次。你可以使用前面提到的方法通过多次调用batchUpdate方法来实现这一点,但现在有一个更方便的方法。除了SQL语句外,这个方法还接受一个包含参数的对象集合、每个批次要进行的更新次数,以及一个ParameterizedPreparedStatementSetter来设置预编译语句的参数值。框架会遍历提供的值,并根据指定的大小将更新调用分成批次。

以下示例展示了一个使用批次大小为100的批更新:

public class JdbcActorDao implements ActorDao {

	private JdbcTemplate jdbcTemplate;

	public void setDataSource(DataSource dataSource) {
		this.jdbcTemplate = new JdbcTemplate(dataSource);
	}

	public int[][] batchUpdate(final Collection<Actor> actors) {
		int[][] updateCounts = jdbcTemplate.batchUpdate(
				"update t_actor set first_name = ?, last_name = ? where id = ?",
				actors,
				100,
				(PreparedStatement ps, Actor actor) -> {
					ps.setString(1, actor.getFirstName());
					ps.setString(2, actor.getLastName());
					ps.setLong(3, actor.getId().longValue());
				});
		return updateCounts;
	}

	// ... additional methods
}

这个批量更新方法的调用返回一个int数组的数组,该数组为每个批处理包含一个数组条目,每个更新操作包含受影响的行数。顶级数组的长度表示运行的批处理数量,而第二级数组的长度表示该批处理中的更新数量。每个批处理中的更新数量应该是为所有批处理提供的批量大小(除了最后一个批处理可能更少),这取决于提供的更新对象的总数。每个更新语句的更新计数是由JDBC驱动程序报告的。如果计数不可用,JDBC驱动程序将返回-2的值。

使用SimpleJdbc类简化JDBC操作

SimpleJdbcInsertSimpleJdbcCall类通过利用可以通过JDBC驱动程序获取的数据库元数据来提供简化的配置。这意味着你在前期需要配置的内容较少,尽管如果你更喜欢在代码中提供所有详细信息,也可以覆盖或关闭元数据处理。

使用SimpleJdbcInsert插入数据

我们从使用最少配置选项的SimpleJdbcInsert类开始。你应在数据访问层的初始化方法中实例化SimpleJdbcInsert。在这个例子中,初始化方法是setDataSource方法。你不需要继承SimpleJdbcInsert类。相反,你可以创建一个新实例,并使用withTableName方法设置表名。这个类的配置方法遵循fluid 的接口风格,即每个方法都会返回SimpleJdbcInsert的实例,从而允许你链式调用所有配置方法。下面的例子只使用了一个配置方法:

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcInsert insertActor;

	public void setDataSource(DataSource dataSource) {
		this.insertActor = new SimpleJdbcInsert(dataSource).withTableName("t_actor");
	}

	public void add(Actor actor) {
		Map<String, Object> parameters = new HashMap<>(3);
		parameters.put("id", actor.getId());
		parameters.put("first_name", actor.getFirstName());
		parameters.put("last_name", actor.getLastName());
		insertActor.execute(parameters);
	}

	// ... additional methods
}

这里使用的execute方法只接受一个普通的java.util.Map作为参数。这里需要注意的重要一点是,用于Map的键必须与数据库中定义的表列名相匹配。这是因为我们读取元数据来构建实际的插入语句。

使用SimpleJdbcInsert检索自动生成的键

下一个示例使用了与前面示例相同的插入操作,但是,它并没有传递id,而是检索自动生成的键并将其设置在新的Actor对象上。在创建SimpleJdbcInsert时,除了指定表名之外,它还使用usingGeneratedKeyColumns方法指定了生成的键列的名称。下面的列表展示了它是如何工作的:

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcInsert insertActor;

	public void setDataSource(DataSource dataSource) {
		this.insertActor = new SimpleJdbcInsert(dataSource)
				.withTableName("t_actor")
				.usingGeneratedKeyColumns("id");
	}

	public void add(Actor actor) {
		Map<String, Object> parameters = new HashMap<>(2);
		parameters.put("first_name", actor.getFirstName());
		parameters.put("last_name", actor.getLastName());
		Number newId = insertActor.executeAndReturnKey(parameters);
		actor.setId(newId.longValue());
	}

	// ... additional methods
}

使用第二种方法执行插入操作时的主要区别在于,你不需要将id添加到Map中,而是调用executeAndReturnKey方法。这会返回一个java.lang.Number对象,你可以用它来创建你在域类中使用的数值类型的实例。你不能依赖所有数据库在这里返回特定的Java类。java.lang.Number是你可以依赖的基类。如果你有多个自动生成的列或者生成的值不是数字类型,你可以使用executeAndReturnKeyHolder方法返回的KeyHolder

为SimpleJdbcInsert指定列

你可以通过usingColumns方法指定一个列名列表来限制插入操作的列,如下例所示:

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcInsert insertActor;

	public void setDataSource(DataSource dataSource) {
		this.insertActor = new SimpleJdbcInsert(dataSource)
				.withTableName("t_actor")
				.usingColumns("first_name", "last_name")
				.usingGeneratedKeyColumns("id");
	}

	public void add(Actor actor) {
		Map<String, Object> parameters = new HashMap<>(2);
		parameters.put("first_name", actor.getFirstName());
		parameters.put("last_name", actor.getLastName());
		Number newId = insertActor.executeAndReturnKey(parameters);
		actor.setId(newId.longValue());
	}

	// ... additional methods
}

插入操作的执行与依赖元数据来确定使用哪些列的方式相同。

使用SqlParameterSource提供参数值

使用Map来提供参数值是可以工作的,但它并不是最方便的类来使用。Spring提供了SqlParameterSource接口的几种实现,你可以使用它们来代替Map。第一个实现是BeanPropertySqlParameterSource,如果你有一个符合JavaBean规范的类,其中包含你的值,那么它是一个非常方便的类。它使用相应的getter方法来提取参数值。以下示例展示了如何使用BeanPropertySqlParameterSource

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcInsert insertActor;

	public void setDataSource(DataSource dataSource) {
		this.insertActor = new SimpleJdbcInsert(dataSource)
				.withTableName("t_actor")
				.usingGeneratedKeyColumns("id");
	}

	public void add(Actor actor) {
		SqlParameterSource parameters = new BeanPropertySqlParameterSource(actor);
		Number newId = insertActor.executeAndReturnKey(parameters);
		actor.setId(newId.longValue());
	}

	// ... additional methods
}

另一个选项是MapSqlParameterSource,它类似于一个Map,但提供了更方便的addValue方法,可以链式调用。以下示例展示了如何使用它:

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcInsert insertActor;

	public void setDataSource(DataSource dataSource) {
		this.insertActor = new SimpleJdbcInsert(dataSource)
				.withTableName("t_actor")
				.usingGeneratedKeyColumns("id");
	}

	public void add(Actor actor) {
		SqlParameterSource parameters = new MapSqlParameterSource()
				.addValue("first_name", actor.getFirstName())
				.addValue("last_name", actor.getLastName());
		Number newId = insertActor.executeAndReturnKey(parameters);
		actor.setId(newId.longValue());
	}

	// ... additional methods
}

正如你所看到的,配置是相同的。只需要改变执行代码来使用这些替代的输入类。

使用SimpleJdbcCall调用存储过程

SimpleJdbcCall 类使用数据库中的元数据来查找输入和输出参数的名称,这样你就不必显式地声明它们了。如果你愿意这样做,或者你的参数(比如 ARRAYSTRUCT 类型)没有自动映射到 Java 类,你可以声明参数。第一个示例展示了一个简单的存储过程,它仅从 MySQL 数据库中返回 VARCHARDATE 格式的标量值。这个示例过程读取一个指定的演员条目,并以输出参数的形式返回 first_namelast_namebirth_date 列。以下是第一个示例的列表:

CREATE PROCEDURE read_actor (
	IN in_id INTEGER,
	OUT out_first_name VARCHAR(100),
	OUT out_last_name VARCHAR(100),
	OUT out_birth_date DATE)
BEGIN
	SELECT first_name, last_name, birth_date
	INTO out_first_name, out_last_name, out_birth_date
	FROM t_actor where id = in_id;
END;

in_id 参数包含了你正在查找的演员的id out 参数返回从表中读取的数据。

你可以以类似于声明 SimpleJdbcInsert 的方式来声明 SimpleJdbcCall。你应该在数据访问层的初始化方法中实例化和配置这个类。与 StoredProcedure 类相比,你不需要创建一个子类,也不需要声明那些可以在数据库元数据中查找到的参数。以下是一个 SimpleJdbcCall 配置的示例,它使用了前面提到的存储过程(除了数据源之外,唯一的配置选项是存储过程的名称):

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcCall procReadActor;

	public void setDataSource(DataSource dataSource) {
		this.procReadActor = new SimpleJdbcCall(dataSource)
				.withProcedureName("read_actor");
	}

	public Actor readActor(Long id) {
		SqlParameterSource in = new MapSqlParameterSource()
				.addValue("in_id", id);
		Map out = procReadActor.execute(in);
		Actor actor = new Actor();
		actor.setId(id);
		actor.setFirstName((String) out.get("out_first_name"));
		actor.setLastName((String) out.get("out_last_name"));
		actor.setBirthDate((Date) out.get("out_birth_date"));
		return actor;
	}

	// ... additional methods
}

你写的执行调用的代码涉及创建一个包含输入参数的 SqlParameterSource。你必须确保为输入值提供的名称与存储过程中声明的参数名称相匹配。这里不需要考虑大小写匹配,因为你会使用元数据来确定在存储过程中应如何引用数据库对象。在存储过程源代码中指定的名称并不一定就是它在数据库中存储的方式。一些数据库会将名称转换为全部大写,而另一些数据库则使用小写,或者使用指定的大小写。

execute 方法接收输入参数(IN 参数),并返回一个包含所有输出参数的 Map,这些输出参数以在存储过程中指定的名称作为键。在这种情况下,输出参数的名称是 out_first_nameout_last_nameout_birth_date

execute 方法的最后一部分创建了一个 Actor 实例,用于返回检索到的数据。重要的是要使用在存储过程中声明的输出参数名称。此外,存储在结果映射中的输出参数名称的大小写需要与数据库中输出参数名称的大小写相匹配,这在不同数据库之间可能有所不同。为了使你的代码更具可移植性,你应该进行不区分大小写的查找,或者指示 Spring 使用 LinkedCaseInsensitiveMap。要执行后者,你可以创建自己的 JdbcTemplate 实例,并将 setResultsMapCaseInsensitive 属性设置为 true。然后,你可以将这个自定义的 JdbcTemplate 实例传递给 SimpleJdbcCall 的构造函数。以下示例展示了这种配置:

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcCall procReadActor;

	public void setDataSource(DataSource dataSource) {
		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
		jdbcTemplate.setResultsMapCaseInsensitive(true);
		this.procReadActor = new SimpleJdbcCall(jdbcTemplate)
				.withProcedureName("read_actor");
	}

	// ... additional methods
}

通过采取这一行动,你可以避免返回的out 参数名称在大小写方面产生的冲突。

明确声明用于 SimpleJdbcCall 的参数

在本章的前面部分,我们描述了如何根据元数据推断参数,但如果你愿意的话,也可以显式地声明它们。你可以通过创建和配置 SimpleJdbcCall 并使用 declareParameters 方法来实现这一点,该方法接受可变数量的 SqlParameter 对象作为输入。

如果你使用的数据库不是 Spring 支持的数据库,那么就需要显式声明参数。目前,Spring 支持以下数据库的存储过程调用的元数据查找:Apache Derby、DB2、MySQL、Microsoft SQL Server、Oracle 和 Sybase。此外,我们还支持 MySQL、Microsoft SQL Server 和 Oracle 的存储函数的元数据查找。

你可以选择显式声明一个、一些或全部参数。在没有显式声明参数的地方,仍然会使用参数元数据。如果你想绕过所有潜在参数的元数据查找处理,并且只使用声明的参数,你可以在声明时调用 withoutProcedureColumnMetaDataAccess 方法。假设你为数据库函数声明了两个或更多不同的调用签名。在这种情况下,你会调用 useInParameterNames 来指定要包含在给定签名中的输入参数名称列表。

以下示例展示了一个完全声明的过程调用,并使用了前面示例中的信息:

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcCall procReadActor;

	public void setDataSource(DataSource dataSource) {
		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
		jdbcTemplate.setResultsMapCaseInsensitive(true);
		this.procReadActor = new SimpleJdbcCall(jdbcTemplate)
				.withProcedureName("read_actor")
				.withoutProcedureColumnMetaDataAccess()
				.useInParameterNames("in_id")
				.declareParameters(
						new SqlParameter("in_id", Types.NUMERIC),
						new SqlOutParameter("out_first_name", Types.VARCHAR),
						new SqlOutParameter("out_last_name", Types.VARCHAR),
						new SqlOutParameter("out_birth_date", Types.DATE)
				);
	}

	// ... additional methods
}

这两个示例的执行和最终结果是相同的。第二个示例明确指定了所有细节,而不是依赖于元数据。

如何定义 SqlParameters

为了给 SimpleJdbc 类以及 RDBMS 操作类定义参数,你可以使用 SqlParameter 或其子类之一。要做到这一点,你通常需要在构造函数中指定参数名和 SQL 类型。SQL 类型是通过使用 java.sql.Types 中的常量来指定的。在本章的前面部分,我们看到了类似于以下的声明:

new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),

第一行使用 SqlParameter 声明了一个输入参数(IN 参数)。你可以在使用 SqlQuery 及其子类时,为存储过程调用和查询都使用输入参数。

第二行(使用 SqlOutParameter)声明了一个out 参数,用于存储过程调用。对于InOut 参数(即向过程提供输入值并返回值的参数),还有 SqlInOutParameter

只有声明为 SqlParameterSqlInOutParameter 的参数被用来提供输入值。这与 StoredProcedure 类有所不同,StoredProcedure 类(出于向后兼容的原因)允许为声明为 SqlOutParameter 的参数提供输入值。

对于输入参数(IN 参数),除了参数名和 SQL 类型之外,你还可以为数值数据指定一个小数位数,或为自定义数据库类型指定一个类型名。对于输出参数(out 参数),你可以提供一个 RowMapper 来处理从 REF 游标返回的行的映射。另一种选择是指定一个 SqlReturnType,它提供了一个机会来定义返回值的自定义处理。

使用 SimpleJdbcCall 调用存储函数(Stored Function)

你可以几乎以与调用存储过程相同的方式调用存储函数,只是你需要提供一个函数名而不是过程名。你使用 withFunctionName 方法作为配置的一部分来指示你想要调用一个函数,并生成相应的函数调用字符串。一个专门的调用方法(executeFunction)用于运行函数,并返回指定类型的对象作为函数的返回值,这意味着你不需要从结果映射中检索返回值。对于只有一个输出参数的存储过程,也有一个类似的便捷方法(名为 executeObject)。以下示例(针对 MySQL)基于一个名为 get_actor_name 的存储函数,该函数返回演员的全名:

CREATE FUNCTION get_actor_name (in_id INTEGER)
RETURNS VARCHAR(200) READS SQL DATA
BEGIN
	DECLARE out_name VARCHAR(200);
	SELECT concat(first_name, ' ', last_name)
		INTO out_name
		FROM t_actor where id = in_id;
	RETURN out_name;
END;

为了调用这个函数,我们再次在初始化方法中创建一个 SimpleJdbcCall 实例,如下例所示:

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcCall funcGetActorName;

	public void setDataSource(DataSource dataSource) {
		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
		jdbcTemplate.setResultsMapCaseInsensitive(true);
		this.funcGetActorName = new SimpleJdbcCall(jdbcTemplate)
				.withFunctionName("get_actor_name");
	}

	public String getActorName(Long id) {
		SqlParameterSource in = new MapSqlParameterSource()
				.addValue("in_id", id);
		String name = funcGetActorName.executeFunction(String.class, in);
		return name;
	}

	// ... additional methods
}

executeFunction 方法用于返回一个字符串,该字符串包含函数调用的返回值。

使用 SimpleJdbcCall 返回 ResultSet 或 REF CURSOR

调用返回结果集的存储过程或函数有点复杂。一些数据库在JDBC结果处理过程中返回结果集,而其他数据库则需要显式注册特定类型的out 参数。这两种方法都需要额外的处理来遍历结果集和处理返回的行。使用SimpleJdbcCall时,你可以使用returningResultSet方法,并声明一个RowMapper实现来用于特定参数。如果结果集是在结果处理过程中返回的,那么就没有定义名称,因此返回的结果必须与你声明RowMapper实现的顺序相匹配。指定的名称仍然用于将处理过的结果列表存储在从execute语句返回的结果映射中。

下一个示例(针对MySQL)使用了一个存储过程,该存储过程不接受任何 IN 参数,而是返回t_actor表中的所有行。

CREATE PROCEDURE read_all_actors()
BEGIN
 SELECT a.id, a.first_name, a.last_name, a.birth_date FROM t_actor a;
END;

要调用这个存储过程,你可以声明一个RowMapper。由于你想要映射到的类遵循JavaBean规则,你可以使用BeanPropertyRowMapper,它是通过在newInstance方法中传入要映射到的目标类来创建的。下面的示例展示了如何做到这一点:

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcCall procReadAllActors;

	public void setDataSource(DataSource dataSource) {
		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
		jdbcTemplate.setResultsMapCaseInsensitive(true);
		this.procReadAllActors = new SimpleJdbcCall(jdbcTemplate)
				.withProcedureName("read_all_actors")
				.returningResultSet("actors",
				BeanPropertyRowMapper.newInstance(Actor.class));
	}

	public List getActorsList() {
		Map m = procReadAllActors.execute(new HashMap<String, Object>(0));
		return (List) m.get("actors");
	}

	// ... additional methods
}

执行execute 时传入了一个空的Map,因为这个调用不需要任何参数。然后从结果Map中获取演员列表,并将其返回给调用者。

将JDBC操作建模为Java对象

org.springframework.jdbc.object 包包含了一些类,这些类允许你以更面向对象的方式访问数据库。例如,你可以运行查询并将结果作为包含业务对象的列表返回,这些业务对象的属性映射了关系列的数据。你还可以运行存储过程,以及执行更新、删除和插入语句。

许多Spring开发者认为,下面描述的各种关系型数据库管理系统(RDBMS)操作类(除了StoredProcedure类之外)通常可以被直接的JdbcTemplate调用所替代。通常,编写一个DAO方法直接调用JdbcTemplate上的方法(而不是将查询封装成一个完整的类)会更为简单。

理解SqlQuery

SqlQuery 是一个可重用且线程安全的类,它封装了一个 SQL 查询。子类必须实现 newRowMapper(..) 方法,以提供一个 RowMapper 实例,该实例能够从执行查询时创建的 ResultSet 中迭代每一行并创建一个对象。SqlQuery 类很少直接使用,因为 MappingSqlQuery 子类为将行映射到 Java 类提供了更为便捷的实现。扩展 SqlQuery 的其他实现包括 MappingSqlQueryWithParametersUpdatableSqlQuery

使用MappingSqlQuery

MappingSqlQuery 是一个可重用的查询类,其中具体的子类必须实现抽象的 mapRow(..) 方法,以将提供的 ResultSet 中的每一行转换为指定类型的对象。以下示例展示了一个自定义查询,该查询将 t_actor 关系中的数据映射到 Actor 类的实例:

public class ActorMappingQuery extends MappingSqlQuery<Actor> {

	public ActorMappingQuery(DataSource ds) {
		super(ds, "select id, first_name, last_name from t_actor where id = ?");
		declareParameter(new SqlParameter("id", Types.INTEGER));
		compile();
	}

	@Override
	protected Actor mapRow(ResultSet rs, int rowNumber) throws SQLException {
		Actor actor = new Actor();
		actor.setId(rs.getLong("id"));
		actor.setFirstName(rs.getString("first_name"));
		actor.setLastName(rs.getString("last_name"));
		return actor;
	}
}

该类继承了以Actor类型参数化的MappingSqlQuery。这个自定义查询的构造函数只接受一个DataSource作为参数。在构造函数中,你可以调用父类的构造函数,并传入DataSource以及用于检索该查询行的SQL语句。这条SQL语句用于创建一个PreparedStatement,因此它可能包含占位符,用于在执行时传入任何参数。

你必须使用declareParameter方法声明每个参数,并传入一个SqlParameter对象。SqlParameter接受一个名称和JDBC类型,这些类型在java.sql.Types中定义。定义完所有参数后,你可以调用compile()方法,以便准备语句并在稍后执行。编译后,该类是线程安全的,因此,只要这些实例在DAO初始化时创建,它们就可以作为实例变量保留并重复使用。

以下示例展示了如何定义这样的类:

private ActorMappingQuery actorMappingQuery;

@Autowired
public void setDataSource(DataSource dataSource) {
	this.actorMappingQuery = new ActorMappingQuery(dataSource);
}

public Customer getCustomer(Long id) {
	return actorMappingQuery.findObject(id);
}

前面示例中的方法通过传入唯一的参数(即id)来检索具有该id的客户。由于我们只想返回一个对象,因此我们调用带有id作为参数的findObject便捷方法。如果我们有一个查询返回对象列表并且需要额外的参数,我们会使用那些接受以可变参数(varargs)形式传入的参数值数组的execute方法之一。以下示例展示了这样的方法:

public List<Actor> searchForActors(int age, String namePattern) {
	return actorSearchMappingQuery.execute(age, namePattern);
}

使用SqlUpdate

SqlUpdate 类封装了一个SQL更新操作。与查询一样,更新对象是可重用的,并且与所有RdbmsOperation类一样,更新操作可以包含参数,并且是在SQL中定义的。这个类提供了许多与查询对象的execute(..)方法类似的update(..)方法。SqlUpdate 类是一个具体的类。它可以被子类化,例如,为了添加一个自定义的更新方法。然而,你不需要子类化SqlUpdate类,因为它可以通过设置SQL和声明参数来轻松地进行参数化。以下示例创建了一个名为execute的自定义更新方法:

import java.sql.Types;
import javax.sql.DataSource;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.SqlUpdate;

public class UpdateCreditRating extends SqlUpdate {

	public UpdateCreditRating(DataSource ds) {
		setDataSource(ds);
		setSql("update customer set credit_rating = ? where id = ?");
		declareParameter(new SqlParameter("creditRating", Types.NUMERIC));
		declareParameter(new SqlParameter("id", Types.NUMERIC));
		compile();
	}

	/**
	 * @param id for the Customer to be updated
	 * @param rating the new value for credit rating
	 * @return number of rows updated
	 */
	public int execute(int id, int rating) {
		return update(rating, id);
	}
}

使用StoredProcedure

StoredProcedure 类是RDBMS存储过程对象抽象的一个抽象超类。

继承的 sql 属性是RDBMS中存储过程的名称。

要为StoredProcedure类定义一个参数,你可以使用SqlParameter或其子类。在构造函数中,你必须指定参数名称和SQL类型,如下面的代码片段所示:

new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),

SQL类型是使用java.sql.Types常量来指定的。

第一行(包含SqlParameter的)声明了一个输入(IN)参数。你可以在调用存储过程以及使用SqlQuery及其子类进行查询时都使用输入参数。

第二行(包含SqlOutParameter的)声明了一个out 参数,用于存储过程调用。此外,还有一个SqlInOutParameter用于InOut参数(这些参数既向存储过程提供输入值,也返回一个值)。

对于in 参数,除了名称和SQL类型之外,你还可以为数值数据指定一个小数位数(scale),或者为自定义数据库类型指定一个类型名称。对于out 参数,你可以提供一个RowMapper来处理从REF游标返回的行的映射。另一个选项是指定一个SqlReturnType,它允许你定义对返回值的自定义处理。

下一个简单的数据访问对象(DAO)示例使用StoredProcedure来调用一个函数(sysdate()),这个函数是任何Oracle数据库都自带的。要使用存储过程功能,你需要创建一个继承自StoredProcedure的类。在这个例子中,StoredProcedure类是一个内部类。但是,如果你需要重用StoredProcedure,你可以将其声明为顶级类。这个示例没有输入参数,但是使用SqlOutParameter类声明了一个输出参数,类型为日期。execute()方法运行存储过程,并从结果映射(Map)中提取返回的日期。结果映射对于每个声明的输出参数(在这种情况下只有一个)都有一个条目,使用参数名称作为键。以下列表展示了我们的自定义StoredProcedure类:

import java.sql.Types;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;

public class StoredProcedureDao {

	private GetSysdateProcedure getSysdate;

	@Autowired
	public void init(DataSource dataSource) {
		this.getSysdate = new GetSysdateProcedure(dataSource);
	}

	public Date getSysdate() {
		return getSysdate.execute();
	}

	private class GetSysdateProcedure extends StoredProcedure {

		private static final String SQL = "sysdate";

		public GetSysdateProcedure(DataSource dataSource) {
			setDataSource(dataSource);
			setFunction(true);
			setSql(SQL);
			declareParameter(new SqlOutParameter("date", Types.DATE));
			compile();
		}

		public Date execute() {
			// the 'sysdate' sproc has no input parameters, so an empty Map is supplied...
			Map<String, Object> results = execute(new HashMap<String, Object>());
			Date sysdate = (Date) results.get("date");
			return sysdate;
		}
	}

}

下面的StoredProcedure示例中有两个输出参数(在本例中,是Oracle的REF游标):

import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import oracle.jdbc.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;

public class TitlesAndGenresStoredProcedure extends StoredProcedure {

	private static final String SPROC_NAME = "AllTitlesAndGenres";

	public TitlesAndGenresStoredProcedure(DataSource dataSource) {
		super(dataSource, SPROC_NAME);
		declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
		declareParameter(new SqlOutParameter("genres", OracleTypes.CURSOR, new GenreMapper()));
		compile();
	}

	public Map<String, Object> execute() {
		// again, this sproc has no input parameters, so an empty Map is supplied
		return super.execute(new HashMap<String, Object>());
	}
}

请注意,在TitlesAndGenresStoredProcedure构造函数中使用的declareParameter(..)方法的重载变体是如何传递RowMapper实现实例的。这是一种非常方便且强大的方式来重用现有功能。接下来的两个示例提供了这两个RowMapper实现的代码。

TitleMapper类将ResultSet中的每一行映射为一个Title领域对象,具体实现如下:

import java.sql.ResultSet;
import java.sql.SQLException;
import com.foo.domain.Title;
import org.springframework.jdbc.core.RowMapper;

public final class TitleMapper implements RowMapper<Title> {

	public Title mapRow(ResultSet rs, int rowNum) throws SQLException {
		Title title = new Title();
		title.setId(rs.getLong("id"));
		title.setName(rs.getString("name"));
		return title;
	}
}

GenreMapper类将ResultSet中的每一行映射为一个Genre领域对象,具体实现如下:

import java.sql.ResultSet;
import java.sql.SQLException;
import com.foo.domain.Genre;
import org.springframework.jdbc.core.RowMapper;

public final class GenreMapper implements RowMapper<Genre> {

	public Genre mapRow(ResultSet rs, int rowNum) throws SQLException {
		return new Genre(rs.getString("name"));
	}
}

要向在RDBMS中定义了一个或多个输入参数的存储过程传递参数,你可以编写一个强类型化的execute(..)方法,该方法将委托给超类中的无类型execute(Map)方法,如下例所示:

import java.sql.Types;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import oracle.jdbc.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.StoredProcedure;

public class TitlesAfterDateStoredProcedure extends StoredProcedure {

	private static final String SPROC_NAME = "TitlesAfterDate";
	private static final String CUTOFF_DATE_PARAM = "cutoffDate";

	public TitlesAfterDateStoredProcedure(DataSource dataSource) {
		super(dataSource, SPROC_NAME);
		declareParameter(new SqlParameter(CUTOFF_DATE_PARAM, Types.DATE);
		declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
		compile();
	}

	public Map<String, Object> execute(Date cutoffDate) {
		Map<String, Object> inputs = new HashMap<String, Object>();
		inputs.put(CUTOFF_DATE_PARAM, cutoffDate);
		return super.execute(inputs);
	}
}

参数和数据值处理中常见的问题

为参数提供SQL类型信息

通常,Spring会根据传入参数的类型来确定SQL参数的类型。但在设置参数值时,也可以显式地提供要使用的SQL类型。这在正确设置NULL值时有时是必需的。

你可以通过以下几种方式提供SQL类型信息:

  • JdbcTemplate的许多更新和查询方法都会接受一个额外的参数,这个参数以整型数组的形式存在。该数组通过使用java.sql.Types类中的常量值来指示对应参数的SQL类型。对于每个参数,都需要提供一个条目。
  • 你可以使用SqlParameterValue类来包装需要这些额外信息的参数值。为此,你需要为每个值创建一个新的实例,并在构造函数中传入SQL类型和参数值。对于数值类型,你还可以提供一个可选的小数位数(scale )参数。
  • 对于使用命名参数的方法,你可以使用SqlParameterSource类,如BeanPropertySqlParameterSourceMapSqlParameterSource。它们都有方法来为任何命名参数值注册SQL类型。

理BLOB和CLOB对象

你可以在数据库中存储图像、其他二进制数据以及大量文本。这些大对象对于二进制数据称为BLOB(Binary Large OBject),对于字符数据称为CLOB(Character Large OBject)。在Spring中,你可以直接使用JdbcTemplate来处理这些大对象,也可以使用RDBMS Objects和SimpleJdbc类提供的高级抽象来处理。所有这些方法都使用LobHandler接口的实现来实际管理LOB(Large OBject)数据。LobHandler通过getLobCreator方法提供对LobCreator类的访问,该类用于创建要插入的新LOB对象。

LobCreatorLobHandler为LOB输入和输出提供以下支持:

  • BLOB
    byte[]: getBlobAsBytessetBlobAsBytes
    InputStream: getBlobAsBinaryStreamsetBlobAsBinaryStream
  • CLOB
    String: getClobAsStringsetClobAsString
    InputStream: getClobAsAsciiStreamsetClobAsAsciiStream
    Reader: getClobAsCharacterStreamsetClobAsCharacterStream

接下来的示例展示了如何创建和插入一个BLOB。稍后我们会展示如何从数据库中读取它。

这个示例使用JdbcTemplateAbstractLobCreatingPreparedStatementCallback的实现。它实现了一个方法,即setValues。这个方法提供了一个LobCreator,我们用它来设置SQL插入语句中LOB列的值。

对于这个示例,我们假设有一个变量lobHandler,它已经被设置为DefaultLobHandler的实例。你通常通过依赖注入来设置这个值。

以下示例展示了如何创建和插入一个BLOB:

final File blobIn = new File("spring2004.jpg");
final InputStream blobIs = new FileInputStream(blobIn);
final File clobIn = new File("large.txt");
final InputStream clobIs = new FileInputStream(clobIn);
final InputStreamReader clobReader = new InputStreamReader(clobIs);

jdbcTemplate.execute(
	"INSERT INTO lob_table (id, a_clob, a_blob) VALUES (?, ?, ?)",
	new AbstractLobCreatingPreparedStatementCallback(lobHandler) {
		protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException {
			ps.setLong(1, 1L);
			lobCreator.setClobAsCharacterStream(ps, 2, clobReader, (int)clobIn.length());
			lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, (int)blobIn.length());
		}
	}
);

blobIs.close();
clobReader.close();

如果你调用从DefaultLobHandler.getLobCreator()返回的LobCreator上的setBlobAsBinaryStreamsetClobAsAsciiStreamsetClobAsCharacterStream方法,你可以为contentLength参数选择性地指定一个负值。如果指定的内容长度为负值,DefaultLobHandler会使用JDBC 4.0版本的设置流方法,该方法不包含长度参数。否则,它会将指定的长度传递给驱动程序。

现在到了从数据库中读取LOB数据的时候了。同样,你需要使用JdbcTemplate,并且使用相同的实例变量lobHandler和对DefaultLobHandler的引用。以下示例展示了如何操作:

List<Map<String, Object>> l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table",
	new RowMapper<Map<String, Object>>() {
		public Map<String, Object> mapRow(ResultSet rs, int i) throws SQLException {
			Map<String, Object> results = new HashMap<String, Object>();
			String clobText = lobHandler.getClobAsString(rs, "a_clob");
			results.put("CLOB", clobText);
			byte[] blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob");
			results.put("BLOB", blobBytes);
			return results;
		}
	});

为IN子句传递值列表

SQL标准允许基于包含可变值列表的表达式来选择行。一个典型的例子是select * from T_ACTOR where id in (1, 2, 3)。JDBC标准不直接支持为预编译语句提供这种可变列表。你不能声明可变数量的占位符。你需要为期望数量的占位符准备多个不同的版本,或者当你知道需要多少占位符时,你需要动态生成SQL字符串。NamedParameterJdbcTemplate提供的命名参数支持采用后一种方法。你可以将值作为java.util.List(或任何Iterable)的简单值传递。这个列表用于将所需的占位符插入实际的SQL语句中,并在语句执行期间传递值。

在传递大量值时,请注意。JDBC标准不保证你可以在IN表达式列表中使用超过100个值。各种数据库可能会超过这个数量,但它们通常对允许的最大值有一个硬限制。例如,Oracle的限制是1000。

除了值列表中的基本值,你还可以创建一个对象数组的java.util.List。这个列表可以支持为in子句定义多个表达式,例如select * from T_ACTOR where (id, last_name) in ((1, 'Johnson'), (2, 'Harrop'))。当然,这需要你的数据库支持这种语法。

处理存储过程调用的复杂类型

当你调用存储过程时,有时可能会使用特定于数据库的复杂类型。为了处理这些类型,Spring提供了SqlReturnType来处理从存储过程调用返回的类型,以及SqlTypeValue来作为参数传递给存储过程。

SqlReturnType接口有一个必须实现的方法(名为getTypeValue)。此接口用作SqlOutParameter声明的一部分。以下示例展示了返回用户声明的类型ITEM_TYPE的Oracle STRUCT对象的值:

public class TestItemStoredProcedure extends StoredProcedure {

	public TestItemStoredProcedure(DataSource dataSource) {
		// ...
		declareParameter(new SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE",
			(CallableStatement cs, int colIndx, int sqlType, String typeName) -> {
				STRUCT struct = (STRUCT) cs.getObject(colIndx);
				Object[] attr = struct.getAttributes();
				TestItem item = new TestItem();
				item.setId(((Number) attr[0]).longValue());
				item.setDescription((String) attr[1]);
				item.setExpirationDate((java.util.Date) attr[2]);
				return item;
			}));
		// ...
	}

你可以使用SqlTypeValue将Java对象(如TestItem)的值传递给存储过程。SqlTypeValue接口有一个必须实现的方法(名为createTypeValue)。传递了活动的连接,你可以使用它来创建特定于数据库的对象,如StructDescriptor实例或ArrayDescriptor实例。以下示例创建了一个StructDescriptor实例:

final TestItem testItem = new TestItem(123L, "A test item",
		new SimpleDateFormat("yyyy-M-d").parse("2010-12-31"));

SqlTypeValue value = new AbstractSqlTypeValue() {
	protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
		StructDescriptor itemDescriptor = new StructDescriptor(typeName, conn);
		Struct item = new STRUCT(itemDescriptor, conn,
		new Object[] {
			testItem.getId(),
			testItem.getDescription(),
			new java.sql.Date(testItem.getExpirationDate().getTime())
		});
		return item;
	}
};

现在,你可以将这个SqlTypeValue添加到存储过程执行调用的输入参数Map中。

SqlTypeValue的另一个用途是将值的数组传递给Oracle存储过程。Oracle有它自己的内部ARRAY类,必须在这种情况下使用,你可以使用SqlTypeValue来创建Oracle ARRAY的实例,并用Java ARRAY中的值填充它,如下例所示:

final Long[] ids = new Long[] {1L, 2L};

SqlTypeValue value = new AbstractSqlTypeValue() {
	protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
		ArrayDescriptor arrayDescriptor = new ArrayDescriptor(typeName, conn);
		ARRAY idArray = new ARRAY(arrayDescriptor, conn, ids);
		return idArray;
	}
};

嵌入式数据库支持

org.springframework.jdbc.datasource.embedded包为嵌入式的Java数据库引擎提供支持。它原生地支持HSQL、H2和Derby。此外,你还可以使用可扩展的API来插入新的嵌入式数据库类型和DataSource实现。

为什么使用嵌入式数据库?

由于嵌入式数据库的轻量级特性,它在项目的开发阶段非常有用。其优势包括配置简单、启动迅速、可测试性强,以及在开发过程中能够迅速调整SQL语句的能力。

使用Spring XML创建嵌入式数据库

如果你想要将一个嵌入式数据库实例作为Spring ApplicationContext中的一个bean来暴露,你可以使用spring-jdbc命名空间中的embedded-database标签:

<jdbc:embedded-database id="dataSource" generate-name="true">
	<jdbc:script location="classpath:schema.sql"/>
	<jdbc:script location="classpath:test-data.sql"/>
</jdbc:embedded-database>

上述配置创建了一个嵌入式HSQL数据库,该数据库通过classpath根目录下的schema.sqltest-data.sql资源中的SQL语句进行填充。此外,作为最佳实践,嵌入式数据库被分配了一个唯一生成的名称。嵌入式数据库作为类型为javax.sql.DataSource的bean提供给Spring容器,然后根据需要注入到数据访问对象中。

通过编程方式创建嵌入式数据库

EmbeddedDatabaseBuilder类提供了一个流畅的API,用于通过编程方式构建嵌入式数据库。当你需要在独立环境或独立集成测试中创建嵌入式数据库时,可以使用该类,如下例所示:

EmbeddedDatabase db = new EmbeddedDatabaseBuilder()
		.generateUniqueName(true)
		.setType(H2)
		.setScriptEncoding("UTF-8")
		.ignoreFailedDrops(true)
		.addScript("schema.sql")
		.addScripts("user_data.sql", "country_data.sql")
		.build();

// perform actions against the db (EmbeddedDatabase extends javax.sql.DataSource)

db.shutdown()

你也可以使用EmbeddedDatabaseBuilder通过Java配置来创建嵌入式数据库,如下例所示:

@Configuration
public class DataSourceConfig {

	@Bean
	public DataSource dataSource() {
		return new EmbeddedDatabaseBuilder()
				.generateUniqueName(true)
				.setType(H2)
				.setScriptEncoding("UTF-8")
				.ignoreFailedDrops(true)
				.addScript("schema.sql")
				.addScripts("user_data.sql", "country_data.sql")
				.build();
	}
}

选择嵌入式数据库类型

本节介绍了如何选择Spring支持的三种嵌入式数据库之一。包括以下主题:

  • 使用HSQL
  • 使用H2
  • 使用Derby

使用HSQL

Spring支持HSQL 1.8.0及以上版本。如果没有明确指定类型,HSQL就是默认的嵌入式数据库。要明确指定HSQL,请将embedded-database标签的type属性设置为HSQL。如果你使用构建器API,请使用EmbeddedDatabaseType.HSQL调用setType(EmbeddedDatabaseType)方法。

使用H2

Spring支持H2数据库。要启用H2,请将embedded-database标签的type属性设置为H2。如果你使用构建器API,请使用EmbeddedDatabaseType.H2调用setType(EmbeddedDatabaseType)方法。

使用Derby

Spring支持Apache Derby 10.5及以上版本。要启用Derby,请将embedded-database标签的type属性设置为DERBY。如果你使用构建器API,请使用EmbeddedDatabaseType.DERBY调用setType(EmbeddedDatabaseType)方法。

使用嵌入式数据库测试数据访问逻辑

嵌入式数据库为测试数据访问代码提供了一种轻量级的方式。接下来的示例是一个使用嵌入式数据库的数据访问集成测试模板。当嵌入式数据库不需要在多个测试类之间重用时,使用这种模板会很有用。但是,如果你希望创建一个在测试套件内共享的嵌入式数据库,请考虑使用Spring TestContext Framework,在Spring ApplicationContext中将嵌入式数据库配置为一个bean。以下列表显示了测试模板:

public class DataAccessIntegrationTestTemplate {

	private EmbeddedDatabase db;

	@BeforeEach
	public void setUp() {
		// creates an HSQL in-memory database populated from default scripts
		// classpath:schema.sql and classpath:data.sql
		db = new EmbeddedDatabaseBuilder()
				.generateUniqueName(true)
				.addDefaultScripts()
				.build();
	}

	@Test
	public void testDataAccess() {
		JdbcTemplate template = new JdbcTemplate(db);
		template.query( /* ... */ );
	}

	@AfterEach
	public void tearDown() {
		db.shutdown();
	}

}

为嵌入式数据库生成唯一名称

开发团队在测试套件无意中尝试重新创建同一个数据库的额外实例时,经常会遇到嵌入式数据库的错误。如果XML配置文件或@Configuration类负责创建嵌入式数据库,并且相应的配置在相同的测试套件(即相同的JVM进程中)内的多个测试场景中重复使用,那么这种情况就很容易发生——例如,针对嵌入式数据库的集成测试,其ApplicationContext配置仅在活动的bean定义profile方面有所不同。

这些错误的根本原因是,Spring的EmbeddedDatabaseFactory(在内部同时被<jdbc:embedded-database> XML命名空间元素和用于Java配置的EmbeddedDatabaseBuilder使用)如果未指定名称,则将嵌入式数据库的名称设置为testdb。对于<jdbc:embedded-database>的情况,嵌入式数据库通常被分配一个与bean的id相同的名称(通常类似于dataSource)。因此,随后尝试创建嵌入式数据库并不会生成新的数据库。相反,相同的JDBC连接URL会被重复使用,尝试创建新的嵌入式数据库实际上是指向使用相同配置创建的现有嵌入式数据库。

为了解决这个常见问题,Spring Framework 4.2为嵌入式数据库提供了生成唯一名称的支持。要启用使用生成名称的功能,请使用以下选项之一。

  • EmbeddedDatabaseFactory.setGenerateUniqueDatabaseName()
  • EmbeddedDatabaseBuilder.generateUniqueName()
  • <jdbc:embedded-database generate-name="true" …​ >

扩展嵌入式数据库支持

可以通过以下两种方式扩展Spring JDBC嵌入式数据库支持:

  • 实现EmbeddedDatabaseConfigurer接口以支持新的嵌入式数据库类型。
  • 实现DataSourceFactory接口以支持新的DataSource实现,例如连接池,用于管理嵌入式数据库的连接。

初始化数据源(DataSource)

org.springframework.jdbc.datasource.init包提供了对现有数据源(DataSource)进行初始化的支持。嵌入式数据库支持为应用程序创建和初始化数据源提供了一种选择。然而,有时你可能需要初始化一个运行在某个服务器上的实例。

使用Spring XML初始化数据库

如果你想初始化一个数据库,并且你可以提供一个DataSource bean的引用,那么你可以使用spring-jdbc命名空间中的initialize-database标签:

<jdbc:initialize-database data-source="dataSource">
	<jdbc:script location="classpath:com/foo/sql/db-schema.sql"/>
	<jdbc:script location="classpath:com/foo/sql/db-test-data.sql"/>
</jdbc:initialize-database>

前面的示例会运行两个指定的脚本来操作数据库。第一个脚本创建了一个模式(schema),第二个脚本则使用测试数据集填充表格。脚本的位置也可以是带有通配符的模式,这些通配符遵循Spring中用于资源的常见Ant风格(例如,classpath*:/com/foo/**/sql/*-data.sql)。如果你使用模式,脚本将按照其URL或文件名的字典顺序运行。

数据库初始化器的默认行为是无条件地运行提供的脚本。这可能并不总是你想要的——例如,如果你针对已经包含测试数据的数据库运行脚本。通过遵循常见的模式,即首先创建表,然后插入数据,可以减少意外删除数据的可能性。如果表已经存在,第一步将失败。

然而,为了对现有数据的创建和删除获得更多的控制,XML命名空间提供了一些额外的选项。第一个选项是一个标志,用于打开和关闭初始化。你可以根据环境设置这个标志(例如从系统属性或环境bean中获取布尔值)。以下示例从系统属性中获取一个值:

<jdbc:initialize-database data-source="dataSource"
	enabled="#{systemProperties.INITIALIZE_DATABASE}">
	<jdbc:script location="..."/>
</jdbc:initialize-database>

控制现有数据如何处理的第二个选项是更宽容地对待失败。为此,你可以控制初始化器忽略从脚本中运行的SQL中的某些错误的能力,如下例所示:

<jdbc:initialize-database data-source="dataSource" ignore-failures="DROPS">
	<jdbc:script location="..."/>
</jdbc:initialize-database>

在前面的例子中,我们表示我们期望有时脚本会针对一个空数据库运行,并且脚本中有一些DROP语句可能会失败。因此,失败的SQL DROP语句将被忽略,但其他失败将导致抛出异常。如果你的SQL方言不支持DROP …​ IF EXISTS(或类似语句),但你想无条件地移除所有测试数据然后再重新创建它,这将会很有用。在这种情况下,第一个脚本通常是一组DROP语句,后面跟着一组CREATE语句。

ignore-failures选项可以设置为NONE(默认值)、DROPS(忽略失败的删除操作)或ALL(忽略所有失败)。

每个语句应该通过分号(;)分隔,或者如果脚本中完全没有分号,则可以通过换行符分隔。你可以全局控制或者逐脚本控制这一点,如下例所示:

<jdbc:initialize-database data-source="dataSource" separator="@@">
	<jdbc:script location="classpath:com/myapp/sql/db-schema.sql" separator=";"/>
	<jdbc:script location="classpath:com/myapp/sql/db-test-data-1.sql"/>
	<jdbc:script location="classpath:com/myapp/sql/db-test-data-2.sql"/>
</jdbc:initialize-database>

在这个例子中,两个测试数据脚本使用@@作为语句分隔符,而只有db-schema.sql使用;作为分隔符。这种配置指定了默认分隔符为@@,并且为db-schema脚本覆盖了这一默认设置。

如果你需要从XML命名空间中获得更多的控制,你可以直接使用DataSourceInitializer,并将其定义为你应用程序中的一个组件。

依赖于数据库的其他组件的初始化

有一大类应用程序(那些在Spring上下文启动之后才使用数据库的应用程序)可以无需额外复杂设置就使用数据库初始化器。

数据库初始化器依赖于一个DataSource实例,并在其初始化回调中运行提供的脚本(类似于XML bean定义中的init-method,组件中的@PostConstruct方法,或实现InitializingBean接口的组件中的afterPropertiesSet()方法)。如果其他bean也依赖于相同的数据源,并在初始化回调中使用数据源,可能会出现问题,因为数据尚未初始化。一个常见的例子是一个缓存,它在应用启动时急切地初始化并从数据库中加载数据。

为了解决这个问题,你有两个选择:改变你的缓存初始化策略到稍后的阶段,或者确保数据库初始化器首先被初始化。

如果你的应用程序在你的控制之下,改变缓存初始化策略可能会很简单。以下是一些关于如何实现这一点的建议:

  • 使缓存首次使用时进行懒加载,这样可以提高应用程序的启动时间。
  • 让你的缓存或初始化缓存的单独组件实现LifecycleSmartLifecycle接口。当应用程序上下文启动时,你可以通过设置SmartLifecycleautoStartup标志来自动启动它,你也可以通过调用包含上下文的ConfigurableApplicationContext.start()方法来手动启动Lifecycle
  • 使用Spring的ApplicationEvent或类似的自定义观察者机制来触发缓存初始化。当上下文准备好使用时(即所有bean都已初始化后),ContextRefreshedEvent总是由上下文发布,因此这通常是一个有用的钩子(SmartLifecycle默认就是这样工作的)。

确保数据库初始化器首先被初始化也可以很简单。以下是一些关于如何实现这一点的建议:

  • 依赖于Spring BeanFactory的默认行为,即bean是按照注册顺序进行初始化的。你可以通过采用XML配置中一组<import/>元素的通用做法来轻松安排这一点,这些元素将你的应用程序模块排序,并确保数据库和数据库初始化列在首位。
  • DataSource和使用它的业务组件分开,并将它们放在不同的ApplicationContext实例中来控制它们的启动顺序(例如,父级上下文包含DataSource,子级上下文包含业务组件)。这种结构在Spring Web应用程序中很常见,但也可以更广泛地应用。
  • 13
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值