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

3.7 做爲Java對象JDBC操做模型

org.springframework.jdbc.object包包含一些類,這些類使你以更加面向對象的方式訪問數據庫。例如,你能夠運行查詢並將結果做爲包含業務對象的列表返回,該業務對象的關聯列數據映射到業務對象的屬性。你還能夠運行存儲過程並運行update,delete和insert語句。html

許多Spring開發人員認爲,下面描述的各類RDBMS操做類(StoredProcedure類除外)一般能夠用直接的JdbcTemplate調用代替。一般,編寫直接在JdbcTemplate上調用方法的DAO方法(與將查詢封裝爲完整的類相對)更簡單。可是,若是經過使用RDBMS操做類得到可測量的價值,則應繼續使用這些類。
3.7.1 理解SqlQuery

SqlQuery是可重用的、線程安全的類,它封裝了SQL查詢。子類必須實現newRowMapper(..)方法以提供RowMapper實例,該實例能夠爲遍歷查詢執行期間建立的ResultSet所得到的每一行建立一個對象。不多直接使用SqlQuery類,由於MappingSqlQuery子類爲將行映射到Java類提供了更爲方便的實現。擴展SqlQuery的其餘實現是MappingSqlQueryWithParameters和UpdatableSqlQuery。java

3.7.2 使用MappingSqlQuery

MappingSqlQuery是可重用的查詢,其中具體的子類必須實現抽象的mapRow(..)方法,以將提供的ResultSet的每一行轉換爲指定類型的對象。如下示例顯示了一個自定義查詢,該查詢將t_actor關係中的數據映射到Actor類的實例:git

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時建立這些實例,就能夠將它們保留爲實例變量並能夠重用。下面的示例演示如何定義此類:spring

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便捷方法。相反,若是有一個查詢返回一個對象列表並採用其餘參數,則將使用其中一種執行方法,該方法採用以能夠變參數形式傳入的參數值數組。sql

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(..)方法均具備受保護的訪問權限,除了經過提供更嚴格類型的子類以外,還能夠防止使用。apache

繼承的sql屬性是RDBMS中存儲過程的名稱。編程

要爲StoredProcedure類定義參數,可使用SqlParameter或其子類之一。你必須在構造函數中指定參數名稱和SQL類型,如如下代碼片斷所示:api

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 and setBlobAsBytes
    • InputStream: getBlobAsBinaryStream and setBlobAsBinaryStream
  • CLOB

    • String: getClobAsString and setClobAsString
    • InputStream: getClobAsAsciiStream and setClobAsAsciiStream
    • Reader: getClobAsCharacterStream and setClobAsCharacterStream

下一個示例顯示瞭如何建立和插入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();
  1. 傳入lobHandler(在此示例中)爲普通的DefaultLobHandler。
  2. 使用setClobAsCharacterStream方法傳遞CLOB內容。
  3. 使用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;
        }
    });
  1. 使用方法getClobAsString檢索CLOB的內容。
  2. 使用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數據庫引擎提供支持。本地提供對HSQLDerby的支持。你還可使用可擴展的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

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>
  1. 從名爲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>
  1. 將分隔符腳本設置爲@@。
  2. 將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男
相關文章
相關標籤/搜索