Categories
程式開發

Spring 5 中文解析數據存儲篇-JDBC數據存儲(下)


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 {

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 searchForActors(int age, String namePattern) {
List 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 results = execute(new HashMap());
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 execute() {
// again, this sproc has no input parameters, so an empty Map is supplied
return super.execute(new HashMap());
}
}

請注意如何在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 {</p> <p> public Title mapRow(ResultSet rs, int rowNum) throws SQLException {<br /> Title title = new Title();<br /> title.setId(rs.getLong("id"));<br /> title.setName(rs.getString("name"));<br /> return title;<br /> }<br /> }</code></p> </p> <p>GenreMapper類針對提供的ResultSet中的每一行將ResultSet映射到Genre域對象,如下所示:</p> </p> <p><code>import java.sql.ResultSet;<br /> import java.sql.SQLException;<br /> import com.foo.domain.Genre;<br /> import org.springframework.jdbc.core.RowMapper;</p> <p>public final class GenreMapper implements RowMapper {</p> <p> public Genre mapRow(ResultSet rs, int rowNum) throws SQLException {<br /> return new Genre(rs.getString("name"));<br /> }<br /> }</code></p> </p> <p>要將參數傳遞給在RDBMS中定義中具有一個或多個輸入參數的存儲過程,可以編寫一個強類型化execute(..(方法的代碼,該方法將委託給超類中的非類型execute(Map )方法,例如以下示例顯示:</p> </p> <p><code>import java.sql.Types;<br /> import java.util.Date;<br /> import java.util.HashMap;<br /> import java.util.Map;<br /> import javax.sql.DataSource;<br /> import oracle.jdbc.OracleTypes;<br /> import org.springframework.jdbc.core.SqlOutParameter;<br /> import org.springframework.jdbc.core.SqlParameter;<br /> import org.springframework.jdbc.object.StoredProcedure;</p> <p>public class TitlesAfterDateStoredProcedure extends StoredProcedure {</p> <p> private static final String SPROC_NAME = "TitlesAfterDate";<br /> private static final String CUTOFF_DATE_PARAM = "cutoffDate";</p> <p> public TitlesAfterDateStoredProcedure(DataSource dataSource) {<br /> super(dataSource, SPROC_NAME);<br /> declareParameter(new SqlParameter(CUTOFF_DATE_PARAM, Types.DATE);<br /> declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));<br /> compile();<br /> }</p> <p> public Map execute(Date cutoffDate) {<br /> Map inputs = new HashMap();<br /> inputs.put(CUTOFF_DATE_PARAM, cutoffDate);<br /> return super.execute(inputs);<br /> }<br /> }</code></p> </p> </p> <p><h5>3.8 參數和數據值處理的常見問題</h5> </p> </p> <p>參數和數據值的常見問題存在於Spring框架的JDBC支持所提供的不同方法中。本節介紹如何解決它們。</p> </p> </p> <p>###### 3.8.1 提供參數的SQL類型信息</p> </p> </p> <p>通常,Spring根據傳入的參數類型確定參數的SQL類型。可以明確提供設置參數值時要使用的SQL類型。有時需要正確設置NULL值。</p> </p> <p>你可以通過幾種方式提供SQL類型信息:</p> </p> <p>JdbcTemplate的許多更新和查詢方法都採用int數組形式的附加參數。該數組用於通過使用java.sql.Types類中的常量值來指示相應參數的SQL類型。為每個參數提供一個條目。你可以使用SqlParameterValue類包裝需要此附加信息的參數值。為此,請為每個值創建一個新實例,然後在構造函數中傳入SQL類型和參數值。你還可以為數字值提供可選的精度參數。對於使用命名參數的方法,可以使用SqlParameterSource類,BeanPropertySqlParameterSource或MapSqlParameterSource。它們都具有用於為任何命名參數值註冊SQL類型的方法。</p> </p> </p> <p>###### 3.8.2 處理BLOB和CLOB對象</p> </p> </p> <p>你可以在數據庫中存儲圖像,其他二進制數據和大塊文本。這些大對象稱為二進制數據的BLOB(二進制大型對象),而字符數據稱為CLOB(字符大型對象)。在Spring中,可以直接使用JdbcTemplate來處理這些大對象,也可以使用RDBMS Objects和SimpleJdbc類提供的更高抽象來處理這些大對象。所有這些方法都使用LobHandler接口的實現來實際管理LOB(大對象)數據。 LobHandler通過getLobCreator方法提供對LobCreator類的訪問,該方法用於創建要插入的新LOB對象。</p> </p> <p>LobCreator和LobHandler為LOB輸入和輸出提供以下支持:</p> </p> <p>BLOB</p> <p> *字節[]:getBlobAsBytes和setBlobAsBytes</p> <p> * InputStream:getBlobAsBinaryStream和setBlobAsBinaryStream</p> <p>CLOB</p> <p> *字符串:getClobAsString和setClobAsString</p> <p> * InputStream:getClobAsAsciiStream和setClobAsAsciiStream</p> <p> *讀者:getClobAsCharacterStream和setClobAsCharacterStream</p> </p> <p>下一個示例顯示瞭如何創建和插入BLOB。稍後我們展示如何從數據庫中讀取它。本示例使用JdbcTemplate和AbstractLobCreatingPreparedStatementCallback的實現。它實現了一種方法setValues。此方法提供了一個LobCreator,我們可以使用它來設置SQL插入語句中的LOB列的值。</p> </p> <p>對於此示例,我們假設存在一個變量lobHandler,該變量已設置為DefaultLobHandler的實例。通常,你可以通過依賴注入來設置此值。</p> </p> <p>以下示例顯示如何創建和插入BLOB:</p> </p> <p><code>final File blobIn = new File("spring2004.jpg");<br /> final InputStream blobIs = new FileInputStream(blobIn);<br /> final File clobIn = new File("large.txt");<br /> final InputStream clobIs = new FileInputStream(clobIn);<br /> final InputStreamReader clobReader = new InputStreamReader(clobIs);</p> <p>jdbcTemplate.execute(<br /> "INSERT INTO lob_table (id, a_clob, a_blob) VALUES (?, ?, ?)",<br /> new AbstractLobCreatingPreparedStatementCallback(lobHandler) { //1<br /> protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException {<br /> ps.setLong(1, 1L);<br /> lobCreator.setClobAsCharacterStream(ps, 2, clobReader, (int)clobIn.length()); //2<br /> lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, (int)blobIn.length()); //3<br /> }<br /> }<br /> );</p> <p>blobIs.close();<br /> clobReader.close();</code></p> </p> <p>傳入lobHandler(在此示例中)為普通的DefaultLobHandler。使用setClobAsCharacterStream方法傳遞CLOB內容。使用setBlobAsBinaryStream方法傳遞BLOB內容。</p> </p> <blockquote><p>如果在從DefaultLobHandler.getLobCreator()返回的LobCreator上調用setBlobAsBinaryStream、setClobAsAsciiStream或setClobAsCharacterStream方法,則可以選擇為contentLength參數指定一個負值。如果指定的內容長度為負,則DefaultLobHandler將使用set-stream方法的JDBC 4.0變體,而不使用length參數。否則,它將指定的長度傳遞給驅動程序。</p></blockquote> <p>></p> <blockquote><p>請參閱有關JDBC驅動程序的文檔,以用於驗證它是否支持流式LOB,而不提供內容長度。</p></blockquote> </p> <p>現在是時候從數據庫中讀取LOB數據了。再次,你將JdbcTemplate與相同的實例變量lobHandler和對DefaultLobHandler的引用一起使用。以下示例顯示瞭如何執行此操作:</p> </p> <p><code>List<Map> l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table",<br /> new RowMapper<Map>() {<br /> public Map mapRow(ResultSet rs, int i) throws SQLException {<br /> Map results = new HashMap();<br /> String clobText = lobHandler.getClobAsString(rs, "a_clob");//1<br /> results.put("CLOB", clobText);<br /> byte[] blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob"); //2<br /> results.put("BLOB", blobBytes);<br /> return results;<br /> }<br /> });</code></p> </p> <p>使用方法getClobAsString檢索CLOB的內容。使用getBlobAsBytes方法檢索BLOB的內容。</p> </p> </p> <p>###### 3.8.3 傳入IN子句的值列表</p> </p> </p> <p>SQL標准允許基於包含變量值列表的表達式選擇行。典型的例子s​​elect * from T_ACTOR where id in (1, 2, 3)。 JDBC標準不直接為準備好的語句支持此變量列表。你不能聲明可變數量的佔位符。你需要準備好所需數目的佔位符的多種變體,或者一旦知道需要多少個佔位符,就需要動態生成SQL字符串。 NamedParameterJdbcTemplate和JdbcTemplate中提供的命名參數支持採用後一種方法。你可以將值作為原始對象的java.util.List傳入。該列表用於插入所需的佔位符,並在語句執行期間傳遞值。</p> </p> <blockquote><p>傳遞許多值時要小心。 JDBC標準不保證你可以為in表達式列表使用100個以上的值。各種數據庫都超過了這個數目,但是它們通常對允許多少個值有硬性限制。例如,Oracle的限制為1000。</p></blockquote> </p> <p>除了值列表中的原始類型值外,還可以創建對像數組的java.util.List。該列表可以支持為in子句定義的多個表達式,例如,T_ACTOR的select * from((1,’Johnson’),(2,’Harrop’))中的(id,last_name)。當然,這要求你的數據庫支持此語法。</p> </p> </p> <p>###### 3.8.4 處理存儲過程調用的複雜類型</p> </p> </p> <p>調用存儲過程時,有時可以使用特定於數據庫的複雜類型。為了容納這些類型,Spring提供了一個SqlReturnType來處理從存儲過程調用返回的這些類型,並提供SqlTypeValue作為參數作為參數傳遞給存儲過程的情況。</p> </p> <p>SqlReturnType接口具有必須實現的單個方法(名為getTypeValue)。此接口用作SqlOutParameter聲明的一部分。以下示例顯示了返回用戶聲明類型為ITEM_TYPE的Oracle STRUCT對象的值:</p> </p> <p><code>public class TestItemStoredProcedure extends StoredProcedure {</p> <p> public TestItemStoredProcedure(DataSource dataSource) {<br /> // ...<br /> declareParameter(new SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE",<br /> (CallableStatement cs, int colIndx, int sqlType, String typeName) -> {<br /> STRUCT struct = (STRUCT) cs.getObject(colIndx);<br /> Object[] attr = struct.getAttributes();<br /> TestItem item = new TestItem();<br /> item.setId(((Number) attr[0]).longValue());<br /> item.setDescription((String) attr[1]);<br /> item.setExpirationDate((java.util.Date) attr[2]);<br /> return item;<br /> }));<br /> // ...<br /> }</code></p> </p> <p>你可以使用SqlTypeValue將Java對象(例如TestItem)的值傳遞給存儲過程。 SqlTypeValue接口具有必須實現的單個方法(名為createTypeValue)。活動連接被傳入,你可以使用它來創建特定於數據庫的對象,例如StructDescriptor實例或ArrayDescriptor實例。下面的示例創建一個StructDescriptor實例:</p> </p> <p><code>final TestItem testItem = new TestItem(123L, "A test item",<br /> new SimpleDateFormat("yyyy-M-d").parse("2010-12-31"));</p> <p>SqlTypeValue value = new AbstractSqlTypeValue() {<br /> protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {<br /> StructDescriptor itemDescriptor = new StructDescriptor(typeName, conn);<br /> Struct item = new STRUCT(itemDescriptor, conn,<br /> new Object[] {<br /> testItem.getId(),<br /> testItem.getDescription(),<br /> new java.sql.Date(testItem.getExpirationDate().getTime())<br /> });<br /> return item;<br /> }<br /> };</code></p> </p> <p>現在,你可以將此SqlTypeValue添加到包含用於存儲過程的execute調用的輸入參數的Map中。</p> </p> <p>SqlTypeValue的另一個用途是將值數組傳遞給Oracle存儲過程。在這種情況下,Oracle具有自己的內部ARRAY類,並且你可以使用SqlTypeValue創建Oracle ARRAY的實例,並使用Java ARRAY中的值填充它,如以下示例所示:</p> </p> <p><code>final Long[] ids = new Long[] {1L, 2L};</p> <p>SqlTypeValue value = new AbstractSqlTypeValue() {<br /> protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {<br /> ArrayDescriptor arrayDescriptor = new ArrayDescriptor(typeName, conn);<br /> ARRAY idArray = new ARRAY(arrayDescriptor, conn, ids);<br /> return idArray;<br /> }<br /> };</code></p> </p> </p> <p><h5>3.9 嵌入式數據庫支持</h5> </p> </p> <p>org.springframework.jdbc.datasource.embedded包為嵌入式Java數據庫引擎提供支持。本地提供對[HSQL](http://www.hsqldb.org/,<a href="https://www.h2database.com/" rel="nofollow noopener" target="_blank">H2</a>“和[Derby](https://db.apache.org/derby)的支持。你還可以使用可擴展的API來插入新的嵌入式數據庫類型和DataSource實現。</p> </p> </p> <p>###### 3.9.1 為什麼要使用嵌入式數據庫?</p> </p> </p> <p>嵌入式數據庫由於具有輕量級的特性,因此在項目的開發階段可能會很有用。好處包括易於配置,啟動時間短,可測試性以及在開發過程中快速演化SQL的能力。</p> </p> </p> <p>###### 3.9.2 使用Spring XML創建嵌入式數據庫</p> </p> </p> <p>如果要在Spring ApplicationContext中將嵌入式數據庫實例作為Bean公開,則可以在spring-jdbc命名空間中使用Embedded-database標記:</p> </p> <p><code></p> <p></code></p> </p> <p>前面的配置創建了一個嵌入式HSQL數據庫,該數據庫由來自類路徑根目錄中的schema.sql和test-data.sql資源的SQL填充。另外,作為最佳實踐,將為嵌入式數據庫分配一個唯一生成的名稱。嵌入式數據庫作為javax.sql.DataSource類型的bean提供給Spring容器,然後可以根據需要將其註入到數據訪問對像中。</p> </p> </p> <p>###### 3.9.3 以編程方式創建嵌入式數據庫</p> </p> </p> <p>EmbeddedDatabaseBuilder類提供了一種流暢的API,可用於以編程方式構造嵌入式數據庫。當你需要在獨立環境或獨立集成測試中創建嵌入式數據庫時,可以使用此方法,如以下示例所示:</p> </p> <p> Java</p> <p>EmbeddedDatabase db =新的EmbeddedDatabaseBuilder()</p> <p> .generateUniqueName(真)</p> <p> .setType(H2)</p> <p> .setScriptEncoding(“ UTF-8”)</p> <p> .ignoreFailedDrops(true)</p> <p> .addScript(“ schema.sql”)</p> <p> .addScripts(“ userdata.sql”,“ countrydata.sql”)</p> <p> 。建立();</p> </p> <p>//對數據庫執行操作(EmbeddedDatabase擴展了javax.sql.DataSource)</p> </p> <p>db.shutdown()</p> <p> “`</p> </p> <p>有關所有支持的選項的更多詳細信息,請參見<a href="https://docs.spring.io/spring-framework/docs/5.2.8.RELEASE/javadoc-api/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.html" rel="nofollow noopener" target="_blank">EmbeddedDatabaseBuilder</a>“的javadoc。</p> </p> <p>你還可以使用EmbeddedDatabaseBuilder通過Java配置創建嵌入式數據庫,如以下示例所示:</p> </p> <p><code>@Configuration<br /> public class DataSourceConfig {</p> <p> @Bean<br /> public DataSource dataSource() {<br /> return new EmbeddedDatabaseBuilder()<br /> .generateUniqueName(true)<br /> .setType(H2)<br /> .setScriptEncoding("UTF-8")<br /> .ignoreFailedDrops(true)<br /> .addScript("schema.sql")<br /> .addScripts("user_data.sql", "country_data.sql")<br /> .build();<br /> }<br /> }</code></p> </p> </p> <p>###### 3.9.4 選擇嵌入式數據庫類型</p> </p> </p> <p>本節介紹如何選擇Spring支持的三個嵌入式數據庫之一。它包括以下主題:</p> </p> <p><a href="https://docs.spring.io/spring/docs/5.2.8.RELEASE/spring-framework-reference/data-access.html#jdbc-embedded-database-using-HSQL" rel="nofollow noopener" target="_blank">使用HSQL</a>”<a href="https://docs.spring.io/spring/docs/5.2.8.RELEASE/spring-framework-reference/data-access.html#jdbc-embedded-database-using-H2" rel="nofollow noopener" target="_blank">使用H2</a>”<a href="https://docs.spring.io/spring/docs/5.2.8.RELEASE/spring-framework-reference/data-access.html#jdbc-embedded-database-using-Derby" rel="nofollow noopener" target="_blank">使用德比</a>”</p> </p> </p> <p>使用HSQL</p> </p> <p>Spring支持HSQL 1.8.0及更高版本。如果未明確指定類型,則HSQL是默認的嵌入式數據庫。要明確指定HSQL,請將嵌入式數據庫標記的type屬性設置為HSQL。如果使用構建器API,請使用EmbeddedDatabaseType.HSQL調用setType(EmbeddedDatabaseType)方法。</p> </p> </p> <p>使用H2</p> </p> <p>Spring支持H2數據庫。要啟用H2,請將嵌入式數據庫標記的type屬性設置為H2。如果使用構建器API,請使用EmbeddedDatabaseType.H2調用setType(EmbeddedDatabaseType)方法。</p> </p> </p> <p>使用德比</p> </p> <p>Spring支持Apache Derby 10.5及更高版本。要啟用Derby,請將嵌入式數據庫標記的type屬性設置為DERBY。如果使用構建器API,請使用EmbeddedDatabaseType.DERBY調用setType(EmbeddedDatabaseType)方法。</p> </p> </p> <p>###### 3.9.5 使用嵌入式數據庫測試數據訪問邏輯</p> </p> </p> <p>嵌入式數據庫提供了一種輕量級的方法來測試數據訪問代碼。下一個示例是使用嵌入式數據庫的數據訪問集成測試模板。當嵌入式數據庫不需要在測試類之間重用時,使用這種模板可以一次性使用。但是,如果您希望創建在測試套件中共享的嵌入式數據庫,請考慮使用<a href="https://docs.spring.io/spring/docs/5.2.8.RELEASE/spring-framework-reference/testing.html#testcontext-framework" rel="nofollow noopener" target="_blank">Spring TestContext框架</a>“並將嵌入式數據庫配置為Spring ApplicationContext中的Bean,如使用[Spring XML创建嵌入式数据库](https://docs.spring.io/spring/docs/5.2.8.RELEASE/spring-framework-reference/data-access.html#jdbc-embedded-database-xml)和以[编程方式嵌入数据库](https://docs.spring.io/spring/docs/5.2.8.RELEASE/spring-framework-reference/data-access.html#jdbc-embedded-database-java)。以下清單顯示了測試模板:</p> </p> <p><code>public class DataAccessIntegrationTestTemplate {</p> <p> private EmbeddedDatabase db;</p> <p> @BeforeEach<br /> public void setUp() {<br /> // creates an HSQL in-memory database populated from default scripts<br /> // classpath:schema.sql and classpath:data.sql<br /> db = new EmbeddedDatabaseBuilder()<br /> .generateUniqueName(true)<br /> .addDefaultScripts()<br /> .build();<br /> }</p> <p> @Test<br /> public void testDataAccess() {<br /> JdbcTemplate template = new JdbcTemplate(db);<br /> template.query( /* ... */ );<br /> }</p> <p> @AfterEach<br /> public void tearDown() {<br /> db.shutdown();<br /> }</p> <p>}</code></p> </p> </p> <p>###### 3.9.6 為嵌入式數據庫生成唯一名稱</p> </p> </p> <p>如果開發團隊的測試套件無意間嘗試重新創建同一數據庫的其他實例,則開發團隊經常會遇到錯誤。如果XML配置文件或@Configuration類負責創建嵌入式數據庫,然後在同一測試套件(即同一JVM進程)中的多個測試場景中重用相應的配置,則這很容易發生。集成測試針對其ApplicationContext配置僅在哪些bean定義配置文件處於活動狀態方面有所不同的嵌入式數據庫進行。</p> </p> <p>造成此類錯誤的根本原因是,如果未另行指定,Spring的EmbeddedDatabaseFactory(由XML名稱空間元素和EmbeddedDatabaseBuilder 為Java配置在內部使用)會將嵌入式數據庫的名稱設置為testdb。對於的情況,通常為嵌入式數據庫分配的名稱等於Bean的ID(通常是類似於dataSource的名稱)。因此,隨後創建嵌入式數據庫的嘗試不會產生新的數據庫。取而代之的是,相同的JDBC連接URL被重用,並且嘗試創建新的嵌入式數據庫實際上指向的是從相同配置創建的現有嵌入式數據庫。</p> </p> <p>為了解決這個常見問題,Spring框架4.2提供了對生成嵌入式數據庫的唯一名稱的支持。要啟用生成名稱的使用,請使用以下選項之一。</p> </p> <p>EmbeddedDatabaseFactory.setGenerateUniqueDatabaseName()EmbeddedDatabaseBuilder.generateUniqueName()</p> </p> </p> <p>###### 3.9.7 擴展嵌入式數據庫支持</p> </p> </p> <p>你可以通過兩種方式擴展Spring JDBC嵌入式數據庫的支持:</p> </p> <p>實現EmbeddedDatabaseConfigurer以支持新的嵌入式數據庫類型。實現DataSourceFactory以支持新的DataSource實現,例如用於管理嵌入式數據庫連接的連接池。</p> </p> </p> <p><h5>3.10 初始化`DataSource`</h5> </p> </p> <p>org.springframework.jdbc.datasource.init包提供了對初始化現有DataSource的支持。嵌入式數據庫支持提供了一種為應用程序創建和初始化數據源的選項。但是,有時你可能需要初始化在某處的服務器上運行的實例。</p> </p> </p> <p>###### 3.10.1 使用Spring XML初始化數據庫</p> </p> </p> <p>如果要初始化數據庫,並且可以提供對DataSource bean的引用,則可以在spring-jdbc命名空間中使用initialize-database標籤:</p> </p> <p><code></p> <p></code></p> </p> <p>前面的示例對數據庫運行兩個指定的腳本。第一個腳本創建schema,第二個腳本用測試數據集填充表。腳本位置也可以是帶有通配符的模式,該模式具有用於Spring中資源的常用Ant樣式(例如,classpath *:/com/foo/**/sql/*-data.sql)。如果使用模式,則腳本以其URL或文件名的詞法順序運行。</p> </p> <p>數據庫初始化程序的默認行為是無條件運行所提供的腳本。這可能並不總是你想要的。例如,如果你對已經有測試數據的數據庫運行腳本。通過遵循首先創建表然後插入數據的通用模式(如前所示),可以減少意外刪除數據的可能性。如果表已經存在,則第一步失敗。</p> </p> <p>但是,為了更好地控制現有數據的創建和刪除,XML名稱空間提供了一些其他選項。第一個是用於打開和關閉初始化的標誌。你可以根據環境進行設置(例如,從系統屬性或環境Bean中獲取布爾值)。以下示例從系統屬性獲取值:</p> </p> <p><code> //1</p> <p></code></p> </p> <p>從名為INITIALIZE_DATABASE的系統屬性中獲取啟用的值。</p> </p> <p>控制現有數據會發生什麼的第二種選擇是更容忍故障。為此,你可以控制初始化程序忽略腳本運行的SQL中某些錯誤的能力,如以下示例所示:</p> </p> <p><code></p> <p></code></p> </p> <p>在前面的示例中,我們說我們期望有時腳本是針對空數據庫運行的,並且腳本中有一些DROP語句可能因此失敗。因此失敗的SQL DROP語句將被忽略,但其他失敗將導致異常。如果你的SQL方言不支持DROP … IF EXISTS(或類似),但你想要無條件地刪除所有測試數據然後重新創建,則此功能非常有用。在那種情況下,第一個腳本通常是一組DROP語句,然後是一組CREATE語句。</p> </p> <p>可以將ignore-failures選項設置為NONE(默認值),DROPS(忽略失敗的丟棄)或ALL(忽略所有失敗)。</p> </p> <p>每個語句都應用;或如果換行;腳本中根本沒有字符。你可以全局控制該腳本,也可以按腳本控制,如以下示例所示:</p> </p> <p><code>//1<br /> //2</p> <p></code></p> </p> <p>將分隔符腳本設置為@<a href="https://my.oschina.net/u/2432730" rel="nofollow noopener" target="_blank">@。</a>“將db-schema.sql的分隔符設置為;。</p> </p> <p>在此示例中,兩個測試數據腳本使用@@作為語句分隔符,而只有db-schema.sql使用;。此配置指定默認分隔符為@@,並覆蓋db-schema腳本的默認分隔符。</p> </p> <p>如果你需要比從XML名稱空間獲得更多控制權,則可以直接使用DataSourceInitializer並將其定義為應用程序中的組件。</p> </p> </p> <p>初始化依賴於數據庫的其他組件</p> </p> <p>大量應用程序(那些在Spring上下文啟動之後才使用數據庫的應用程序)可以使用數據庫初始化程序,而不會帶來更多麻煩。如果你的應用程序不是其中之一,則可能需要閱讀本節的其餘部分。</p> </p> <p>數據庫初始化程序依賴於DataSource實例,並運行其初始化回調中提供的腳本(類似於XML bean定義中的init方法,組件中的@PostConstruct方法或實現InitializingBean的組件中的afterPropertiesSet()方法)。如果其他bean依賴於同一數據源並在初始化回調中使用該數據源,則可能存在問題,因為數據尚未初始化。一個常見的例子是一個高速緩存,它會在應用程序啟動時急於初始化並從數據庫加載數據。</p> </p> <p>要解決此問題,你有兩個選擇:將高速緩存初始化策略更改為以後的階段,或者確保首先初始化數據庫初始化程序。</p> </p> <p>如果應用程序在你的控制之下,則更改緩存初始化策略可能很容易,否則就不那麼容易。有關如何實現這一點的一些建議包括:</p> </p> <p>使高速緩存在首次使用時延遲初始化,從而縮短了應用程序的啟動時間。讓你的緩存或初始化緩存的單獨組件實現Lifecycle或SmartLifecycle。當應用程序上下文啟動時,你可以通過設置其SmartStartup標誌來自動啟動SmartLifecycle,並且可以通過在封閉上下文中調用ConfigurableApplicationContext.start()來手動啟動Lifecycle。使用Spring ApplicationEvent或類似的自定義觀察者機制來觸發緩存初始化。 ContextRefreshedEvent在準備好使用時(在所有bean都初始化之後)總是由上下文發布,因此通常是一個有用的鉤子(默認情況下,SmartLifecycle的工作方式)。</p> </p> <p>確保首先初始化數據庫初始化程序也很容易。關於如何實現這一點的一些建議包括:</p> </p> <p>依靠Spring BeanFactory的默認行為,即按註冊順序初始化bean。通過採用XML配置中的一組元素(對應用程序模塊進行排序)的通用做法,並確保首先列出數據庫和數據庫初始化,可以輕鬆地進行安排。將數據源和使用它的業務組件分開,並通過將它們放在單獨的ApplicationContext實例中來控制啟動順序(例如,父上下文包含DataSource,子上下文包含業務組件)。這種結構在Spring Web應用程序中很常見,但可以更廣泛地應用。</p> </p> <p><h4>作者</h4> </p> <blockquote><p>個人從事金融行業,就職過易極付、思建科技、某網約車平台等重慶一流技術團隊,目前就職於某銀行負責統一支付系統建設。自身對金融行業有強烈的愛好。同時也實踐大數據、數據存儲、自動化集成和部署、分佈式微服務、響應式編程、人工智能等領域。同時也熱衷於技術分享創立公眾號和博客站點對知識體系進行分享。關注公眾號:青年IT男獲取最新技術文章推送!</p></blockquote> </p> <p>博客地址: http://youngitman.tech</p> </p> <p>CSDN:https://blog.csdn.net/liyong1028826685</p> </p> <p>微信公眾號:</p> <p><img src="https://images2-focus-opensocial.googleusercontent.com/gadgets/proxy?container=focus&gadget=a&no_expand=1&refresh=604800&url=https://static001.geekbang.org/infoq/0e/0e1ef0e34fb3ecf6b6c553250015a224.jpeg" alt="Spring 5 中文解析數據存儲篇-JDBC數據存儲(下) 1" title="Spring 5 中文解析數據存儲篇-JDBC數據存儲(下) 1"></img></p> </p> <p>技術交流群:</p> <p><img src="https://images2-focus-opensocial.googleusercontent.com/gadgets/proxy?container=focus&gadget=a&no_expand=1&refresh=604800&url=https://static001.geekbang.org/infoq/08/08a8ee7a2b0ac7122252c47d2b3a3b94.png" alt="Spring 5 中文解析數據存儲篇-JDBC數據存儲(下) 2" title="Spring 5 中文解析數據存儲篇-JDBC數據存儲(下) 2"></img></p> </p> <blockquote><p>該系列文章請關注微信公眾:青年IT男</p></blockquote> </p> <p>