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男
发表回复