說明:未修訂版,閱讀起來極度困難
1、Spring框架JDBC的介紹
Spring JDBC - who does what?
| 動作 | Spring | 你 |
| 定義連接參數 | 是 | |
| 打開連接 | 是 | |
| 指定SQL語句 | 是 | |
| 聲明參數,提供參數值 | 是 | |
| 准備、執行語句 | 是 | |
| 迭代結果(如果有) | 是 | |
| 操作每個迭代 | 是 | |
| 處理任何異常 | 是 | |
| 處理事務 | 是 | |
| 關閉連接、語句、結果集 | 是 |
一句話,Spring框架負責所有的低級別細節操作。
1.1、選擇JDBC數據庫訪問的路徑
所有的路徑都需要兼容JDBC 2.0的驅動,部分特性可能需要JDBC 3.0 的驅動。
JdbcTemplate 是經典的Spring JDBC 路徑,最流行。最低級的路徑,所有其他的底層都是用一個JdbcTemplate。
NamedParameterJdbcTemplate 封裝了一個JdbcTemplate,使用帶名字的參數取代了傳統的JDBC 占位符(?)。該路徑,提供了更好的文檔,當你有很多參數時,更易於使用。
SimpleJdbcInsert和SimpleJdbcCall 優化了數據庫元數據,限制了必要數據的數量。該路徑簡化了編碼,你只要提供表名或procedure並提供參數和列匹配的映射即可。這只有在數據庫提供了足夠的元數據時才起作用。否則需要顯式的提供足夠的數據。
RDBM Objects 包括 MappingSqlQuery、SqlUpdate和StoredProcedure,要求在數據訪問層初始化時創建可復用的、線程安全的對象。
1.2、package層級
Spring框架的JDBC抽象框架由四個不同的包組成:core、datasource、object以及support。
org.springframework.jdbc.core 包含JdbcTemplate類和其各種回調接口,以及一組相關的類。子包jdbc.core.simple包含了SimpleJdbcInsert 和 SimpleJdbcCall類。子包jdbc.core.namedparam 包含了NamedParameterJdbcTemplate類和相關支持類。
jdbc.datasource包含了簡化DataSource使用的工具類,以及各種簡單的DataSource實現(可以用於Java EE 容器之外的測試和運行)。子包jdbc.datasource.embedded支持使用Java數據庫引擎創建內嵌的數據庫,如HSQL、H2、Derby。
jdbc.object包含了代表RDBMS查詢、更新,以及stored-procedure(存儲過程)。
This approach is modeled by JDO, although objects returned by queries are naturally disconnected from the database. This higher level of JDBC abstraction depends on the lower-level abstraction in the org.springframework.jdbc.core package.
The org.springframework.jdbc.support package provides SQLException translation functionality and some utility classes. Exceptions thrown during JDBC processing are translated to exceptions defined in the org.springframework.dao package. This means that code using the Spring JDBC abstraction layer does not need to implement JDBC or RDBMS-specific error handling. All translated exceptions are unchecked, which gives you the option of catching the exceptions from which you can recover while allowing other exceptions to be propagated to the caller.
2、使用JDBC核心類來控制基本的JDBC處理和錯誤處理
2.1、JdbcTemplate
處理資源的創建和釋放。執行core JDBC工作流的基本任務,例如語句的創建和執行,讓應用代碼只需要提供SQL和抽取結果即可。
當你使用JdbcTemplate時,你只需要實現回調接口們。PreparedStatementCreator回調接口會根據給定的Connection和SQL以及任何必要的參數來創建預處理語句。CallableStatementCreator接口也是一樣,只不過是創建callable語句。RowCallbackHandler接口會從ResultSet的每一行中提取值。
JdbcTemplate能在DAO實現內使用,使用DataSource實例化。
在Spring IoC容器中,DataSource總是需要被配置的!
JdbcTemplate使用例子:
本部分提供了一些JdbcTemplate的使用例子。沒有詳盡的列出JdbcTemplate的所有功能,具體見javadoc。
查詢(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);
// 將結果映射到domain object 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; } });
// 將查詢結果映射到一組domain objects。和上面相比,實際上只有等號左邊變化了。 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; } });
或者,上面的可以這樣寫:
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; } }
使用JdbcTemplate更新 (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
this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
調用一個簡單的存儲過程:
this.jdbcTemplate.update( "call SUPPORT.REFRESH_ACTORS_SUMMARY(?)", Long.valueOf(unionId));
JdbcTemplate 最佳實踐
JdbcTemplate 實例 是線程安全的!就是說,你只需要定義一個實例即可,隨便注入並使用!
通常的實踐是配置一個DataSource,然后將其注入到DAO類中。JdbcTemplate通過setDataSource(..)創建:
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 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 http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://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 public class JdbcCorporateEventDao implements CorporateEventDao { private JdbcTemplate jdbcTemplate; @Autowired public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } // JDBC-backed implementations of the methods on the CorporateEventDao follow... }
<?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 http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- Scans within the base package of the application for @Component classes to configure as beans --> <context:component-scan base-package="org.springframework.docs.test" /> <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>
如果使用Spring的JdbcDaoSupport類,可以讓你的DAO類繼承它,然后自動繼承了setDataSource(..)方法。
只有在訪問多個數據庫時才需要創建多個JdbcTemplate實例!
2.2、NamedParameterJdbcTemplate
// some JDBC-backed DAO class... private NamedParameterJdbcTemplate namedParameterJdbcTemplate; public void setDataSource(DataSource dataSource) { this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); } public int countOfActorsByFirstName(String firstName) { String sql = "select count(*) from T_ACTOR where first_name = :first_name"; SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName); // this return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class); }
// some JDBC-backed DAO class... private NamedParameterJdbcTemplate namedParameterJdbcTemplate; public void setDataSource(DataSource dataSource) { this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); } 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); // 換成map了,一樣 return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class); }
還可以,從domain object中獲取參數 -- 不是寫入domain object!!!如下:
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... }
// some JDBC-backed DAO class... private NamedParameterJdbcTemplate namedParameterJdbcTemplate; public void setDataSource(DataSource dataSource) { this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); } 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); }
一個接口,其實現類能夠在SQLExceptions和Spring自己的org.springframework.dao.DataAccessException之間轉換。其實現可以是泛型的也可以是具體的。
SQLErrorCodeSQLExceptionTranslator 是默認使用的實現。
2.4、執行語句
執行一個SQL語句只需要少量的代碼。
import javax.sql.DataSource; import org.springframework.jdbc.core.JdbcTemplate; public class ExecuteAStatement { private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } public void doExecute() { this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))"); } }
2.5、運行查詢
一些查詢方法返回單個值。使用queryForObject(..)從一行中獲取一個計數或特定的值。后者將返回的JDBC類型轉成指定的Java類型。如果類型轉換無效,會拋出InvalidDataAccessApiUsageException異常。
import javax.sql.DataSource; import org.springframework.jdbc.core.JdbcTemplate; public class RunAQuery { private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } public int getCount() { // 返回int return this.jdbcTemplate.queryForObject("select count(*) from mytable", Integer.class); } public String getName() { // 返回String return this.jdbcTemplate.queryForObject("select name from mytable", String.class); } }
除了單個結果的查詢方法,有幾個方法返回一個列表。最泛型的方法是queryForList(..),會返回List<Map<col_name_type, col_value_type>>。如下:
private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } public List<Map<String, Object>> getList() { return this.jdbcTemplate.queryForList("select * from mytable"); }
返回結果類似這樣:
[{name=Bob, id=1}, {name=Mary, id=2}]
2.6、更新數據庫
下面的例子示意了更新某個特定主鍵對應的列。參數值可以通過變參數或數組傳入。primitive類型會自動裝箱。
import javax.sql.DataSource; import org.springframework.jdbc.core.JdbcTemplate; public class ExecuteAnUpdate { private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } public void setName(int id, String name) { this.jdbcTemplate.update("update mytable set name = ? where id = ?", name, id); } }
2.7、獲取自動生成的鍵 auto-generated keys -- 就是mybatis的<selectKey>
JDBC 3.0標准支持使用update()方法獲取數據庫生成的主鍵。該方法使用一個PreparedStatementCreator作為其第一個參數。另一個參數是一個KeyHolder,其包含了update成功時生成的key。
下面的例子在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
3、控制數據庫連接
3.1、DataSource
Spring通過一個DataSource來獲取到數據庫的連接。DataSource是JDBC標准的一部分,是一個連接工廠。它允許容器或框架隱藏連接池和事務管理。
當使用Spring的JDBC層,你是從JNDI獲取一個data source,或者,你使用第三方連接池實現來配置自己的data source。流行的連接池實現包括DBCP和C3P0. Spring中的實現僅用於測試目的,不提供pooling功能。
本部分使用Spring的DriverMangerDataSource實現和幾個其他實現。
DriverMangerDataSource 僅用於測試目的!
DriverManagerDataSource dataSource = new DriverManagerDataSource(); // 實際工作中不要使用這個!!! dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); dataSource.setUrl("jdbc:hsqldb:hsql://localhost:"); dataSource.setUsername("sa"); dataSource.setPassword("");
<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的基本的連接和配置:
<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"/>
<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"/>
3.2、DataSourceUtils
幫助類,提供了靜態方法從JNDI獲取連接,以及關閉連接(如有必要)。支持線程綁定的連接,例如DataSourceTransactionManager。
3.3、SmartDataSource
擴展了DataSource接口,允許類使用它查詢連接是否應該在某操作之后被關閉。如果你明確會復用一個連接,它會很有效率的。
3.4、AbstractDataSource
是Spring DataSource實現的抽象類。
3.5、SingleConnectionDataSource
是SmartDataSource的實現,封裝了一個單獨的Connection,每次使用都不會關閉,所以不具備多線程功能。
測試類。
3.6、DriverManagerDataSource
是標准DataSource的實現,通過bean properties配置簡單的JDBC驅動,每次返回一個新的Connection!
在測試情況以及Java EE容器之外的環境下很有用。 -- 不要在生產環境中使用!
3.7、TransactionAwareDataSourceProxy
是對目標DataSource的代理,封裝了目標DataSource to add awareness of Spring-managed transactions。從這個角度看,類似於一個Java EE服務器提供的事務性的JNDIDataSource。
-- 很少很少用到這個類!
3.8、DataSourceTransactionManager
是PlatformTransactionManager的單JDBC datasources的實現。它將一個特定data source的JDBC連接綁定到當前執行的線程,允許每個data source一個線程。
需要代碼使用DataSourceUtils.getConnection(DataSource)代替DataSource.getConnection來獲取JDBC連接。它會拋出unchecked org.springframework.dao 異常,而不是SQLExceptions。所有的框架類如JdbcTemplate都隱式的使用這種策略。
DataSourceTransactionManager 支持自定義隔離級別、超時時間(用於每個JDBC語句查詢)。為了支持后者,代碼中應該使用JdbcTemplate或者DataSourceUtils.applyTransactionTimeout(..)方法來為每個創建的語句設置超時時間。
該實現可以替代JtaTransactionManager -- 在單資源的情況下,因為此時不需要容器支持JTA。二者的切換僅僅是配置的問題。
3.9、NativeJdbcExtractor
有時候,你需要訪問供應商獨有的JDBC方法,而非標准JDBC API。這時你可以使用NativeJdbcExtractor配置你的JdbcTemplate或者OracleLobHandler。
NativeJdbcExtractor 有不同變體:
- SimpleNativeJdbcExtractor
- C3P0NativeJdbcExtractor
- CommonsDbcpNativeJdbcExtractor
- JBossNativeJdbcExtractor
- WebLogicNativeJdbcExtractor
- WebSphereNativeJdbcExtractor
- XAPoolNativeJdbcExtractor
多數情況下,針對為封裝的Connection對象,只需要使用SimpleNativeJdbcExtractor。
4、JDBC 批處理操作
當你的批處理需要調用相同的預處理語句時,多數JDBC驅動提供了性能改進。
4.1、使用JdbcTemplate進行基本的批處理操作
JdbcTemplate批處理需要實現BatchPreparedStatementSetter接口的兩個方法,並將其作為第二個參數傳入你的batchUpdate(..)方法。使用getBatchSize()方法來提供當前batch的尺寸。使用setValues(..)方法來為預處理語句的參數設置。 該方法會被調用n次 -- 你在getBatchSize()方法中設置的次數。例子:
public class JdbcActorDao implements ActorDao { private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } public int[] batchUpdate(final List<Actor> actors) { int[] updateCounts = 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(); } }); return updateCounts; } // ... additional methods }
如果你在處理一個更新stream或者從文件中讀取的stream,可能會預先設置一個batch size,但最后的batch可能不會滿足batch size。這種情況下,可以使用InterruptibleBatchPreparedStatementSetter接口,其isBatchExhausted()方法允許你判斷批處理的結束。
4.2、針對對象列表的批處理操作
JdbcTemplate和NamedParameterJdbcTemplate 都有另一種提供batch update的方法。不需要實現特定的batch接口,只需要以列表形式提供所有的參數值。框架會遍歷這些值並使用一個內部的預處理語句SETTER。其API視你使用的命名參數不同而不同。For the named parameters you provide an array of SqlParameterSource, one entry for each member of the batch. You can use the SqlParameterSource.createBatch method to create this array, passing in either an array of JavaBeans or an array of Maps containing the parameter values.
例子(使用命名參數):
public class JdbcActorDao implements ActorDao { private NamedParameterTemplate namedParameterJdbcTemplate; // ? public void setDataSource(DataSource dataSource) { this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); } public int[] batchUpdate(final List<Actor> actors) { SqlParameterSource[] batch = SqlParameterSourceUtils.createBatch(actors.toArray()); int[] updateCounts = namedParameterJdbcTemplate.batchUpdate( "update t_actor set first_name = :firstName, last_name = :lastName where id = :id", batch); return updateCounts; } // ... additional methods }
使用JDBC的占位符:
public class JdbcActorDao implements ActorDao { private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } 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); } int[] updateCounts = jdbcTemplate.batchUpdate( "update t_actor set first_name = ?, last_name = ? where id = ?", // ? 占位符 batch); return updateCounts; } // ... additional methods }
上面兩個例子最后都會返回一個int數組,每個值都代表一條語句執行后影響的行數。如果不可用,會返回-2。
4.3、多個批處理的批處理操作
當批處理非常大時,你可能想將其拆分成幾個小的批處理。當然,你可以使用上面的方法,多次調用batchUpdate方法即可,但這里有一個更方便的方法。該方法除了使用SQL語句,還需要一個對象的集合 -- 包含了參數、每個batch中update的數量、以及一個ParameterizedPreparedStatementSetter 來為參數設值。框架會遍歷提供的值,並將update調用拆分成指定大小的batch中。
例子:
public class JdbcActorDao implements ActorDao { private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } public int[][] batchUpdate(final Collection<Actor> actors) { int[][] updateCounts = jdbcTemplate.batchUpdate( "update t_actor set first_name = ?, last_name = ? where id = ?", actors, 100, // this, size of each batch new ParameterizedPreparedStatementSetter<Actor>() { // this 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數組的數組。不再解釋。
5、使用SimpleJdbc類來簡化JDBC操作
SimpleJdbcInsert和SimpleJdbcCall類利用了可以從JDBC驅動獲取的數據庫的元數據,從而提供了簡化的配置。
5.1、使用SimpleJdbcInsert 插入數據
public class JdbcActorDao implements ActorDao { private JdbcTemplate jdbcTemplate; private SimpleJdbcInsert insertActor; // this public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); this.insertActor = new SimpleJdbcInsert(dataSource).withTableName("t_actor"); // here!!! } 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 }
5.2、使用SimpleJdbcInsert獲取自動生成的鍵
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") // here .usingGeneratedKeyColumns("id"); // here } 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); // here actor.setId(newId.longValue()); } // ... additional methods }
如果返回多個自動生成的鍵,可以使用executeAndReturnKeyHolder,這會返回一個KeyHolder。
5.3、為SimpleJdbcInsert指定列
就是限制插入的列。
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") // here .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 }
5.4、使用SqlParameterSource提供參數值
使用Map提供參數值很不錯,但不是最方便的。Spring提供了一組SqlParameterSource接口的實現,很不錯。第一個是BeanPropertySqlParameterSource,適用於JavaBean類。例子:
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); // here 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() // this .addValue("first_name", actor.getFirstName()) // 鏈式編程 .addValue("last_name", actor.getLastName()); // 鏈式編程 Number newId = insertActor.executeAndReturnKey(parameters); actor.setId(newId.longValue()); } // ... additional methods }
5.5、使用SimpleJdbcCall調用存儲過程
略
5.6、為SimpleJdbcCall顯式聲明參數
略
5.7、如何定義SqlParameter
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/jdbc.html#jdbc-params
為了SimpleJdbc類還有RDBMS的操作類定義參數,可以使用SqlParameter或其任一子類。通常需要在構造器中指定參數名和SQL類型。SQL類型使用java.sql.Types中的常量。
new SqlParameter("in_id", Types.NUMERIC), new SqlOutParameter("out_first_name", Types.VARCHAR),
略。。。
5.8、使用SimpleJdbcCall調用stored function
略
5.9、從SimpleJdbcCall返回ResultSet/REF 游標
略
6、將JDBC操作模型化為Java對象
org.springframework.jdbc.object
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/jdbc.html#jdbc-object
略
6.1、SqlQuery
6.2、MappingSqlQuery
6.3、SqlUpdate
6.4、StoredProcedure
7、參數和數據值處理的常見問題
7.1、為參數提供SQL type信息
一般,Spring是基於傳入的參數類型決定參數的SQL type。但還可以顯式的提供SQL type。有時候為了正確設置NULL值,這樣做很有必要。
有以下幾種方式:
- JdbcTemplate的很多update和query方法會接收一個額外的參數:int數組形式。該數組用於表明相應參數的SQL類型,其值來自java.sql.Types類的常量。
- 還可以使用SqlParameterValue類來封裝需要額外信息的參數值。為每個值創建一個實例,將SQL type和參數值傳入構造器。You can also provide an optional scale parameter for numeric values.
- 當使用命名參數時,使用SqlParameterSource類的子類:BeanPropertySqlParameterSource 或 MapSqlParameterSource。它們都有為任意命名參數值注冊SQL type的方法。
7.2、處理BLOB 和 CLOB對象
你可以在數據庫中存儲圖像、其他二進制數據,還有大量的文本。這些大對象稱為:BLOB(Binary Large Object) -- 針對二進制數據;CLOB (Character Large Object) -- 針對字符數據。在Spring中你可以使用JdbcTemplate處理這些大對象 (In Spring you can handle these large objects by using the JdbcTemplate directly and also when using the higher abstractions provided by RDBMS Objects and the SimpleJdbc classes.)。所有這些途徑都是用了LobHandler接口的實現。LobHandler可以訪問LobCreator類以創建新的LOB對象--使用getLobCreator()方法,從而插入數據庫。
LobCreator/LobHandler 為LOB的輸入輸出提供下列支持:
- BLOB
- byte[] -- getBlobAsBytes, setBlobAsBytes
- InputStream -- getBlobAsBinaryStream, setBlobAsBinaryStream
- CLOB
- String -- getClobAsString, setClobAsString
- InputStream -- getClobAsAsciiStream, setClobAsAsciiStream
- Reader -- getClobAsCharacterStream, setClobAsCharacterStream
下面的例子使用了JdbcTemplate以及AbstractLobCreatingPreparedStatementCallback的一個實現。它實現了setValues方法,該方法提供了一個LobCreator可以為LOB列設值。
下面的例子假定已經有一個變量lobHandler,且已經設置成DefaultLobHandler的實例。
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();
- Pass in the
lobHandlerthat in this example is a plainDefaultLobHandler. - Using the method
setClobAsCharacterStream, pass in the contents of the CLOB. - Using the method
setBlobAsBinaryStream, pass in the contents of the BLOB.
If you invoke the
setBlobAsBinaryStream,setClobAsAsciiStream, orsetClobAsCharacterStreammethod on theLobCreatorreturned fromDefaultLobHandler.getLobCreator(), you can optionally specify a negative value for thecontentLengthargument. If the specified content length is negative, theDefaultLobHandlerwill use the JDBC 4.0 variants of the set-stream methods without a length parameter; otherwise, it will pass the specified length on to the driver.
上面的例子是寫入LOB數據,下面看看讀取LOB數據:
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; } });
Using the method getClobAsString, retrieve the contents of the CLOB.
Using the method getBlobAsBytes, retrieve the contents of the BLOB.
7.3、為IN語句傳入值列表
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/jdbc.html#jdbc-in-clause
略
7.4、為存儲過程的調用處理復雜類型
略
8、內嵌數據庫支持
原生支持HSQL, H2, Derby。可以擴展。
8.1、為什么使用內嵌數據庫?
開發階段很有用,因為其輕量級特性。易配置,啟動快,便於測試,以及快速的改進SQL。
8.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>
另外,作為最佳實踐,內嵌數據庫會被賦予一個獨特的生成的名字。內嵌數據庫在Spring容器中是一種javax.sql.DataSource類型的bean,可以注入任何dao中。
8.3、編碼式創建一個內嵌數據庫
類似這樣:
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()
還可以這樣:
@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(); } }
8.4、選擇內嵌數據庫類型
使用HSQL
Spring支持HSQL 1.8.0及以上版本。如果沒有指定,默認就是HSQL。需要指定的話,將embedded-database標簽的type屬性設值為HSQL即可。
如果使用builder API,調用setType(EmbeddedDatabaseType.HSQL) 即可。
其他:H2,Derby。類似HSQL。
8.5、使用內嵌數據庫測試數據訪問邏輯
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(); } }
Embedded databases provide a lightweight way to test data access code. The following is a data access integration test template that uses an embedded database. Using a template like this can be useful for one-offs when the embedded database does not need to be reused across test classes. However, if you wish to create an embedded database that is shared within a test suite, consider using the Spring TestContext Framework and configuring the embedded database as a bean in the Spring
ApplicationContextas described in Section 19.8.2, “Creating an embedded database using Spring XML” and Section 19.8.3, “Creating an embedded database programmatically”.
8.6、為內嵌數據庫生成一個唯一的名字
8.7、擴展內嵌數據庫支持
9、初始化一個DataSource
org.springframework.jdbc.datasource.init包提供了初始化已有DataSource的支持。內嵌數據庫支持 提供了為一個應用創建和初始化一個DataSource的選項,但是,有時候你需要在服務器或者其他地方初始化一個實例。
9.1、使用Spring XML初始化一個數據庫
<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>
該數據庫初始化器的默認行為是無條件的執行提供的腳本。這通常不是你想要的,例如,如果你要針對一個已有數據的數據庫進行初始化。偶然刪除數據的可能可以通過以下常見模式(先創建表,再插入數據,如果已經存在表,那第一步就會失敗)來降低。
然而,為了獲取針對已有數據的創建和刪除的更多控制,XML命名空間提供了一些額外的選項。第一個選項就是切換是否初始化。這可以根據環境來設置(例如從系統properties中獲取一個boolean值),如下:
<jdbc:initialize-database data-source="dataSource" enabled="#{systemProperties.INITIALIZE_DATABASE}"> <jdbc:script location="..."/> </jdbc:initialize-database>
The second option to control what happens with existing data is to be more tolerant of failures. 你可以控制初始化器的能力,讓其在執行scripts中的SQL時忽略特定錯誤。例如:
<jdbc:initialize-database data-source="dataSource" ignore-failures="DROPS"> <jdbc:script location="..."/> </jdbc:initialize-database>
上面的例子是說,有時候我們期望scripts能夠在一個空的數據庫上執行 ,scripts中的一些DROP語句會執行失敗。這些失敗的SQL DROP語句會被忽略,但其他失敗會導致異常。當你的SQL dialect不支持 DROP ... IF EXISTS ,但你又想在重建數據前無條件的清除所有測試數據時,這會很有用。
ignore-failures 選項,可以被設置為NONE(默認)、DROPS、或 ALL。
每個語句都應以分號間隔,或者完全不使用分號,而用換行代替。你可以控制它:
<jdbc:initialize-database data-source="dataSource" separator="@@"> <jdbc:script location="classpath:com/foo/sql/db-schema.sql" separator=";"/> <jdbc:script location="classpath:com/foo/sql/db-test-data-1.sql"/> <jdbc:script location="classpath:com/foo/sql/db-test-data-2.sql"/> </jdbc:initialize-database>
上面是用 @@作為通用的分隔符,又給db-schema.sql設置了分號分隔符。
如果需要更多控制,使用DataSourceInitializer即可。
依賴數據庫的其他組件的初始化
數據庫初始化器依賴於一個DataSource實例,且會執行在其初始化回調(類似@PostConstruct)中提供的scripts。如果其他beans依賴相同的data source,並也在初始化回調中使用它,那么可能會導致問題,因為數據還沒有初始化!常見的例子是在應用啟動時就迫切的初始化並加載數據的緩存。
圍繞這個問題,你有兩個選擇:改變你的緩存初始化策略,不要迫切初始化,而是延遲初始化,或者確保數據庫初始化器已經初始化了。
第一個選擇可能很簡單 -- 如果你負責這個應用。一些建議:
- 讓緩存懶初始化--第一次使用時再初始化。
- 讓你的緩存或者初始化緩存的組件實現Lifecycle或SmartLifecycle。當應用上下文啟動時,SmartLifecyle會被自動啟動--如果它的autoStartup被設置了,可以在該上下文中手動調用
ConfigurableApplicationContext.start()來啟動Lifecycle - 使用ApplicationEvent或類似的觀察者機制來觸發緩存初始化。ContextRefreshedEvent通常是個很有用的鈎子。
第二個選擇可能同樣簡單。一些建議:
- 依賴Spring BeanFactory的默認行為--beans以注冊順序被初始化。你可以很簡單的安排順序--一組<import/>,讓數據庫放在前面。
- 分離DataSource和使用其的業務組件,通過將它們放在不同的ApplicationContext實例中來控制它們的啟動順序。--例如,父上下文包含DataSource,子上下文包含業務組件。這種結構在Spring web應用中很常見,但可用於更多地方。
