通過(guò) spring 征服 JDBC
spring 的數(shù)據(jù)訪問(wèn)哲學(xué)

spring 異常體系


Spring將數(shù)據(jù)訪問(wèn)過(guò)程中固定的和可變的部分明確劃分為兩個(gè)不同的類:模板(template)和回調(diào)(callback)。模板管理過(guò)程中固定的部分,而回調(diào)處理自定義的數(shù)據(jù)訪問(wèn)代碼。

針對(duì)不同的持久化平臺(tái),Spring提供了多個(gè)可選的模板。如果直接使用JDBC,那你可以選擇JdbcTemplate。如果你希望使用對(duì)象關(guān)系映射框架,那HibernateTemplate或JpaTemplate可能會(huì)更適合你。表10.2列出了Spring所提供的所有數(shù)據(jù)訪問(wèn)模板及其用途。


配置數(shù)據(jù)源
使用 JDNI 數(shù)據(jù)
利用Spring,我們可以像使用Spring bean那樣配置JNDI中數(shù)據(jù)源的引用并將其裝配到需要的類中。位于jee命名空間下的<jee:jndilookup>元素可以用于檢索JNDI中的任何對(duì)象(包括數(shù)據(jù)源)并將其作為Spring的bean。例如,如果應(yīng)用程序的數(shù)據(jù)源配置在JNDI中,我們可以使用<jee:jndi-lookup>元素將其裝配到Spring中,如下所示:
<jee:jndi-lookup id="dataSource" jndi-name="/jdbc/SpitterDS" resource-ref="true"/>
如果想使用Java配置的話,那我們可以借助JndiObjectFactoryBean從JNDI中查找DataSource:
@Bean
public JndiObjectFactoryBean dataSource(){
JndiObjectFactoryBean jndiObjectFB = new JndiObjectFactoryBean();
jndiObjectFB.setJndiName("jdbc/SpittrDS");
jndiObjectFB.setResourceRef(true);
jndiObjectFB.seProxyInterface(javax.sql.DataSource.class);
return jndiObjectFB;
}
使用數(shù)據(jù)源連接池
是配置DBCP BasicDataSource的方式:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
p:driverClassName="org.h2.Driver"
p:url="jdbc:h2:tcp://localhost/~/spitter"
p:username="sa"
p:password=""
p:initialSize="5"
p:maxActive="10"/>
@Bean
public BasicDataSource dataSource(){
BasicDataSource ds = new BasicDataSource();
ds.setDriverClassName("org.h2.Driver");
ds.setUrl("jdbc:h2:tcp://localhost/~/spitter");
ds.setUsername("sa");
ds.setPassword("");
ds.setInitalSize(5);
ds.setMaxActive(10);
return ds;
}
BasicDataSource 配置屬性

基于 JDBC 驅(qū)動(dòng)的數(shù)據(jù)源
在Spring中,通過(guò)JDBC驅(qū)動(dòng)定義數(shù)據(jù)源是最簡(jiǎn)單的配置方式。Spring提供了三個(gè)這樣的數(shù)據(jù)源類(均位于org.springframework.jdbc.datasource包中)供選擇:
-DriverManagerDataSource:在每個(gè)連接請(qǐng)求時(shí)都會(huì)返回一個(gè)新建的連接。與DBCP的BasicDataSource不同,由DriverManagerDataSource提供的連接并沒有進(jìn)行池化管理;
-SimpleDriverDataSource:與DriverManagerDataSource的工作方式類似,但是它直接使用JDBC驅(qū)動(dòng),來(lái)解決在特定環(huán)境下的類加載問(wèn)題,這樣的環(huán)境包括OSGi容器;
-SingleConnectionDataSource:在每個(gè)連接請(qǐng)求時(shí)都會(huì)返回同一個(gè)的連接。盡管SingleConnectionDataSource不是嚴(yán)格意義上的連接池?cái)?shù)據(jù)源,但是你可以將其視為只有一個(gè)連接的池。
@Bean
public DataSource dataSource(){
DriverMangerDataSource ds = new DriverMangerDataSource();
ds.setDriverClassName("org.h2.Driver");
ds.setUrl("jdbc:h2:tcp://localhost/~/spitter");
ds.setUsername("sa");
ds.setPassword("");
return ds;
}
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"
p:driverClassName="org.h2.Driver"
p:url="jdbc:h2:tcp://localhost/~/spitter"
p:username="sa"
p:password="" />
盡管這些數(shù)據(jù)源對(duì)于小應(yīng)用或開發(fā)環(huán)境來(lái)說(shuō)是不錯(cuò)的,但是要將其用于生產(chǎn)環(huán)境,你還是需要慎重考慮。因?yàn)镾ingleConnectionDataSource有且只有一個(gè)數(shù)據(jù)庫(kù)連接,所以不適合用于多線程的應(yīng)用程序,最好只在測(cè)試的時(shí)候使用。而DriverManagerDataSource和SimpleDriverDataSource盡管支持多線程,但是在每次請(qǐng)求連接的時(shí)候都會(huì)創(chuàng)建新連接,這是以性能為代價(jià)的。鑒于以上的這些限制,我強(qiáng)烈建議應(yīng)該使用數(shù)據(jù)源連接池。
使用嵌入式的數(shù)據(jù)源
使用jdbc命名空間配置嵌入式數(shù)據(jù)庫(kù)
<?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:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc-3.1.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
...
<jdbc:embedded-database id="dataSource" type="H2">
<jdbc:script location="com/habuma/spitter/db/jdbc/schema.sql"/>
<jdbc:script location="com/habuma/spitter/db/jdbc/test-data.sql"/>
</jdbc:embedded-database>
</beans>
我們將<jdbc:embedded-database>的type屬性設(shè)置為H2,表明嵌入式數(shù)據(jù)庫(kù)應(yīng)該是H2數(shù)據(jù)庫(kù)(要確保H2位于應(yīng)用的類路徑下)。另外,我們還可以將type設(shè)置為DERBY,以使用嵌入式的ApacheDerby數(shù)據(jù)庫(kù)。
除了搭建嵌入式數(shù)據(jù)庫(kù)以外,<jdbc:embedded-database>元素還會(huì)暴露一個(gè)數(shù)據(jù)源,我們可以像使用其他的數(shù)據(jù)源那樣來(lái)使用它。在這里,id屬性被設(shè)置成了dataSource,這也是所暴露數(shù)據(jù)源的bean ID。因此,當(dāng)我們需要javax.sql.DataSource的時(shí)候,就可以注入dataSource bean。
如果使用Java來(lái)配置嵌入式數(shù)據(jù)庫(kù)時(shí),不會(huì)像jdbc命名空間那么簡(jiǎn)便,我們可以使用EmbeddedDatabaseBuilder來(lái)構(gòu)建
@Bean
public DataSource dataSource(){
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:schema.sql")
.addScript("classpath:test-data.sql")
.build();
}
使用profile選擇數(shù)據(jù)源
對(duì)于開發(fā)期來(lái)說(shuō),<jdbc:embedded-database>元素是很合適的,而在QA環(huán)境中,你可能希望使用DBCP的BasicDataSource,在生產(chǎn)部署環(huán)境下,可能需要使用<jee:jndi-lookup>。
public class DataSourceConfiguration {
@Profile("development")
@Bean
public DataSource embeddedDataSource(){
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:schema.sql")
.addScript("classpath:test-data.sql")
.build();
}
@Profile("qa")
@Bean
public DataSource Data(){
BasicDataSource ds = new BasicDataSource();
ds.setDriverClassName("org.h2.Driver");
ds.setUrl("jdbc.h2:tcp://localhost/~/spitter");
ds.setUsername("sa");
ds.setPassword("");
ds.setInitialSize(5);
ds.setMaxActive(10);
return ds;
}
@Profile("production")
@Bean
public DataSource dataSource(){
JndiObjectFactoryBean jndiObjectFactoryBean
= new JndiObjectFactoryBean();
jndiObjectFactoryBean.setJndiName("jdbc/SpittrDS");
jndiObjectFactoryBean.setResourceRef(true);
jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
return (DataSource) jndiObjectFactoryBean.getObject();
}
}
}
通過(guò)使用profile功能,會(huì)在運(yùn)行時(shí)選擇數(shù)據(jù)源,這取決于哪一個(gè)profile處于激活狀態(tài)。如程序清單10.2配置所示,當(dāng)且僅當(dāng)developmentprofile處于激活狀態(tài)時(shí),會(huì)創(chuàng)建嵌入式數(shù)據(jù)庫(kù),當(dāng)且僅當(dāng)qa profile處于激活狀態(tài)時(shí),會(huì)創(chuàng)建DBCPBasicDataSource,當(dāng)且僅當(dāng)productionprofile處于激活狀態(tài)時(shí),會(huì)從JNDI獲取數(shù)據(jù)源。
<?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:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jee="http://www.springframework.org/schema/jee"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc-3.1.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<beans profile="development">
<jdbc:embedded-database id="dataSource" type="H2">
<jdbc:script location="com/habuma/spitter/db/jdbc/schema.sql"/>
<jdbc:script location="com/habuma/spitter/db/jdbc/test-data.sql"/>
</jdbc:embedded-database>
</beans>
<beans profile="qa">
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
p:driverClassName="org.h2.Driver"
p:url="jdbc:h2:tcp://localhost/~/spitter"
p:username="sa"
p:password=""
p:initialSize="5"
p:maxActive="10"/>
</beans>
<beans profile="production">
<jee:jndi-lookup id="dataSource"
jndi-name="/jdbc/SpitterDS"
resource-ref="true"/>
</beans>
</beans>
在 spring 中使用 JDBC
使用 JDBC 在數(shù)據(jù)庫(kù)插入一行
private static final String SQL_INSERT_SPITTER = "insert into spitter (username,password,fullname) values (?,?,?)";
private DataSource dataSource;
public void addSpitter (Spitter spitter){
Connection conn = null;
PreparedStatement stmt = null;
try{
conn = dataSource.getConnection();
stmt = conn.prepareStatement(SQL_INSERT_SPITTER);
stmt.setString(1,spitter.getUsername());
stmt.setString(2.spitter.getPassword());
stmt.setString(3,spiiter.getFullName());
stmt.execute();
}catch(SQLException e){
//do somthing...not sure what, though
}finally{
try{
if(stmt != null){
stmt.close();
}
if(conn != null){
conn.close();
}
}catch(SQLException e){
//I'm even less sure about what to do here
}
}
}
使用 JDBC 在數(shù)據(jù)庫(kù)更新一行
private static final String SQL_UPDATE_SPITTER = "update spitter set username = ?,password = ?, fullname = ? " + "where id =?";
public void saveSpitter(Spitter spitter) {
Connection conn = null;
PreparedStatement stmt = null;
try{
conn = dataSource.getConnection();
stmt = conn.prepareStatement(SQL_UPDATE_SPITTER);
stmt.setString(1,spitter.getUsername());
stmt.setString(2,spitter.getPassword());
stmt.setString(3,spitter.getFullname());
stmt.setLong(4,spitter.getId());
stmt.execute();
}catch(SQLException e){
//still not sure what I'm supposed to do here
}finally{
try{
if(stmt != null){
stmt.close();
}
if(conn != null){
conn.close();
}
}catch(SQLException e){
//or here
}
}
}
使用 JDBC 數(shù)據(jù)庫(kù)查詢一行數(shù)據(jù)
private static final String SQL_SELECT_SPITTER = "select id username,fullname from spitter where id = ?";
public Spitter findOne(long id) {
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try{
conn = dataSource.getConnection();
stmt = conn.prepareStatement(SQL_SELECT_SPITTER);
stmt.setLong(1,id);
rs = stmt.executeQuery();
Spitter spitter = null;
if (rs.next()){
spitter = new Spitter();
spitter.setId(rs.getLong("id"));
spitter.setUsername(rs.getString("username"));
spitter.setUsername(rs.getString("password"));
spitter.setUsername(rs.getString("fullname"));
}
return spitter;
}catch(SQLException e){
}finally{
if(rs != null){
try{
rs.close();
}catch(SQLException e){}
}
if(stmt != null){
try{
stmt.close();
}catch(SQLException e){}
}
if(conn != null){
try{
conn.close();
}catch(SQLException e){}
}
}
}
上面三個(gè)示例代碼都比較冗長(zhǎng),但是保證了代碼的健壯性,下面我們來(lái)看看如何使用 JDBC 模板來(lái)簡(jiǎn)化代碼:
Spring的JDBC框架承擔(dān)了資源管理和異常處理的工作,從而簡(jiǎn)化了JDBC代碼,讓我們只需編寫從數(shù)據(jù)庫(kù)讀寫數(shù)據(jù)的必需代碼。
-JdbcTemplate:最基本的Spring JDBC模板,這個(gè)模板支持簡(jiǎn)單的JDBC數(shù)據(jù)庫(kù)訪問(wèn)功能以及基于索引參數(shù)的查詢;
-NamedParameterJdbcTemplate:使用該模板類執(zhí)行查詢時(shí)可以將值以命名參數(shù)的形式綁定到SQL中,而不是使用簡(jiǎn)單的索引參數(shù);
-SimpleJdbcTemplate:該模板類利用Java 5的一些特性如自動(dòng)裝箱、泛型以及可變參數(shù)列表來(lái)簡(jiǎn)化JDBC模板的使用。
使用 JdbcTemplate 插入數(shù)據(jù)
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource){
return new JdbcTemplate(dataSource);
}
現(xiàn)在,我們可以將jdbcTemplate裝配到Repository中并使用它來(lái)訪問(wèn)數(shù)據(jù)庫(kù)。例如,SpitterRepository使用了JdbcTemplate:
@Repository
public class JdbcSpitterRepository implements SpitterRepository{
private JdbcOperations jdbcOperations;
@Inject
public JdbcSpitterRepository(JdbcOperations jdbcOperations){
this.jdbcOperations = jdbcOperations;
}
...
}
在這里,JdbcSpitterRepository類上使用了@Repository注解,這表明它將會(huì)在組件掃描的時(shí)候自動(dòng)創(chuàng)建。它的構(gòu)造器上使用了@Inject注解,因此在創(chuàng)建的時(shí)候,會(huì)自動(dòng)獲得一個(gè)JdbcOperations對(duì)象。JdbcOperations是一個(gè)接口,定義了JdbcTemplate所實(shí)現(xiàn)的操作。通過(guò)注入JdbcOperations,而不是具體的JdbcTemplate,能夠保證JdbcSpitterRepository通過(guò)JdbcOperations接口達(dá)到與JdbcTemplate保持松耦合。
作為另外一種組件掃描和自動(dòng)裝配的方案,我們可以將JdbcSpitterRepository顯式聲明為Spring中的bean,如下所示:
@Bean
public SpitterRepository spitterRepository(JdbcTemplate jdbcTemplate) {
return new JdbcSpitterRepository(jdbcTemplate);
}
基于JdbcTemplate的addSpitter()方法如下:
public void addSpitter(Spitter spitter){
jdbcOperations.update(INSERT_SPITTER,
spitter.getUsername(),
spitter.getPassword(),
spitter.getFullName(),
spitter.getEmail(),
spitter.isUpdateByEmail());
}
使用JdbcTemplate查詢Spitter
public Spitter findOne(long id){
return jdbcOperations.queryForObject(SELECT_SPITTER_BY_ID,new SpitterRowMapper(),id);
}
...
private static final class SpitterRowMapper implements RowMapper<Spitter>{
public Spitter mapRow(ResultSet rs, int rowNum)throws SQLException{
return new Spitter(rs.getLong("Id"),
rs.getString("username"),
rs.getString("password"),
rs.getString("fullName"),
rs.getString("email"),
rs.getBoolean("updateByEmail"));
}
}
使用Spring JDBC模板的命名參數(shù)功能
private static final String INSERT_SPITTER = "insert into Spitter" +
"(username,password,fullname,email,updateByEmail)" +
"values" + "(:username,:password,:fullname,:email,:updateByEmail)";
public void addSpitter(Spitter spitter){
Map<String,Object> paramMap = new HashMap<>();
paramMap.put("username",spitter.getUserName());
paramMap.put("password",spitter.getPassword());
paramMap.put("fullname",spitter.getFullName());
paramMap.put("email",spitter.getEmail());
paramMap.put("updateByEmail",spitter.isUpdateByEmail());
jdbcOperation.update(INSERT_SPITTER,paramMap);
}
如果感興趣,請(qǐng)關(guān)注我的微信公眾號(hào):