Appearance
Spring 数据访问:使用 JDBC 的数据访问 - Part2
Tip:基于 Spring Core 5.3.30 版本。
1. 将 JDBC 操作建模为 Java 对象
org.springframework.jdbc.object 包包含了一些类,让你能以更面向对象的方式访问数据库。例如,你可以运行查询并将结果作为包含业务对象的列表返回,其中关系列数据映射到业务对象的属性。你也可以运行存储过程以及运行更新、删除和插入语句。
Note
许多 Spring 开发者认为,下面描述的各种 RDBMS 操作类(除了
StoredProcedure类)往往可以被直接的JdbcTemplate调用所替代。通常,直接在 DAO 方法中调用JdbcTemplate的方法(而不是将查询封装为一个完整的类)会更简单。然而,如果你从使用 RDBMS 操作类中获得了可衡量的价值,你应该继续使用这些类。
1.1. 理解 SqlQuery
SqlQuery 是一个可重用的、线程安全的类,它封装了一个 SQL 查询。子类必须实现 newRowMapper(..) 方法,以提供一个 RowMapper 实例,该实例可以创建一个对象,该对象来自于在查询执行期间创建的 ResultSet 的迭代中获得的每一行。SqlQuery 类很少直接使用,因为 MappingSqlQuery 子类为将行映射到 Java 类提供了更为方便的实现。其他扩展 SqlQuery 的实现包括 MappingSqlQueryWithParameters 和 UpdatableSqlQuery。
1.2. 使用 MappingSqlQuery
MappingSqlQuery 是一个可重用的查询,其中具体的子类必须实现抽象的 mapRow(..) 方法,将提供的 ResultSet 中的每一行转换为指定类型的对象。以下示例显示了一个自定义查询,该查询将 t_actor 关系的数据映射到 Actor 类的实例:
Java
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;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
这个类扩展了参数化为 Actor 类型的 MappingSqlQuery。这个自定义查询的构造函数只接受一个 DataSource 参数。在这个构造函数中,你可以调用超类的构造函数,传入 DataSource 和应该运行以检索此查询的行的 SQL。这个 SQL 用于创建 PreparedStatement,所以它可能包含在执行期间要传入的任何参数的占位符。你必须使用 declareParameter 方法声明每个参数,并传入一个 SqlParameter。SqlParameter 接受一个名称和在 java.sql.Types 中定义的 JDBC 类型。定义所有参数后,你可以调用 compile() 方法,以便可以准备并稍后运行语句。这个类在编译后是线程安全的,所以,只要在初始化 DAO 时创建了这些实例,它们就可以作为实例变量保留并重用。以下示例显示了如何定义这样的类:
Java
private ActorMappingQuery actorMappingQuery;
@Autowired
public void setDataSource(DataSource dataSource) {
this.actorMappingQuery = new ActorMappingQuery(dataSource);
}
public Customer getCustomer(Long id) {
return actorMappingQuery.findObject(id);
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
前面的示例中的方法用作为传入的唯一参数 id 来检索客户。由于我们只想返回一个对象,所以我们调用 findObject 便利方法,并将 id 作为参数。如果我们有一个返回对象列表并接受额外参数的查询,我们会使用一个接受作为可变参数传入的参数值数组的 execute 方法。以下示例显示了这样的方法:
Java
public List<Actor> searchForActors(int age, String namePattern) {
List<Actor> actors = actorSearchMappingQuery.execute(age, namePattern);
return actors;
}1
2
3
4
2
3
4
1.3. 使用 SqlUpdate
SqlUpdate 类封装了 SQL 更新操作。就像查询一样,更新对象是可重用的,而且,就像所有的 RdbmsOperation 类一样,更新可以有参数,并且在 SQL 中定义。这个类提供了一些与查询对象的 execute(..) 方法类似的 update(..) 方法。SqlUpdate 类是具体的。它可以被子类化 —— 例如,添加一个自定义的更新方法。然而,你不必子类化 SqlUpdate 类,因为它可以通过设置 SQL 和声明参数轻松地被参数化。下面的示例创建了一个名为 execute 的自定义更新方法:
Java
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);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1.4. 使用 StoredProcedure
StoredProcedure 类是 RDBMS 存储过程对象抽象的抽象超类。
继承的 sql 属性是 RDBMS 中存储过程的名称。
要为 StoredProcedure 类定义一个参数,你可以使用 SqlParameter 或其子类之一。你必须在构造函数中指定参数名称和 SQL 类型,如下面的代码片段所示:
Java
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),1
2
2
SQL 类型是使用 java.sql.Types 常量来指定的。
第一行(带有 SqlParameter)声明了一个 IN 参数。你可以在存储过程调用和使用 SqlQuery 及其子类的查询中使用 IN 参数(在理解 SqlQuery 中有介绍)。
第二行(带有 SqlOutParameter)声明了一个用于存储过程调用的 OUT 参数。还有一个 SqlInOutParameter 用于 InOut 参数(提供给过程的 IN 值,并且也返回一个值)。
对于 IN 参数,除了名称和 SQL 类型外,你还可以为数值数据指定一个刻度,或者为自定义数据库类型指定一个类型名称。对于 OUT 参数,你可以提供一个 RowMapper 来处理从 REF 游标返回的行的映射。另一个选项是指定一个 SqlReturnType,让你定义返回值的自定义处理。
下一个简单的 DAO 示例使用 StoredProcedure 调用一个函数(sysdate()),这个函数随任何 Oracle 数据库一起提供。要使用存储过程功能,你必须创建一个扩展 StoredProcedure 的类。在这个示例中,StoredProcedure 类是一个内部类。然而,如果你需要重用 StoredProcedure,你可以将其声明为顶级类。这个示例没有输入参数,但是使用 SqlOutParameter 类声明了一个日期类型的输出参数。execute() 方法运行过程并从结果 Map 中提取返回的日期。结果 Map 对每个声明的输出参数(在这种情况下,只有一个)都有一个条目,使用参数名称作为键。以下清单显示了我们的自定义 StoredProcedure 类:
Java
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;
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
以下的 StoredProcedure 示例有两个输出参数(在这种情况下,是 Oracle REF 游标):
Java
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>());
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
注意,已经在 TitlesAndGenresStoredProcedure 构造函数中使用的 declareParameter(..) 方法的重载变体是传递 RowMapper 实现实例。这是一个非常方便且强大的方式来重用现有功能。接下来的两个示例提供了两个 RowMapper 实现的代码。
TitleMapper 类将 ResultSet 映射到每个提供的 ResultSet 行的 Title 域对象,如下所示:
Java
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;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
TitleMapper 类将 ResultSet 映射到每个提供的 ResultSet 行的 Genre 域对象,如下所示:
Java
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"));
}
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
要将参数传递给 RDBMS 中定义的具有一个或多个输入参数的存储过程,你可以编写一个强类型的 execute(..) 方法,该方法将委托给超类中的无类型 execute(Map) 方法,如下例所示:
Java
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);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2. 处理参数和数据值的常见问题
在 Spring 框架的 JDBC 支持提供的不同方法中,存在着参数和数据值的常见问题。本节将介绍如何解决这些问题。
2.1. 为参数提供 SQL 类型信息
通常,Spring 会根据传入的参数类型来确定参数的 SQL 类型。当设置参数值时,可以显式提供要使用的 SQL 类型。这有时是为了正确设置 NULL 值而必要的。
你可以通过以下几种方式提供 SQL 类型信息:
JdbcTemplate的许多更新和查询方法都接受一个形式为int数组的额外参数。这个数组用于通过使用java.sql.Types类的常量值来指示相应参数的 SQL 类型。为每个参数提供一个条目。你可以使用
SqlParameterValue类来包装需要这些额外信息的参数值。为此,为每个值创建一个新实例,并在构造函数中传入 SQL 类型和参数值。你还可以为数值提供一个可选的刻度参数。对于使用命名参数的方法,你可以使用
SqlParameterSource类,BeanPropertySqlParameterSource或MapSqlParameterSource。它们都有用于注册任何命名参数值的 SQL 类型的方法。
2.2. 处理 BLOB 和 CLOB 对象
你可以在数据库中存储图像、其他二进制数据和大块的文本。这些大对象被称为 BLOB(Binary Large OBject,二进制大对象)用于二进制数据,和 CLOB(Character Large OBject,字符大对象)用于字符数据。在 Spring 中,你可以通过直接使用 JdbcTemplate 以及使用 RDBMS 对象和 SimpleJdbc 类提供的更高级的抽象来处理这些大对象。所有这些方法都使用 LobHandler 接口的实现来实际管理 LOB(Large OBject,大对象)数据。LobHandler 通过 getLobCreator 方法提供对 LobCreator 类的访问,该类用于创建要插入的新 LOB 对象。
LobCreator 和 LobHandler 提供了以下对 LOB 输入和输出的支持:
- BLOB
byte[]:getBlobAsBytes和setBlobAsBytesInputStream:getBlobAsBinaryStream和setBlobAsBinaryStream
- CLOB
String:getClobAsString和setClobAsStringInputStream:getClobAsAsciiStream和setClobAsAsciiStreamReader:getClobAsCharacterStream和setClobAsCharacterStream
下一个示例显示了如何创建和插入一个 BLOB。稍后我们将展示如何从数据库中读取它。
这个示例使用了 JdbcTemplate 和 AbstractLobCreatingPreparedStatementCallback 的一个实现。它实现了一个方法,setValues。这个方法提供了一个 LobCreator,我们用它来为你的 SQL 插入语句中的 LOB 列设置值。
对于这个示例,我们假设有一个变量 lobHandler,它已经被设置为 DefaultLobHandler 的一个实例。你通常通过依赖注入来设置这个值。
以下示例显示了如何创建和插入一个 BLOB:
Java
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();1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Note
如果你在从
DefaultLobHandler.getLobCreator()返回的LobCreator上调用setBlobAsBinaryStream、setClobAsAsciiStream或setClobAsCharacterStream方法,你可以选择为contentLength参数指定一个负值。如果指定的内容长度为负,DefaultLobHandler将使用没有长度参数的 JDBC 4.0 的 set-stream 方法的变体。否则,它将指定的长度传递给驱动程序。请查看你使用的 JDBC 驱动程序的文档,以验证它是否支持在不提供内容长度的情况下流式传输 LOB。
现在是时候从数据库中读取 LOB 数据了。再次使用 JdbcTemplate,使用相同的实例变量 lobHandler 和对 DefaultLobHandler 的引用。以下示例展示了如何做到这一点:
Java
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;
}
});1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2.3. 传入 IN 子句的值列表
SQL 标准允许基于包含值可变列表的表达式来选择行。一个典型的例子是 select * from t_actor where id in (1, 2, 3)。这个可变列表并不直接被 JDBC 标准支持用于预处理语句。你不能声明一个可变数量的占位符。你需要准备一些具有所需占位符数量的变量,或者你需要在知道需要多少占位符后动态生成 SQL 字符串。NamedParameterJdbcTemplate 提供的命名参数支持采取后一种方法。你可以将值作为简单值的 java.util.List(或任何可迭代的)传入。这个列表被用来在实际的 SQL 语句中插入所需的占位符,并在语句执行期间传入值。
Note:在传入许多值时要小心。JDBC 标准并不保证你可以在 IN 表达式列表中使用超过 100 个值。各种数据库超过了这个数字,但是它们通常对允许的值数量有一个硬限制。例如,Oracle 的限制是 1000。
除了值列表中的原始值外,你还可以创建一个 java.util.List 的对象数组。这个列表可以支持为 in 子句定义多个表达式,例如 select * from t_actor where (id, last_name) in ((1, 'Johnson'), (2, 'Harrop'))。当然,这需要你的数据库支持这种语法。
2.4. 处理存储过程调用的复杂类型
当你调用存储过程时,有时可以使用特定于数据库的复杂类型。为了适应这些类型,Spring 提供了 SqlReturnType 来处理从存储过程调用返回的这些类型,以及 SqlTypeValue 来处理作为存储过程参数传入的这些类型。
SqlReturnType 接口有一个必须实现的单一方法(名为 getTypeValue)。这个接口被用作 SqlOutParameter 声明的一部分。以下示例显示了返回用户声明的类型 ITEM_TYPE 的 Oracle STRUCT 对象的值:
Java
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;
}));
// ...
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
你可以使用 SqlTypeValue 将 Java 对象(如 TestItem)的值传递给存储过程。SqlTypeValue 接口有一个你必须实现的单一方法(名为 createTypeValue)。活动连接被传入,你可以使用它来创建特定于数据库的对象,如 StructDescriptor 实例或 ArrayDescriptor 实例。以下示例创建了一个 StructDescriptor 实例:
Java
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;
}
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
你现在可以将这个 SqlTypeValue 添加到包含存储过程 execute 调用的输入参数的 Map 中。
SqlTypeValue 的另一个用途是将一个值数组传递给 Oracle 存储过程。在这种情况下,必须使用 Oracle 自己的内部 ARRAY 类,你可以使用 SqlTypeValue 创建一个 Oracle ARRAY 的实例,并用 Java 数组的值填充它,如下面的示例所示:
Java
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;
}
};1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
3. 嵌入式数据库支持
org.springframework.jdbc.datasource.embedded 包为嵌入式 Java 数据库引擎提供支持。对 HSQL、H2 和 Derby 的支持是内置的。你也可以使用一个可扩展的 API 来插入新的嵌入式数据库类型和 DataSource 实现。
3.1. 为什么使用嵌入式数据库?
嵌入式数据库在项目开发阶段可能非常有用,因为它的轻量级特性。其优点包括配置简单、启动快速、可测试性强,以及在开发过程中能够快速演进你的 SQL。
3.2. 使用 Spring XML 创建嵌入式数据库
如果你想在 Spring ApplicationContext 中将嵌入式数据库实例暴露为一个 Bean,你可以在 spring-jdbc 命名空间中使用 embedded-database 标签。
XML
<jdbc:embedded-database id="dataSource" generate-name="true">
<jdbc:script location="classpath:schema.sql"/>
<jdbc:script location="classpath:test-data.sql"/>
</jdbc:embedded-database>1
2
3
4
2
3
4
前面的配置创建了一个嵌入式 HSQL 数据库,该数据库使用类路径根目录中的 schema.sql 和 test-data.sql 资源填充 SQL。此外,作为最佳实践,嵌入式数据库被赋予一个唯一生成的名称。嵌入式数据库作为 javax.sql.DataSource 类型的 Bean 提供给 Spring 容器,然后可以根据需要注入到数据访问对象中。
3.3. 以编程方式创建嵌入式数据库
EmbeddedDatabaseBuilder 类提供了一个流式的 API,用于以编程方式构建嵌入式数据库。当你需要在独立环境或独立集成测试中创建嵌入式数据库时,可以使用这个,如下面的示例所示:
Java
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()1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
请参阅 EmbeddedDatabaseBuilder 的 JavaDoc 以获取所有支持选项的详细信息。
你也可以使用 EmbeddedDatabaseBuilder 通过 Java 配置来创建嵌入式数据库,如下面的示例所示:
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();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
3.4. 选择嵌入式数据库类型
本节介绍了如何选择 Spring 支持的三种嵌入式数据库中的一种。
3.4.1. 使用 HSQL
Spring 支持 HSQL 1.8.0 及以上版本。如果没有明确指定类型,HSQL 是默认的嵌入式数据库。要明确指定 HSQL,将 embedded-database 标签的 type 属性设置为 HSQL。如果你使用构建器 API,使用 EmbeddedDatabaseType.HSQL 调用 setType(EmbeddedDatabaseType) 方法。
3.4.2. 使用 H2
Spring 支持 H2 数据库。要启用 H2,将 embedded-database 标签的 type 属性设置为 H2。如果你使用构建器 API,使用 EmbeddedDatabaseType.H2 调用 setType(EmbeddedDatabaseType) 方法。
3.4.3. 使用 Derby
Spring 支持 Apache Derby 10.5 及以上版本。要启用 Derby,将 embedded-database 标签的 type 属性设置为 DERBY。如果你使用构建器 API,使用 EmbeddedDatabaseType.DERBY 调用 setType(EmbeddedDatabaseType) 方法。
3.5. 使用嵌入式数据库测试数据访问逻辑
嵌入式数据库提供了一种轻量级的方式来测试数据访问代码。下一个示例是一个使用嵌入式数据库的数据访问集成测试模板。当嵌入式数据库不需要在测试类之间重用时,使用这样的模板可能很有用。然而,如果你希望创建一个在测试套件中共享的嵌入式数据库,可以考虑使用 Spring TestContext 框架,并按照使用 Spring XML 创建嵌入式数据库和以编程方式创建嵌入式数据库中的描述,将嵌入式数据库配置为 Spring ApplicationContext 中的一个 Bean。以下清单显示了测试模板:
Java
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();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
3.6. 为嵌入式数据库生成唯一名称
开发团队经常会遇到一种嵌入式数据库的错误,如果他们的测试套件无意中尝试重新创建同一数据库的额外实例。如果一个 XML 配置文件或 @Configuration 类负责创建嵌入式数据库,并且相应的配置然后在同一测试套件(即,在同一 JVM 进程)中的多个测试场景中重用,这种情况就很容易发生 —— 例如,针对嵌入式数据库的集成测试,其 ApplicationContext 配置只在哪个 Bean 定义配置文件处于活动状态方面有所不同。
这种错误的根本原因是 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" … >
3.7. 扩展嵌入式数据库支持
你可以通过两种方式扩展 Spring JDBC 的嵌入式数据库支持:
实现
EmbeddedDatabaseConfigurer以支持新的嵌入式数据库类型。实现
DataSourceFactory以支持新的DataSource实现,例如用于管理嵌入式数据库连接的连接池。
我们鼓励你在 GitHub Issues 上向 Spring 社区贡献扩展。
4. 初始化 DataSource
org.springframework.jdbc.datasource.init 包提供了初始化现有 DataSource 的支持。嵌入式数据库支持为应用程序创建和初始化 DataSource 提供了一种选项。然而,有时您可能需要初始化在服务器上运行的实例。
4.1. 使用 Spring XML 初始化数据库
如果你想初始化一个数据库,并且你可以提供一个 DataSource Bean 的引用,你可以在 spring-jdbc 命名空间中使用 initialize-database 标签。
XML
<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>1
2
3
4
2
3
4
前面的示例将两个指定的脚本运行在数据库上。第一个脚本创建一个 Schema,第二个脚本用测试数据集填充表。脚本位置也可以是带有通配符的模式,就像 Spring 中用于资源的常见 Ant 风格(例如,classpath*:/com/foo/**/sql/*-data.sql)。如果你使用模式,脚本将按照它们的 URL 或文件名的字典顺序(Lexical Order)运行。
数据库初始化器的默认行为是无条件地运行提供的脚本。这可能并不总是你想要的 —— 例如,如果你对已经有测试数据的数据库运行脚本。通过遵循常见的模式(先前显示)来创建表,然后插入数据,可以减少意外删除数据的可能性。如果表已经存在,第一步会失败。
然而,为了更好地控制现有数据的创建和删除,XML 命名空间提供了一些额外的选项。第一个是一个开关初始化的标志。你可以根据环境设置这个(例如,从系统属性或环境 Bean 中提取布尔值)。以下示例从系统属性中获取一个值:
XML
<jdbc:initialize-database data-source="dataSource"
enabled="#{systemProperties.INITIALIZE_DATABASE}">
<jdbc:script location="..."/>
</jdbc:initialize-database>1
2
3
4
2
3
4
控制现有数据发生的情况的第二种选项是对失败更加宽容。为此,你可以控制初始化器忽略从脚本运行的 SQL 中的某些错误的能力,如下面的示例所示:
XML
<jdbc:initialize-database data-source="dataSource" ignore-failures="DROPS">
<jdbc:script location="..."/>
</jdbc:initialize-database>1
2
3
2
3
在前面的示例中,我们期望有时脚本会在空数据库上运行,并且脚本中有一些 DROP 语句,因此,会失败。所以,失败的 SQL DROP 语句将被忽略,但其他失败将引发异常。如果你的 SQL 方言不支持 DROP ... IF EXISTS(或类似的语句),但你希望在重新创建之前无条件地删除所有测试数据,那么这将非常有用。在这种情况下,第一个脚本通常是一组 DROP 语句,后面跟着一组 CREATE 语句。
ignore-failures 选项可以设置为 NONE(默认),DROPS(忽略失败的 Drop),或 ALL(忽略所有失败)。
每个语句应该由 ; 或者新的一行分隔,如果脚本中根本没有 ; 字符。你可以全局性地控制,也可以按脚本控制,如下面的示例所示:
XML
<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>1
2
3
4
5
2
3
4
5
在这个示例中,两个测试数据脚本使用 @@ 作为语句分隔符,只有 db-schema.sql 使用 ;。这个配置指定默认的分隔符是 @@,并且为 db-schema 脚本覆盖了这个默认值。
如果你需要比从 XML 命名空间得到的更多的控制,你可以直接使用 DataSourceInitializer,并在你的应用程序中将其定义为一个组件。
4.1.1. 初始化依赖于数据库的其他组件
大部分应用程序(那些在 Spring 上下文启动后才使用数据库的应用程序)可以无需进一步复杂操作地使用数据库初始化器。如果你的应用程序不是其中之一,你可能需要阅读本节的其余部分。
数据库初始化器依赖于一个 DataSource 实例,并在其初始化回调中运行提供的脚本(类似于 XML Bean 定义中的 init-method,组件中的 @PostConstruct 方法,或者实现 InitializingBean 的组件中的 afterPropertiesSet() 方法)。如果其他 Bean 依赖于同一数据源,并在初始化回调中使用数据源,可能会出现问题,因为数据尚未初始化。这种情况的一个常见示例是一个在应用程序启动时就急切地初始化并从数据库加载数据的缓存。
要解决这个问题,你有两个选项:将你的缓存初始化策略更改为稍后的阶段,或确保数据库初始化器首先初始化。
如果应用程序在你的控制之下,更改你的缓存初始化策略可能会很容易。以下是一些如何实现这一点的建议:
让缓存在首次使用时懒加载,这可以优化应用程序的启动时间。
让你的缓存或一个单独的组件实现
Lifecycle或SmartLifecycle来初始化缓存。当应用程序上下文开始时,你可以通过设置其autoStartup标志自动启动SmartLifecycle,并可以通过在封闭的上下文上调用ConfigurableApplicationContext.start()手动启动Lifecycle。使用 Spring
ApplicationEvent或类似的自定义观察者机制来触发缓存初始化。ContextRefreshedEvent总是在上下文准备好使用时(所有 Bean 都已初始化后)发布,所以这通常是一个有用的钩子(这就是SmartLifecycle默认的工作方式)。
确保数据库初始化器首先初始化也可能很容易。以下是一些如何实现这一点的建议:
依赖于 Spring
BeanFactory的默认行为,即 Bean 是按照注册顺序初始化的。你可以通过采用一组在 XML 配置中按照你的应用程序模块顺序排列的<import/>元素的常见做法,并确保数据库和数据库初始化首先列出来,来轻松地安排这一点。分离
DataSource和使用它的业务组件,并通过将它们放在单独的ApplicationContext实例中来控制它们的启动顺序(例如,父上下文包含DataSource,子上下文包含业务组件)。这种结构在 Spring Web 应用程序中很常见,但可以更广泛地应用。