3.7 作为 Java 对象 JDBC 操作模型
org.springframework.jdbc.object 包蕴含一些类,这些类使你以更加面向对象的形式拜访数据库。例如,你能够运行查问并将后果作为蕴含业务对象的列表返回,该业务对象的关联列数据映射到业务对象的属性。你还能够运行存储过程并运行 update,delete 和 insert 语句。
许多 Spring 开发人员认为,上面形容的各种 RDBMS 操作类(StoredProcedure 类除外)通常能够用间接的 JdbcTemplate 调用代替。通常,编写间接在 JdbcTemplate 上调用办法的 DAO 办法(与将查问封装为残缺的类绝对)更简略。然而,如果通过应用 RDBMS 操作类取得可测量的价值,则应持续应用这些类。
3.7.1 了解SqlQuery
SqlQuery 是可重用的、线程平安的类,它封装了 SQL 查问。子类必须实现 newRowMapper(..)办法以提供 RowMapper 实例,该实例能够为遍历查问执行期间创立的 ResultSet 所取得的每一行创立一个对象。很少间接应用 SqlQuery 类,因为 MappingSqlQuery 子类为将行映射到 Java 类提供了更为不便的实现。扩大 SqlQuery 的其余实现是 MappingSqlQueryWithParameters 和 UpdatableSqlQuery。
3.7.2 应用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,因而它能够蕴含在执行期间要传递的任何参数的占位符。你必须应用传入 SqlParameter 的 declareParameter 办法申明每个参数。SqlParameter 具备名称,并且具备 java.sql.Types 中定义的 JDBC 类型。定义所有参数之后,能够调用 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 的 customer。因为只心愿返回一个对象,因而咱们以 id 为参数调用 findObject 便捷办法。相同,如果有一个查问返回一个对象列表并采纳其余参数,则将应用其中一种执行办法,该办法采纳以能够变参数模式传入的参数值数组。
public List<Actor> searchForActors(int age, String namePattern) {List<Actor> actors = actorSearchMappingQuery.execute(age, namePattern);
return actors;
}
3.7.3 应用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);
}
}
3.7.4 应用StoredProcedure
StoredProcedure 类是 RDBMS 存储过程的对象形象的超类。此类是形象的,并且其各种 execute(..)办法均具备受爱护的拜访权限,除了通过提供更严格类型的子类之外,还能够避免应用。
继承的 sql 属性是 RDBMS 中存储过程的名称。
要为 StoredProcedure 类定义参数,能够应用 SqlParameter 或其子类之一。你必须在构造函数中指定参数名称和 SQL 类型,如以下代码片段所示:
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
SQL 类型是应用 java.sql.Types 常量指定的。
第一行(带有 SqlParameter)申明一个 IN 参数。您能够将 IN 参数用于存储过程调用以及应用 SqlQuery 及其子类(理解 SqlQuery 中介绍)的查问。
第二行(带有 SqlOutParameter)申明将在存储过程调用中应用的 out 参数。还有一个用于 InOut 参数的 SqlInOutParameter(为过程提供 in 值并返回值的参数)。
对于 in 参数,除了名称和 SQL 类型外,还能够为数字数据指定精度,或者为自定义数据库类型指定类型名称。对于 out 参数,能够提供 RowMapper 来解决从 REF 游标返回的行的映射。另一个抉择是指定一个 SqlReturnType,它容许你定义返回值的自定义解决。
下一个简略 DAO 示例应用 StoredProcedure 调用任何 Oracle 数据库附带的函数 (sysdate())。要应用存储过程性能,你必须创立一个扩大 StoredProcedure 的类。在此示例中,StoredProcedure 类是一个外部类。然而,如果须要重用 StoredProcedure,则能够将其申明为顶级类。此示例没有输出参数,然而应用 SqlOutParameter 类将输入参数申明为日期类型。execute() 办法将运行该过程,并从后果 Map 中提取返回的日期。通过应用参数名称作为键,后果 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 构造函数中应用的 clarifyParameter(..)办法的重载变体传递给 RowMapper 实现实例。这是重用现有性能的十分不便且弱小的办法。接下来的两个示例提供了两个 RowMapper 实现的代码。
TitleMapper 类将提供的 ResultSet 中每一行的 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 中的每一行将 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);
}
}
3.8 参数和数据值解决的常见问题
参数和数据值的常见问题存在于 Spring 框架的 JDBC 反对所提供的不同办法中。本节介绍如何解决它们。
3.8.1 提供参数的 SQL 类型信息
通常,Spring 依据传入的参数类型确定参数的 SQL 类型。能够明确提供设置参数值时要应用的 SQL 类型。有时须要正确设置 NULL 值。
你能够通过几种形式提供 SQL 类型信息:
- JdbcTemplate 的许多更新和查询方法都采纳 int 数组模式的附加参数。该数组用于通过应用 java.sql.Types 类中的常量值来批示相应参数的 SQL 类型。为每个参数提供一个条目。
- 你能够应用 SqlParameterValue 类包装须要此附加信息的参数值。为此,请为每个值创立一个新实例,而后在构造函数中传入 SQL 类型和参数值。你还能够为数字值提供可选的精度参数。
- 对于应用命名参数的办法,能够应用 SqlParameterSource 类,BeanPropertySqlParameterSource 或 MapSqlParameterSource。它们都具备用于为任何命名参数值注册 SQL 类型的办法。
3.8.2 解决 BLOB 和 CLOB 对象
你能够在数据库中存储图像,其余二进制数据和大块文本。这些大对象称为二进制数据的 BLOB(二进制大型对象),而字符数据称为 CLOB(字符大型对象)。在 Spring 中,能够间接应用 JdbcTemplate 来解决这些大对象,也能够应用 RDBMS Objects 和 SimpleJdbc 类提供的更高形象来解决这些大对象。所有这些办法都应用 LobHandler 接口的实现来理论治理 LOB(大对象)数据。LobHandler 通过 getLobCreator 办法提供对 LobCreator 类的拜访,该办法用于创立要插入的新 LOB 对象。
LobCreator 和 LobHandler 为 LOB 输出和输入提供以下反对:
-
BLOB
byte[]
:getBlobAsBytes
andsetBlobAsBytes
InputStream
:getBlobAsBinaryStream
andsetBlobAsBinaryStream
-
CLOB
String
:getClobAsString
andsetClobAsString
InputStream
:getClobAsAsciiStream
andsetClobAsAsciiStream
Reader
:getClobAsCharacterStream
andsetClobAsCharacterStream
下一个示例显示了如何创立和插入 BLOB。稍后咱们展现如何从数据库中读取它。本示例应用 JdbcTemplate 和 AbstractLobCreatingPreparedStatementCallback 的实现。它实现了一种办法 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) { //1
protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException {ps.setLong(1, 1L);
lobCreator.setClobAsCharacterStream(ps, 2, clobReader, (int)clobIn.length()); //2
lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, (int)blobIn.length()); //3
}
}
);
blobIs.close();
clobReader.close();
- 传入 lobHandler(在此示例中)为一般的 DefaultLobHandler。
- 应用 setClobAsCharacterStream 办法传递 CLOB 内容。
- 应用 setBlobAsBinaryStream 办法传递 BLOB 内容。
如果在从 DefaultLobHandler.getLobCreator()返回的 LobCreator 上调用 setBlobAsBinaryStream、setClobAsAsciiStream 或 setClobAsCharacterStream 办法,则能够抉择为 contentLength 参数指定一个负值。如果指定的内容长度为负,则 DefaultLobHandler 将应用 set-stream 办法的 JDBC 4.0 变体,而不应用 length 参数。否则,它将指定的长度传递给驱动程序。
请参阅无关 JDBC 驱动程序的文档,以用于验证它是否反对流式 LOB,而不提供内容长度。
当初是时候从数据库中读取 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");//1
results.put("CLOB", clobText);
byte[] blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob"); //2
results.put("BLOB", blobBytes);
return results;
}
});
- 应用办法 getClobAsString 检索 CLOB 的内容。
- 应用 getBlobAsBytes 办法检索 BLOB 的内容。
3.8.3 传入 IN 子句的值列表
SQL 规范容许基于蕴含变量值列表的表达式抉择行。典型的例子select * from T_ACTOR where id in (1, 2, 3)
。JDBC 规范不间接为筹备好的语句反对此变量列表。你不能申明可变数量的占位符。你须要筹备好所需数目的占位符的多种变体,或者一旦晓得须要多少个占位符,就须要动静生成 SQL 字符串。NamedParameterJdbcTemplate 和 JdbcTemplate 中提供的命名参数反对采纳后一种办法。你能够将值作为原始对象的 java.util.List 传入。该列表用于插入所需的占位符,并在语句执行期间传递值。
传递许多值时要小心。JDBC 规范不保障你能够为 in 表达式列表应用 100 个以上的值。各种数据库都超过了这个数目,然而它们通常对容许多少个值有硬性限度。例如,Oracle 的限度为 1000。
除了值列表中的原始类型值外,还能够创建对象数组的 java.util.List。该列表能够反对为 in 子句定义的多个表达式,例如,T_ACTOR 的 select * from((1,'Johnson'),(2,'Harrop'))
中的(id,last_name)
。当然,这要求你的数据库反对此语法。
3.8.4 解决存储过程调用的简单类型
调用存储过程时,有时能够应用特定于数据库的简单类型。为了包容这些类型,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 增加到蕴含用于存储过程的 execute 调用的输出参数的 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;
}
};
3.9 嵌入式数据库反对
org.springframework.jdbc.datasource.embedded 包为嵌入式 Java 数据库引擎提供反对。本地提供对 HSQL 和 Derby 的反对。你还能够应用可扩大的 API 来插入新的嵌入式数据库类型和 DataSource 实现。
3.9.1 为什么要应用嵌入式数据库?
嵌入式数据库因为具备轻量级的个性,因而在我的项目的开发阶段可能会很有用。益处包含易于配置,启动工夫短,可测试性以及在开发过程中疾速演变 SQL 的能力。
3.9.2 应用 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 数据库,该数据库由来自类门路根目录中的 schema.sql 和 test-data.sql 资源的 SQL 填充。另外,作为最佳实际,将为嵌入式数据库调配一个惟一生成的名称。嵌入式数据库作为 javax.sql.DataSource 类型的 bean 提供给 Spring 容器,而后能够依据须要将其注入到数据拜访对象中。
3.9.3 以编程形式创立嵌入式数据库
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 的 javadoc。
你还能够应用 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();}
}
3.9.4 抉择嵌入式数据库类型
本节介绍如何抉择 Spring 反对的三个嵌入式数据库之一。它包含以下主题:
- 应用 HSQL
- 应用 H2
- 应用 Derby
应用 HSQL
Spring 反对 HSQL 1.8.0 及更高版本。如果未明确指定类型,则 HSQL 是默认的嵌入式数据库。要明确指定 HSQL,请将嵌入式数据库标记的 type 属性设置为 HSQL。如果应用构建器 API,请应用 EmbeddedDatabaseType.HSQL 调用 setType(EmbeddedDatabaseType)办法。
应用 H2
Spring 反对 H2 数据库。要启用 H2,请将嵌入式数据库标记的 type 属性设置为 H2。如果应用构建器 API,请应用 EmbeddedDatabaseType.H2 调用 setType(EmbeddedDatabaseType)办法。
应用 Derby
Spring 反对 Apache Derby 10.5 及更高版本。要启用 Derby,请将嵌入式数据库标记的 type 属性设置为 DERBY。如果应用构建器 API,请应用 EmbeddedDatabaseType.DERBY 调用 setType(EmbeddedDatabaseType)办法。
3.9.5 应用嵌入式数据库测试数据拜访逻辑
嵌入式数据库提供了一种轻量级的办法来测试数据拜访代码。下一个示例是应用嵌入式数据库的数据拜访集成测试模板。当嵌入式数据库不须要在测试类之间重用时,应用这种模板能够一次性应用。然而,如果您心愿创立在测试套件中共享的嵌入式数据库,请思考应用 Spring TestContext 框架并将嵌入式数据库配置为 Spring ApplicationContext 中的 Bean,如应用 Spring XML 创立嵌入式数据库和以编程形式嵌入数据库。以下清单显示了测试模板:
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();
}
}
3.9.6 为嵌入式数据库生成惟一名称
如果开发团队的测试套件无意间尝试从新创立同一数据库的其余实例,则开发团队常常会遇到谬误。如果 XML 配置文件或 @Configuration 类负责创立嵌入式数据库,而后在同一测试套件(即同一 JVM 过程)中的多个测试场景中重用相应的配置,则这很容易产生。集成测试针对其 ApplicationContext 配置仅在哪些 bean 定义配置文件处于活动状态方面有所不同的嵌入式数据库进行。
造成此类谬误的根本原因是,如果未另行指定,Spring 的 EmbeddedDatabaseFactory(由 <jdbc:embedded-database> XML 名称空间元素和 EmbeddedDatabaseBuilder 为 Java 配置在外部应用)会将嵌入式数据库的名称设置为 testdb。对于 <jdbc:embedded-database> 的状况,通常为嵌入式数据库调配的名称等于 Bean 的 ID(通常是相似于 dataSource 的名称)。因而,随后创立嵌入式数据库的尝试不会产生新的数据库。取而代之的是,雷同的 JDBC 连贯 URL 被重用,并且尝试创立新的嵌入式数据库实际上指向的是从雷同配置创立的现有嵌入式数据库。
为了解决这个常见问题,Spring 框架 4.2 提供了对生成嵌入式数据库的惟一名称的反对。要启用生成名称的应用,请应用以下选项之一。
EmbeddedDatabaseFactory.setGenerateUniqueDatabaseName()
EmbeddedDatabaseBuilder.generateUniqueName()
<jdbc:embedded-database generate-name="true" … >
3.9.7 扩大嵌入式数据库反对
你能够通过两种形式扩大 Spring JDBC 嵌入式数据库的反对:
- 实现 EmbeddedDatabaseConfigurer 以反对新的嵌入式数据库类型。
- 实现 DataSourceFactory 以反对新的 DataSource 实现,例如用于治理嵌入式数据库连贯的连接池。
3.10 初始化DataSource
org.springframework.jdbc.datasource.init 包提供了对初始化现有 DataSource 的反对。嵌入式数据库反对提供了一种为应用程序创立和初始化数据源的选项。然而,有时你可能须要初始化在某处的服务器上运行的实例。
3.10.1 应用 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}"> //1
<jdbc:script location="..."/>
</jdbc:initialize-database>
- 从名为 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="@@">//1
<jdbc:script location="classpath:com/myapp/sql/db-schema.sql" separator=";"/> //2
<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.sql
应用;
。此配置指定默认分隔符为@@
,并笼罩 db-schema 脚本的默认分隔符。
如果你须要比从 XML 名称空间取得更多控制权,则能够间接应用 DataSourceInitializer 并将其定义为应用程序中的组件。
初始化依赖于数据库的其余组件
大量应用程序(那些在 Spring 上下文启动之后才应用数据库的应用程序)能够应用数据库初始化程序,而不会带来更多麻烦。如果你的应用程序不是其中之一,则可能须要浏览本节的其余部分。
数据库初始化程序依赖于 DataSource 实例,并运行其初始化回调中提供的脚本 (相似于 XML bean 定义中的 init 办法,组件中的 @PostConstruct 办法或实现 InitializingBean 的组件中的 afterPropertiesSet() 办法 )。如果其余 bean 依赖于同一数据源并在初始化回调中应用该数据源,则可能存在问题,因为数据尚未初始化。一个常见的例子是一个高速缓存,它会在应用程序启动时急于初始化并从数据库加载数据。
要解决此问题,你有两个抉择:将高速缓存初始化策略更改为当前的阶段,或者确保首先初始化数据库初始化程序。
如果应用程序在你的管制之下,则更改缓存初始化策略可能很容易,否则就不那么容易。无关如何实现这一点的一些倡议包含:
- 使高速缓存在首次应用时提早初始化,从而缩短了应用程序的启动工夫。
- 让你的缓存或初始化缓存的独自组件实现 Lifecycle 或 SmartLifecycle。当应用程序上下文启动时,你能够通过设置其 SmartStartup 标记来主动启动 SmartLifecycle,并且能够通过在关闭上下文中调用 ConfigurableApplicationContext.start()来手动启动 Lifecycle。
- 应用 Spring ApplicationEvent 或相似的自定义观察者机制来触发缓存初始化。ContextRefreshedEvent 在筹备好应用时(在所有 bean 都初始化之后)总是由上下文公布,因而通常是一个有用的钩子(默认状况下,SmartLifecycle 的工作形式)。
确保首先初始化数据库初始化程序也很容易。对于如何实现这一点的一些倡议包含:
- 依附 Spring BeanFactory 的默认行为,即按注册程序初始化 bean。通过采纳 XML 配置中的一组 <import /> 元素(对利用程序模块进行排序)的通用做法,并确保首先列出数据库和数据库初始化,能够轻松地进行安顿。
- 将数据源和应用它的业务组件离开,并通过将它们放在独自的 ApplicationContext 实例中来管制启动程序(例如,父上下文蕴含 DataSource,子上下文蕴含业务组件)。这种构造在 Spring Web 应用程序中很常见,但能够更宽泛地利用。
作者
集体从事金融行业,就任过易极付、思建科技、某网约车平台等重庆一流技术团队,目前就任于某银行负责对立领取零碎建设。本身对金融行业有强烈的喜好。同时也实际大数据、数据存储、自动化集成和部署、散布式微服务、响应式编程、人工智能等畛域。同时也热衷于技术分享创建公众号和博客站点对常识体系进行分享。关注公众号:青年 IT 男 获取最新技术文章推送!
博客地址: http://youngitman.tech
CSDN: https://blog.csdn.net/liyong1…
微信公众号:
技术交换群:
该系列文章请关注微信公众:青年 IT 男