前言
需要引入Spring JDBC模塊
<!--Spring JDBC--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency>
一、選擇JDBC數據庫訪問方式
- JdbcTemplate是經典的也是最流行的 Spring JDBC方法。
- NamedParameterJdbcTemplate包裝了一個JdbcTemplate來提供命名參數,而不是傳統的JDBC?占位符。當一個SQL語句有多個參數時,這種方法提供了更好的文檔和易用性。
- SimpleJdbcInsert 和 SimpleJdbcCall 將優化數據庫元數據,以限制必要的配置量。這種方法簡化了編碼,因此只需提供表或過程的名稱,並提供與列名匹配的參數映射。只有當數據庫提供足夠的元數據時,這才有效。如果數據庫不提供此元數據,則必須提供參數的顯式配置。
- RDBMS對象,包括MappingSqlQuery、SqlUpdate和StoredProcedure,要求您在初始化數據訪問層時創建可重用的線程安全對象。這種方法是在JDO Query之后建模的,在JDO Query中定義查詢字符串、聲明參數並編譯查詢。一旦您這樣做了,execute方法就可以用不同的參數值被多次調用。
二、Spring JDBC包目錄
Spring框架的JDBC模塊由四個不同的包組成:
org.springframework.jdbc.core
包含JdbcTemplate類及其各種回調接口,以及各種相關類。
子包org.springframework.jdbc.core.simple包含SimpleJdbcInsert和SimpleJdbcCall類。
子包org.springframework.jdbc.core.namedparam包含NamedParameterJdbcTemplate和相關的支持類。
org.springframework.jdbc.datasource
包含一個用於輕松訪問數據源的實用程序類和各種簡單的數據源實現,您可以使用這些實現在javaee容器外測試和運行未修改的JDBC代碼。
子包org.springframework.jdbc.datasource.embedded提供對使用Java數據庫引擎(如HSQL、H2和Derby)創建嵌入式數據庫的支持。
org.springframework.jdbc.object
包含將RDBMS查詢、更新和存儲過程表示為線程安全、可重用的對象的類。
org.springframework.jdbc.support
提供了SQLException轉換功能和一些實用程序類。在JDBC處理期間拋出的異常被定義在org.springframework.dao(在spring-tx中)。這意味着使用Spring JDBC抽象層的代碼不需要實現JDBC或RDBMS特定的錯誤處理。
三、使用JDBC核心類控制基本的JDBC處理和錯誤處理
本小節介紹如何使用JDBC核心類來控制基本的JDBC處理,包括錯誤處理。
使用JdbcTemplate
JdbcTemplate是JDBC核心包中的中心類。它處理資源的創建和釋放,這有助於避免常見錯誤,例如忘記關閉連接。它執行核心JDBC的基本任務(例如語句創建和執行),讓應用程序代碼提供SQL和提取結果。
- 運行SQL查詢
- 更新語句和存儲過程調用
- 對ResultSet實例執行迭代並提取返回的參數值。
- 捕獲JDBC異常並將其轉換為在org.springframework.dao
創建JdbcTemplate只需要將DataSource提供它就行。
下面提供了一些JdbcTemplate用法的示例:
查詢(Select)
獲取行數
int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);
使用一個綁定變量查詢
int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject( "select count(*) from t_actor where first_name = ?", Integer.class, "Joe");
查詢一個字符串
String lastName = this.jdbcTemplate.queryForObject( "select last_name from t_actor where id = ?", new Object[]{1212L}, String.class);
查找並填充單個實體對象
Actor actor = this.jdbcTemplate.queryForObject( "select first_name, last_name from t_actor where id = ?", new Object[]{1212L}, new RowMapper<Actor>() { public Actor mapRow(ResultSet rs, int rowNum) throws SQLException { Actor actor = new Actor(); actor.setFirstName(rs.getString("first_name")); actor.setLastName(rs.getString("last_name")); return actor; } });
查找並填充多個實體對象
List<Actor> actors = this.jdbcTemplate.query( "select first_name, last_name from t_actor", new RowMapper<Actor>() { public Actor mapRow(ResultSet rs, int rowNum) throws SQLException { Actor actor = new Actor(); actor.setFirstName(rs.getString("first_name")); actor.setLastName(rs.getString("last_name")); return actor; } });
如果最后兩段代碼存在於同一個應用程序中,那么有必要刪除兩個RowMapper匿名內部類中的一個,並將它們提取到單個類(通常是靜態嵌套類),然后DAO方法可以根據需要引用該類。例如,最好按以下方式編寫前面的代碼片段:
public List<Actor> findAllActors() { return this.jdbcTemplate.query( "select first_name, last_name from t_actor", new ActorMapper()); } private static final class ActorMapper implements RowMapper<Actor> { public Actor mapRow(ResultSet rs, int rowNum) throws SQLException { Actor actor = new Actor(); actor.setFirstName(rs.getString("first_name")); actor.setLastName(rs.getString("last_name")); return actor; } }
更新(INSERT, UPDATE, DELETE)
可以使用update(..)方法執行插入、更新和刪除操作。參數值通常變量參數或對象數組。
插入一條數據:
this.jdbcTemplate.update( "insert into t_actor (first_name, last_name) values (?, ?)", "Leonor", "Watling");
更新一條數據
this.jdbcTemplate.update( "update t_actor set last_name = ? where id = ?", "Banjo", 5276L);
刪除一條數據
this.jdbcTemplate.update( "delete from actor where id = ?", Long.valueOf(actorId));
JdbcTemplate其他操作
可以使用execute(..)方法運行任意SQL,該方法通常用於DDL語句。它有很多重載方法,這些變量采用回調接口、綁定變量數組等。
創建一個表:
this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
調用存儲過程
this.jdbcTemplate.update( "call SUPPORT.REFRESH_ACTORS_SUMMARY(?)", Long.valueOf(unionId));
JdbcTemplate實踐
JdbcTemplate類的實例在配置后是線程安全的。這一點很重要,因為這意味着您可以配置JdbcTemplate的單個實例,然后將此共享引用安全地注入到多個dao中。JdbcTemplate是有狀態的,因為它維護對數據源的引用,但這種狀態不是會話狀態。
使用JdbcTemplate類(以及相關聯的NamedParameterJdbcTemplate類)的一個常見做法是在Spring配置文件中配置一個數據源,然后依賴關系將該共享數據源bean注入到DAO類中。JdbcTemplate是在數據源的setter中創建的。這將導致類似於以下內容的DAO:
public class JdbcCorporateEventDao implements CorporateEventDao { private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } // JDBC-backed implementations of the methods on the CorporateEventDao follow... }
以下示例顯示了相應的XML配置:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <bean id="corporateEventDao" class="com.example.JdbcCorporateEventDao"> <property name="dataSource" ref="dataSource"/> </bean> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="${jdbc.driverClassName}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <context:property-placeholder location="jdbc.properties"/> </beans>
顯式配置的另一種選擇是使用組件掃描和注釋支持進行依賴注入。在這種情況下,您可以用@Repository注解類(這使得它成為組件掃描的候選對象),並用@Autowired為DataSource setter方法添加注解。
@Repository public class JdbcCorporateEventDao implements CorporateEventDao { @Autowired private JdbcTemplate jdbcTemplate; }
如果您使用Spring的JdbcDaoSupport 類,並且您的各種JDBC支持的DAO類都是從它擴展的,那么您的子類從JdbcDaoSupport 類繼承一個setDataSource(..)方法。您可以選擇是否從此類繼承。JdbcDaoSupport類只是為了方便起見而提供的。
無論您選擇使用上述哪種模板初始化樣式,每次運行SQL時都很少需要創建JdbcTemplate類的新實例。一旦配置好,JdbcTemplate實例就是線程安全的。如果您的應用程序訪問多個數據庫,那么您可能需要多個JdbcTemplate實例,這需要多個數據源,並且隨后需要多個不同配置的JdbcTemplate實例。
使用NamedParameterJdbcTemplate
NamedParameterJdbcTemplate類添加了對使用命名參數編程的JDBC語句的支持(這也是它與JdbcTemplate最大的區別),而不是僅使用經典占位符('?')。NamedParameterJdbcTemplate類包裝了一個JdbcTemplate並委托給包裝好的JdbcTemplate來完成它的大部分工作。
基於MapSqlParameterSource的NamedParameterJdbcTemplate
@Autowired private NamedParameterJdbcTemplate namedParameterJdbcTemplate; public int countOfActorsByFirstName(String firstName) { String sql = "select count(*) from T_ACTOR where first_name = :first_name"; SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName); return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class); }
基於類型Map的NamedParameterJdbcTemplate
@Autowired private NamedParameterJdbcTemplate namedParameterJdbcTemplate; public int countOfActorsByFirstName(String firstName) { String sql = "select count(*) from T_ACTOR where first_name = :first_name"; Map<String, String> namedParameters = Collections.singletonMap("first_name", firstName); return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class); }
與NamedParameterJdbcTemplate(存在於同一個Java包中)相關的一個很好的特性是SqlParameterSource接口。SqlParameterSource是NamedParameterJdbcTemplate的命名參數值源。MapSqlParameterSource類是一個簡單的實現,它是一個java.util.Map,其中鍵是參數名,值是參數值。
下面的示例是一個典型的JavaBean:
public class Actor { private Long id; private String firstName; private String lastName; public String getFirstName() { return this.firstName; } public String getLastName() { return this.lastName; } public Long getId() { return this.id; } // setters omitted... }
以下示例使用NamedParameterJdbcTemplate返回JavaBean中的成員計數:
@Autowired private NamedParameterJdbcTemplate namedParameterJdbcTemplate; public int countOfActors(Actor exampleActor) { // notice how the named parameters match the properties of the above 'Actor' class String sql = "select count(*) from T_ACTOR where first_name = :firstName and last_name = :lastName"; SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(exampleActor); return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class); }
記住NamedParameterJdbcTemplate類包裝了一個經典的JdbcTemplate模板。如果您需要訪問包裝好的JdbcTemplate實例來訪問只存在於JdbcTemplate類中的功能,那么可以使用getJdbcOperations()方法通過JdbcOperations接口訪問包裝好的JdbcTemplate。
使用SQLExceptionTranslator
SQLExceptionTranslator是一個接口,實現它的類可以在SQLExceptions和Spring自己的類之間進行轉換org.springframework.dao.DataAccessException。
SQLErrorCodeSQLExceptionTranslator是默認情況下使用的SQLExceptionTranslator的實現。此實現使用特定的供應商代碼。它比SQLState實現更精確。
基於JavaBean類型的名為SQLErrorCodes的錯誤代碼類是由SQLErrorCodesFactory創建並填充,它是一個根據名為sql-error-codes.xml的配置文件的內容創建SQLErrorCodes的工廠。xml文件由供應商代碼填充,並基於從DatabaseMetaData獲取的DatabaseProductName。將使用您正在使用的實際數據庫的代碼。
SQLErrorCodeSQLExceptionTranslator按以下順序應用匹配規則:
- 由子類實現的任何自定義轉換。通常,使用提供的具體SQLErrorCodeSQLExceptionTranslator,因此此規則不適用。它只適用於實際提供了子類實現的情況。
- 作為SQLErrorCodes類的customSqlExceptionTranslator屬性提供的SQLExceptionTranslator接口的任何自定義實現。
- 將搜索CustomSQLErrorCodesTranslation類(為SQLErrorCodes類的customTranslations屬性提供)的實例列表以查找匹配項。
- 應用錯誤代碼匹配。
- 使用回退轉換器。SQLExceptionSubclassTranslator是默認的回退轉換器。如果此轉換不可用,下一個后備轉換器是SQLStateSQLExceptionTranslator。
你可以擴展SQLErrorCodeSQLExceptionTranslator,如下例所示:
public class CustomSQLErrorCodesTranslator extends SQLErrorCodeSQLExceptionTranslator { protected DataAccessException customTranslate(String task, String sql, SQLException sqlex) { if (sqlex.getErrorCode() == -12345) { return new DeadlockLoserDataAccessException(task, sqlex); } return null; } }
在前面的例子中,特定的錯誤代碼(-12345)被轉換,而其他錯誤則由默認的轉換器實現來轉換。要使用此自定義轉換器,必須通過setExceptionTranslator方法將其傳遞給JdbcTemplate,並且必須將此JdbcTemplate用於需要此轉換器的所有數據訪問處理。以下示例顯示如何使用此自定義轉換器:
private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { // create a JdbcTemplate and set data source this.jdbcTemplate = new JdbcTemplate(); this.jdbcTemplate.setDataSource(dataSource); // create a custom translator and set the DataSource for the default translation lookup CustomSQLErrorCodesTranslator tr = new CustomSQLErrorCodesTranslator(); tr.setDataSource(dataSource); this.jdbcTemplate.setExceptionTranslator(tr); } public void updateShippingCharge(long orderId, long pct) { // use the prepared JdbcTemplate for this update this.jdbcTemplate.update("update orders" + " set shipping_charge = shipping_charge * ? / 100" + " where id = ?", pct, orderId); }
獲取自增的ID
update()方法支持檢索數據庫生成的主鍵。這種支持是jdbc3.0標准的一部分。該方法將PreparedStatementCreator作為其第一個參數,這是指定所需insert語句的方式。另一個參數是KeyHolder,它包含從更新成功返回時生成的主鍵。以下示例適用於Oracle,但可能不適用於其他平台:
final String INSERT_SQL = "insert into my_test (name) values(?)"; final String name = "Rob"; KeyHolder keyHolder = new GeneratedKeyHolder(); jdbcTemplate.update( new PreparedStatementCreator() { public PreparedStatement createPreparedStatement(Connection connection) throws SQLException { PreparedStatement ps = connection.prepareStatement(INSERT_SQL, new String[] {"id"}); ps.setString(1, name); return ps; } }, keyHolder); // keyHolder.getKey() now contains the generated key
四、控制數據庫連接
使用DriverManagerDataSource
Spring通過數據源獲得到數據庫的連接。數據源是JDBC規范的一部分,是一個通用的連接工廠。應用程序池管理允許應用程序對事務池問題和代碼池進行隱藏。
當你使用Spring的JDBC層時,你可以從JNDI獲取數據源,也可以使用第三方提供的連接池實現來配置自己的數據源。流行的實現是DBCP和C3P0。Spring發行版中的實現僅用於測試目的,不提供池。
配置DriverMangerDataSource如下:
DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); dataSource.setUrl("jdbc:hsqldb:hsql://localhost:"); dataSource.setUsername("sa"); dataSource.setPassword("");
XML的配置如下:
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="${jdbc.driverClassName}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <context:property-placeholder location="jdbc.properties"/>
下面兩個示例展示了DBCP和C3P0的基本連接和配置
DBCP配置:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="${jdbc.driverClassName}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <context:property-placeholder location="jdbc.properties"/>
C3P0配置:
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"> <property name="driverClass" value="${jdbc.driverClassName}"/> <property name="jdbcUrl" value="${jdbc.url}"/> <property name="user" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <context:property-placeholder location="jdbc.properties"/>
使用DataSourceUtils
DataSourceUtils類是一個方便而強大的輔助類,它提供靜態方法來從JNDI獲取連接,並在必要時關閉連接。它支持線程綁定連接,例如DataSourceTransactionManager。
實現SmartDataSource
SmartDataSource接口應該由可以提供到關系數據庫的連接的類來實現。它擴展了DataSource接口。
繼承AbstractDataSource
AbstractDataSource是Spring數據源實現的一個抽象基類。它實現了所有數據源實現通用的代碼。如果編寫自己的數據源實現,則應該擴展AbstractDataSource類。
使用SingleConnectionDataSource
SingleConnectionDataSource類是SmartDataSource接口的實現,它包裝了一個在每次使用后未關閉的連接。這不支持多線程。
如果任何客戶端代碼在假定池連接的情況下調用close(如使用持久性工具時),則應將suppressClose屬性設置為true。此設置返回包裝物理連接的關閉禁止代理。請注意,你不能再將其強制轉換為本機Oracle連接或類似對象。
SingleConnectionDataSource主要是一個測試類。例如,它與一個簡單的JNDI環境結合使用,可以在應用服務器外部輕松測試代碼。與DriverManager DataSource不同,它始終重用相同的連接,避免過度創建物理連接。
使用DriverManagerDataSource
DriverManagerDataSource類是標准數據源接口的實現,該接口通過bean屬性配置普通JDBC驅動程序,並每次返回一個新連接。
這個實現對於java EE容器之外的測試和獨立環境非常有用,可以作為spring IoC容器中的數據源bean,也可以與簡單的JNDI環境結合使用。任何支持數據源的持久性代碼都可以工作。使用JavaBean風格的連接池(比如commons-dbcp)非常容易,即使在測試環境中,使用這樣的連接池幾乎總是優於DriverManagerDataSource。
使用TransactionAwareDataSourceProxy
TransactionWareDatasourceProxy是目標數據源的代理。代理將目標數據源包裝起來,以增加對Spring管理事務的感知。在這方面,它類似於事務性JNDI數據源,由javaEE服務器提供。
很少需要使用這個類,除非已經存在的代碼必須被調用並傳遞給標准的JDBC數據源接口實現。在這種情況下,你仍然可以使用這些代碼,同時讓這些代碼參與Spring管理的事務。通常情況下,最好使用更高級別的資源管理抽象來編寫自己的新代碼,例如JdbcTemplate或DataSourceUtils。
使用DataSourceTransactionManager
DataSourceTransactionManager類是單個JDBC數據源的PlatformTransactionManager實現。它將指定數據源的JDBC連接綁定到當前正在執行的線程,可能允許每個數據源有一個線程連接。
通過檢索JDBC連接需要應用程序代碼DataSourceUtils.getConnection(數據源)而不是javaEE的標准DataSource.getConnection。它不受約束地拋出org.springframework.dao異常而不是選中的SQLExceptions。所有的框架類(比如JdbcTemplate)都隱式地使用這個策略。如果不與此事務管理器一起使用,則查找策略的行為與普通策略完全相同。因此,它可以在任何情況下使用。
DataSourceTransactionManager類支持自定義的隔離級別和超時,這些級別和超時將應用於適當的JDBC語句查詢超時。為了支持后者,應用程序代碼必須使用JdbcTemplate或調用DataSourceUtils.applyTransactionTimeout(..)方法。
在單數據源情況下,可以使用此實現而不是JtaTransactionManager,因為它不需要容器支持JTA。如果您堅持所需的連接查找模式,那么在這兩者之間切換只是一個配置問題。JTA不支持自定義隔離級別。
五、JDBC批量操作
如果你將多個調用批處理到同一個准備好的語句中,大多數JDBC驅動程序都可以提高性能。通過將更新分組為批,可以限制到數據庫的往返次數。
JdbcTemplate批量操作
通過實現BatchPreparedStatementSetter的兩個方法,並將該實現作為batchUpdate方法調用中的第二個參數傳入,從而完成JdbcTemplate批處理。可以使用getBatchSize方法提供當前批的大小。可以使用setValues方法為准備好的語句的參數設置值。調用此方法的次數是在getBatchSize調用中指定的次數。以下示例基於列表中的條目更新actor表,整個列表用作批處理:
public class JdbcActorDao implements ActorDao { @Autowired private JdbcTemplate jdbcTemplate; public int[] batchUpdate(final List<Actor> actors) { return this.jdbcTemplate.batchUpdate( "update t_actor set first_name = ?, last_name = ? where id = ?", new BatchPreparedStatementSetter() { public void setValues(PreparedStatement ps, int i) throws SQLException { ps.setString(1, actors.get(i).getFirstName()); ps.setString(2, actors.get(i).getLastName()); ps.setLong(3, actors.get(i).getId().longValue()); } public int getBatchSize() { return actors.size(); } }); } // ... additional methods }
如果處理更新流或從文件中讀取數據流,則可能有一個處理大小,但最后一批可能沒有該數量的條目。在本例中,你可以使用InterruptibleBatchPreparedStatementSetter接口,該接口允許你在輸入源耗盡后中斷批處理。isBatchExhausted方法允許你發出批處理結束的信號。
對象列表的批處理
JdbcTemplate和NamedParameterJdbcTemplate都提供了提供批處理更新的另一種方法。不是實現特殊的批處理接口,而是以列表的形式提供調用中的所有參數值。框架循環這些值並使用內部准備好的語句設置器。API會有所不同,具體取決於你是否使用命名參數。對於命名參數,你提供一個SqlParameterSource數組,批處理的每個成員一個條目。你可以使用SqlParameterSourceUtils.createBatch創建這個數組的方便方法,傳入一個bean風格的對象數組(getter方法對應的參數)、key為字符串的Map實例(相應的參數作為值),或者兩者的混合。
以下示例顯示了使用命名參數的批處理更新:
public class JdbcActorDao implements ActorDao { @Autowired private NamedParameterTemplate namedParameterJdbcTemplate; public int[] batchUpdate(List<Actor> actors) { return this.namedParameterJdbcTemplate.batchUpdate( "update t_actor set first_name = :firstName, last_name = :lastName where id = :id", SqlParameterSourceUtils.createBatch(actors)); } // ... additional methods }
對於使用經典語句的SQL語句?占位符,則傳入一個包含更新值的對象數組的列表。對於SQL語句中的每個占位符,此對象數組必須有一個條目,並且它們的順序必須與在SQL語句中定義的順序相同。
下面的示例與前面的示例相同,只是它使用了經典的JDBC?占位符:
public class JdbcActorDao implements ActorDao { @Autowired private JdbcTemplate jdbcTemplate; public int[] batchUpdate(final List<Actor> actors) { List<Object[]> batch = new ArrayList<Object[]>(); for (Actor actor : actors) { Object[] values = new Object[] { actor.getFirstName(), actor.getLastName(), actor.getId()}; batch.add(values); } return this.jdbcTemplate.batchUpdate( "update t_actor set first_name = ?, last_name = ? where id = ?", batch); } // ... additional methods }
我們前面描述的所有批處理更新方法都返回一個int數組,其中包含每個批處理條目的受影響行數。此計數由JDBC驅動程序報告。如果計數不可用,JDBC驅動程序將返回一個值-2。
具有多個批次的批處理操作
前面的批處理更新示例每一批處理的量太大,以至於需要將它們拆分為幾個較小的批操作。你可以通過多次調用batchUpdate方法來使用前面提到的方法來實現這一點,但是現在有了一個更方便的方法。除了SQL語句外,此方法還需要一個包含參數的對象集合、每個批處理要進行的更新次數以及一個ParameterizedPreparedStatementSetter 來設置准備語句的參數值。框架循環提供的值,並將更新調用分成指定大小的批。
以下示例顯示了使用批大小為100的批處理更新:
public class JdbcActorDao implements ActorDao { @Autowired private JdbcTemplate jdbcTemplate; public int[][] batchUpdate(final Collection<Actor> actors) { int[][] updateCounts = jdbcTemplate.batchUpdate( "update t_actor set first_name = ?, last_name = ? where id = ?", actors, 100, new ParameterizedPreparedStatementSetter<Actor>() { public void setValues(PreparedStatement ps, Actor argument) throws SQLException { ps.setString(1, argument.getFirstName()); ps.setString(2, argument.getLastName()); ps.setLong(3, argument.getId().longValue()); } }); return updateCounts; } // ... additional methods }
此調用的批處理更新方法返回一個int數組,其中包含每個批處理的數組項,以及每次更新受影響行數的數組。第一級數組的長度表示執行的批處理數,第二級數組的長度表示該批處理中的更新次數。每個批處理中的更新數量應為為所有批處理提供的批大小(最后一個可能較少的批除外),具體取決於提供的更新對象的總數。每個update語句的更新計數是JDBC驅動程序報告的。如果計數不可用,JDBC驅動程序將返回一個值-2。
六、用SimpleJdbc類簡化JDBC操作
SimpleJdbcInsert和SimpleJdbcCall類通過利用JDBC驅動程序檢索的數據庫元數據來提供簡化的配置。
使用SimpleJdbcInsert插入數據
SimpleJdbcInsert包含最少的配置選項。你應該在數據訪問層的初始化方法中實例化SimpleJdbcInsert。對於本例,初始化方法是setDataSource方法。你不需要將SimpleJdbcInsert類的子類化。相反,您你以創建一個新實例並使用withTableName方法設置表名。此類的配置方法遵循simplejbcinsert實例的鏈式操作,允許你鏈式調用所有配置方法。以下示例僅使用一種配置方法:
public class JdbcActorDao implements ActorDao { private JdbcTemplate jdbcTemplate; private SimpleJdbcInsert insertActor; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); this.insertActor = new SimpleJdbcInsert(dataSource).withTableName("t_actor"); } public void add(Actor actor) { Map<String, Object> parameters = new HashMap<String, Object>(3); parameters.put("id", actor.getId()); parameters.put("first_name", actor.getFirstName()); parameters.put("last_name", actor.getLastName()); insertActor.execute(parameters); } // ... additional methods }
這里使用的execute方法需要一個java.util.Map作為它唯一的參數。這里需要注意的一點是,用於映射的鍵必須與數據庫中定義的表的列名相匹配。這是因為我們讀取元數據來構造實際的insert語句。
使用SimpleJdbcInsert獲取自增ID
這個示例使用與前一個示例相同的insert,但是它沒有傳遞id,而是檢索自動生成的鍵並將其設置在新的Actor對象上。當它創建SimpleJdbcInsert時,除了指定表名之外,它還使用usingGeneratedKeyColumns方法指定生成的鍵列的名稱。
public class JdbcActorDao implements ActorDao { private JdbcTemplate jdbcTemplate; private SimpleJdbcInsert insertActor; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); this.insertActor = new SimpleJdbcInsert(dataSource) .withTableName("t_actor") .usingGeneratedKeyColumns("id"); } public void add(Actor actor) { Map<String, Object> parameters = new HashMap<String, Object>(2); parameters.put("first_name", actor.getFirstName()); parameters.put("last_name", actor.getLastName()); Number newId = insertActor.executeAndReturnKey(parameters); actor.setId(newId.longValue()); } // ... additional methods }
當使用第二種方法運行insert時,主要的區別是不向Map實例添加id,而是調用executeAndReturnKey方法。
這將返回一個包含數值類型的java.lang.Number對象實例。你不能依賴所有數據庫來返回特定的Java類。java.lang.Number是你可以依賴的基類。如果有多個自動生成的列或生成的值不是數字,則可以使用從executeAndReturnKeyHolder方法返回的KeyHolder。
為SimpleJdbcInsert指定列
可以通過使用usingColumns方法指定列名列表來限制插入的列,如下例所示:
public class JdbcActorDao implements ActorDao { private JdbcTemplate jdbcTemplate; private SimpleJdbcInsert insertActor; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); this.insertActor = new SimpleJdbcInsert(dataSource) .withTableName("t_actor") .usingColumns("first_name", "last_name") .usingGeneratedKeyColumns("id"); } public void add(Actor actor) { Map<String, Object> parameters = new HashMap<String, Object>(2); parameters.put("first_name", actor.getFirstName()); parameters.put("last_name", actor.getLastName()); Number newId = insertActor.executeAndReturnKey(parameters); actor.setId(newId.longValue()); } // ... additional methods }
使用SqlParameterSource提供參數值
使用Map來提供參數值很好,但它不是使用最方便的類。Spring提供了SqlParameterSource接口的兩個實現,你可以改用它們。第一個是BeanPropertySqlParameterSource,如果你有一個JavaBean兼容的類包含你的值,那么這是一個非常方便的類。它使用相應的getter方法來提取參數值。以下示例演示如何使用BeanPropertySqlParameterSource:
public class JdbcActorDao implements ActorDao { private JdbcTemplate jdbcTemplate; private SimpleJdbcInsert insertActor; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); this.insertActor = new SimpleJdbcInsert(dataSource) .withTableName("t_actor") .usingGeneratedKeyColumns("id"); } public void add(Actor actor) { SqlParameterSource parameters = new BeanPropertySqlParameterSource(actor); Number newId = insertActor.executeAndReturnKey(parameters); actor.setId(newId.longValue()); } // ... additional methods }
另一個選項是MapSqlParameterSource,它類似於Map,但提供了一個更方便的addValue方法,可以進行鏈式調用。
public class JdbcActorDao implements ActorDao { private JdbcTemplate jdbcTemplate; private SimpleJdbcInsert insertActor; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); this.insertActor = new SimpleJdbcInsert(dataSource) .withTableName("t_actor") .usingGeneratedKeyColumns("id"); } public void add(Actor actor) { SqlParameterSource parameters = new MapSqlParameterSource() .addValue("first_name", actor.getFirstName()) .addValue("last_name", actor.getLastName()); Number newId = insertActor.executeAndReturnKey(parameters); actor.setId(newId.longValue()); } // ... additional methods }
使用SimpleJdbcCall調用存儲過程
SimpleJdbcCall類使用數據庫中的元數據來查找輸入和輸出參數的名稱,這樣就不必顯式地聲明它們。如果您願意聲明參數,或者您的參數(如數組或結構)沒有自動映射到Java類,則可以聲明參數。示例是一個簡單的過程,它只從MySQL數據庫返回VARCHAR和DATE格式的標量值。示例過程讀取指定的actor條目,並以out參數的形式返回first_name、last_name和birth_date列。
CREATE PROCEDURE read_actor ( IN in_id INTEGER, OUT out_first_name VARCHAR(100), OUT out_last_name VARCHAR(100), OUT out_birth_date DATE) BEGIN SELECT first_name, last_name, birth_date INTO out_first_name, out_last_name, out_birth_date FROM t_actor where id = in_id; END;
in_id 包含你要查找的id,out參數返回從表中讀取的數據。
你可以用類似於聲明SimpleJdbcInsert的方式聲明SimpleJdbcCall。你應該在數據訪問層的初始化方法中實例化和配置類。與StoredProcedure類相比,你不需要創建子類,也不需要聲明可以在數據庫元數據中查找的參數。
下面的SimpleJDBCall配置示例使用前面的存儲過程(除了數據源之外,唯一的配置選項是存儲過程的名稱):
public class JdbcActorDao implements ActorDao { private JdbcTemplate jdbcTemplate; private SimpleJdbcCall procReadActor; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); this.procReadActor = new SimpleJdbcCall(dataSource) .withProcedureName("read_actor"); } public Actor readActor(Long id) { SqlParameterSource in = new MapSqlParameterSource() .addValue("in_id", id); Map out = procReadActor.execute(in); Actor actor = new Actor(); actor.setId(id); actor.setFirstName((String) out.get("out_first_name")); actor.setLastName((String) out.get("out_last_name")); actor.setBirthDate((Date) out.get("out_birth_date")); return actor; } // ... additional methods }
提示:這里就先簡單介紹下,更加詳細的內容可以參考官方文檔(非不得已的情況下建議不要使用存儲過程)。
如何定義SqlParameters
要為SimpleJdbc類和RDBMS操作類定義參數,可以使用SqlParameter或其子類之一。為此,通常在構造函數中指定參數名和SQL類型。SQL類型是通過使用java.sql.Types常量。
new SqlParameter("in_id", Types.NUMERIC), new SqlOutParameter("out_first_name", Types.VARCHAR),
七、將JDBC操作建模為Java對象
org.springframework.jdbc.object包含你以更面向對象的方式訪問數據庫的類。例如,您可以執行查詢並將結果作為一個列表返回,該列表包含業務對象,列數據映射到業務對象的屬性。你還可以運行存儲過程和運行update、delete和insert語句。
許多Spring開發人員認為直接用JdbcTemplate來編寫一個DAO方法更方便(除非你認為有用,可以去了解下)。
了解SqlQuery
SqlQuery是一個可重用的線程安全類,它封裝了SQL查詢。子類必須實現newRowMapper(..)方法,以提供一個RowMapper實例,該實例可以為每行創建一個對象,該對象是通過迭代執行查詢期間創建的ResultSet獲得的。SqlQuery類很少直接使用,因為MappingSqlQuery子類為將行映射到Java類提供了更方便的實現。擴展SqlQuery的其他實現包括MappingSqlQueryWithParameters和UpdatableSqlQuery。
八、參數和數據值處理的常見問題
在Spring框架的JDBC所提供的不同方法中,存在參數和數據值的常見問題。本小節介紹如何解決這些問題。
為參數提供SQL類型信息
通常,Spring根據傳入的參數類型確定參數的SQL類型。可以顯式地提供設置參數值時要使用的SQL類型。這有時是正確設置空值所必需的。
您可以通過多種方式提供SQL類型信息:
- 可以使用SqlParameterValue類包裝需要此附加信息的參數值。為此,為每個值創建一個新實例,並在構造函數中傳入SQL類型和參數值。也可以為數值提供可選的縮放參數。
- 對於使用命名參數的方法,可以使用SqlParameterSource類、BeanPropertySqlParameterSource或MapSqlParameterSource。為這兩個方法注冊的SQL參數的任何類型的值。
處理BLOB和CLOB對象
你可以在數據庫中存儲圖像、其他二進制數據和大塊文本。這些大對象對於二進制數據稱為blob(二進制大對象),對於字符數據稱為clob(字符大對象)。在Spring中,您可以通過直接使用JdbcTemplate來處理這些大型對象,也可以在使用RDBMS對象和SimpleJdbc類提供的高級抽象時處理這些大對象。所有這些方法都使用LobHandler接口的實現來實際管理LOB(大對象)數據。LobHandler通過getLobCreator方法提供對LobCreator類的訪問,該類用於創建要插入的新LOB對象。
LobCreator和LobHandler為LOB輸入和輸出提供以下支持:
- BLOB
- byte[]: getBlobAsBytes 和 setBlobAsBytes
- InputStream: getBlobAsBinaryStream 和 setBlobAsBinaryStream
- CLOB
- String: getClobAsString 和 setClobAsString
- InputStream: getClobAsAsciiStream 和 setClobAsAsciiStream
- Reader: getClobAsCharacterStream 和 setClobAsCharacterStream
下一個示例演示如何創建和插入BLOB。稍后我們將演示如何從數據庫中讀回它。
此示例使用JdbcTemplate和AbstractLobCreatingPreparedStatementCallback的實現。它實現了一個方法setValues。此方法提供了一個LobCreator,用於設置sqlinsert語句中LOB列的值。
對於本例,我們假設存在一個變量lobHandler,它已經設置為DefaultLobHandler的實例。通常通過依賴注入來設置該值(當然也可以直接new 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) { 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();
傳入lobHandler(在本例中)是一個普通的DefaultLobHandler。
使用方法setClobAsCharacterStream傳入CLOB的內容。
使用方法setBlobAsBinaryStream傳入BLOB的內容。
如果對DefaultLobHandler.getLobCreator()返回的LobCreator調用setBlobAsBinaryStream、SetClobasAsiIStream或setClobAsCharacterStream方法,你可以選擇為contentLength參數指定負值。如果指定的內容長度為負,則DefaultLobHandler將使用不帶長度參數的set stream方法的jdbc4.0變體。否則,它將指定的長度傳遞給驅動程序。
從數據庫讀取LOB數據同樣使用具有相同實例變量lobHandler和對DefaultLobHandler的引用的JdbcTemplate。下面的示例演示如何執行此操作:
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; } });
使用getClobAsString方法檢索CLOB的內容。
使用getBlobAsBytes方法檢索BLOB的內容。
提示:clob和blob字段
(1) 不同數據庫中對應clob,blob的類型如下:
MySQL中:clob對應text,blob對應blob
DB2/Oracle中:clob對應clob,blob對應blob
(2) domain中對應的類型:
clob對應String,blob對應byte[]
clob對應java.sql.Clob,blob對應java.sql.Blob
(3) hibernate配置文件中對應類型:
clob-->clob ,blob-->binary
也可以直接使用數據庫提供類型,例如:oracle.sql.Clob,oracle.sql.Blob
傳入in子句的值列表
SQL標准允許根據包含變量值列表的表達式選擇行。一個典型的例子是select * from T_ACTOR where id in(1,2,3)。JDBC標准對准備好的語句不直接支持此變量列表。不能聲明可變數量的占位符。您需要一系列具有所需數量占位符的變體,或者需要在知道需要多少占位符后動態生成SQL字符串。NamedParameterJdbcTemplate和JdbcTemplate中提供的命名參數支持采用后一種方法。可以將值作為java.util.List基本體對象。此列表用於在語句執行期間插入所需的占位符並傳入值。
除了值列表中的基本值外,還可以創建java.util.List對象數組的。此列表可以支持為in子句定義多個表達式,例如 select * from T_ACTOR where(id,last_name)in((1,'Johnson'),(2,'Harrop'\))。當然,這需要你的數據庫支持此語法。
傳遞許多值時要小心。JDBC標准並不保證可以為in-expression列表使用100個以上的值。各種數據庫都超過了這個數字,但它們通常對允許的值數量有一個硬限制。例如,Oracle的限制是1000。
處理存儲過程調用的復雜類型
九、嵌入式數據庫支持
org.springframework.jdbc.datasource.embedded提供對嵌入式Java數據庫引擎的支持。對HSQL、H2和Derby的支持是本機提供的。您還可以使用可擴展的API來插入新的嵌入式數據庫類型和數據源實現。
為什么使用嵌入式數據庫
嵌入式數據庫在項目的開發階段非常有用,因為它具有輕量級的特性。其優點包括易於配置、快速啟動、可測試性以及在開發過程中快速改進SQL的能力。
使用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 創建,同時為嵌入式數據庫分配一個唯一生成的名稱。嵌入式數據庫被放到Spring IoC容器中,可以根據需要注入到數據訪問對象中。
以編程方式創建嵌入式數據庫
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通過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(); } }
選擇嵌入式數據庫類型
本小節介紹如何從Spring支持的三個嵌入式數據庫中選擇一個。
使用HSQL
Spring支持hsql1.8.0及更高版本。如果沒有顯式指定類型,HSQL是默認的嵌入式數據庫。要顯式指定HSQL,請將嵌入式數據庫標記的type屬性設置為HSQL。如果使用構建器API,請使用EmbeddedDatabaseType.HSQL。
使用H2
Spring支持H2數據庫。要啟用H2,請將嵌入式數據庫標記的type屬性設置為H2。如果使用builder API,請調用setType(EmbeddedDatabaseType.H2)方法。
使用Derby
Spring支持ApacheDerby10.5及更高版本。要啟用Derby,請將嵌入式數據庫標記的type屬性設置為Derby。如果使用構建器API,請使用EmbeddedDatabaseType.DERBY。
用嵌入式數據庫測試數據訪問邏輯
嵌入式數據庫提供了一種測試數據訪問代碼的輕量級方法。當嵌入式數據庫不需要跨測試類重用時,可以單獨new一個實例。但是,如果希望創建在測試類中共享的嵌入式數據庫,請考慮使用Spring TestContext Framework 框架,並將嵌入式數據庫配置為Spring ApplicationContext中的bean。以下列表顯示了測試模板:
public class DataAccessIntegrationTestTemplate { private EmbeddedDatabase db; @Before 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( /* ... */ ); } @After public void tearDown() { db.shutdown(); } }
為嵌入式數據庫生成唯一名稱
如果開發團隊的測試套件無意中嘗試重新創建同一數據庫的其他實例,那么在使用嵌入式數據庫時經常會遇到錯誤。如果一個XML配置文件或@configuration類負責創建一個嵌入式數據庫,然后在同一個測試套件(即在同一個JVM進程中)內的多個測試場景中重用相應的配置,則很容易發生這種情況。
這些錯誤的根本原因是Spring的EmbeddedDatabaseFactory(由<jdbc:embedded-database>元素和EmbeddedDatabaseBuilder配置類)將嵌入式數據庫的名稱設置為testdb(如果沒有另外指定)。對於<jdbc:embedded-database>,通常為嵌入式數據庫分配一個與bean的id相等的名稱(類似於dataSource)。因此,后續創建嵌入式數據庫的嘗試不會產生新的數據庫。相反,相同的JDBC連接URL被重用,並且嘗試創建新的嵌入式數據庫實際上指向有相同配置創建的現有嵌入式數據庫。
為了解決這個常見問題,springframework4.2支持為嵌入式數據庫生成唯一的名稱。要使用生成的名稱,請使用以下選項之一。
- EmbeddedDatabaseFactory.setGenerateUniqueDatabaseName()
- EmbeddedDatabaseBuilder.generateUniqueName()
- <jdbc:embedded-database generate-name="true" … >
在創建嵌入式數據庫時建議生成唯一名稱。
擴展嵌入式數據庫支持
你可以通過兩種方式擴展Spring JDBC嵌入式數據庫支持:
- 實現EmbeddedDatabaseConfigurer以支持新的嵌入式數據庫類型。
- 實現DataSourceFactory 以支持新的DataSource實現,例如管理嵌入數據庫連接的連接池。
十、初始化數據源
org.springframework.jdbc.datasource.init包提供對已存在數據源的初始化的支持。嵌入式數據庫支持為應用程序創建和初始化數據源提供了一個選項。但是,有時可能需要初始化在某個服務器上運行的實例。
使用spring xml初始化數據庫
如果要初始化數據庫,並且可以提供對數據源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)/*-數據.sql). 如果使用模式,則腳本將按其URL或文件名的詞法順序運行。
數據庫初始值設定項的默認行為是無條件運行提供的腳本。這可能並不總是你想要的。例如,如果你對已包含測試數據的數據庫運行腳本。遵循先創建表然后插入數據的通用模式(如前所示),可以降低意外刪除數據的可能性。如果表已經存在,第一步將失敗。
但是,為了更好地控制現有數據的創建和刪除,XML名稱空間提供了一些附加選項。第一個是用於打開和關閉初始化的標志。你可以根據環境進行設置(例如從系統屬性或環境bean中提取布爾值)。
以下示例從系統屬性獲取值:
<jdbc:initialize-database data-source="dataSource" enabled="#{systemProperties.INITIALIZE_DATABASE}"> <jdbc:script location="..."/> </jdbc:initialize-database>
控制初始化時執行錯誤時忽略一些操作
<jdbc:initialize-database data-source="dataSource" ignore-failures="DROPS"> <jdbc:script location="..."/> </jdbc:initialize-database>