Spring JDBC最佳實踐(1)

Spring提供了兩種使用JDBC API的最佳實踐,一種是以JdbcTemplate爲核心的基於Template的JDBC的使用方式,另外一種則是在JdbcTemplate基礎之上的構建的基於操做對象的JDBC的使用方式。java

基於Template的JDBC的使用方式
該使用方式的最初設想和原型,須要追溯到Rod Johnson在03年出版的Expert One-on-One J2EE Design and Development,在該書的Practical Data Access(數據訪問實踐)中,Rod針對JDBC使用中的一些問題提出了一套改進的實踐原型,並最終將該原型完善後在Spring框架中發佈。算法

JDBC的尷尬
JDBC做爲Java平臺的訪問關係數據庫的標準,其成功是 有目共睹的。幾乎全部java平臺的數據訪問,都直接或者間接的使用了JDBC,它是整個java平臺面向關係數據庫進行數據訪問的基石。
做爲一個標準,無疑JDBC是很成功的,可是要說JDBC在使用過程中多麼的受人歡迎,則不盡然了。JDBC主要是面向較爲底層的數據庫操做,因此在設計的過程中 ,比較的貼切底層以提供儘量多的功能特點。從這個角度來講,JDBC API的設計無可厚非。但是,過於貼切底層的API的設計,對於開發人員則未必是一件好事。即便執行一個最簡單的查詢,開發人員也要按照API的規矩寫上一大堆雷同的代碼,若是不能合理的封裝使用JDBC API,在項目中使用JDBC訪問數據所出現的問題估計會令人發瘋!
對於一般的項目開發來講,若是層次劃分很明確,數據訪問邏輯通常應該在DAO層中實現。根據功能模塊的劃分,可能每一個開發人員都會分得或多或少的實現相應的DAO的任務,假設開發人員A在分得了DAO實現任務後進行開發,他或許開發了以下所示的代碼:
spring

package com.google.spring.jdbc;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;

import javax.sql.DataSource;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class DaoWithA implements IDao
{

	private final Log logger = LogFactory.getLog(DaoWithA.class);
	private DataSource dataSource = null;
	
	public DataSource getDataSource()
	{
		return dataSource;
	}

	public void setDataSource(DataSource dataSource)
	{
		this.dataSource = dataSource;
	}

	@Override
	public int updateSomething(String sql)
	{
		int count;
		Connection conn = null;
		Statement stmt = null;
		try
		{
			conn = getDataSource().getConnection();
			stmt = conn.createStatement();
			count = stmt.executeUpdate(sql);
			stmt.close();
			stmt = null;
		} 
		catch (SQLException e)
		{
			throw new RuntimeException(e);
			
		}
		finally
		{
			if(stmt!=null)
			{
				try
				{
					stmt.close();
				} 
				catch (SQLException ex)
				{
					logger.warn("fail to close statement:"+ex);
				}
			}
			if(conn!=null)
			{
				try
				{
					conn.close();
				} 
				catch (Exception ex)
				{
					logger.warn("failed to close Connection:"+ex);
				}
			}
		}
		return count;
	}

}

而B所負責的DAO的實現中,可能也有相似的更新的操做。無疑,B也要像A這樣,在他的DAO實現類中寫一大堆一樣的JDBC代碼,相似的狀況還可能擴展到C、D等開發人員。若是每一個開發人員都能嚴格的按照JDBC的編程規範開發還好,可是事實是,一個團隊中的開發人員是有差異的。
這其實只是API的使用過程當中的一個插曲,當你看到應用程序中成百的使用JDBC實現類的時候,會發現以下的問題:
一、Statement使用完沒有關閉,而是想着讓Connection關閉的時候一併關閉,但是並不是全部的驅動程序都有這樣的行爲。
二、建立了多個ResultSet或者Statement,只清理了最外層的,忽視了裏層的。

三、忘記關閉Connection。
JDBC規範在指定數據庫訪問異常的時候也沒有可以進行的很完全:
一、將異常類型定義爲SQLException是一個值得商榷的地方。
二、SQLExcpetion沒有采用將具體的異常狀況子類化,以進一步抽象不一樣的數據訪問的狀況,而是採用ErrorCode的方式來區分訪問過程當中所出現的不一樣異常狀況,其實這也沒什麼,只要能區分出具體的錯誤就行,可是JDBC規範卻把ErrorCode的規範留給了數據庫提供商,這致使了不一樣的數據庫供應商對應了不一樣的ErrorCode,進而應用程序在捕獲到SQLException後,還要看當前用的是什麼數據庫。
針對以上問題,Spring提供了相應的解決方案幫助咱們提升開發效率! sql

爲了解決JDBC API在實際使用中的各類尷尬的局面,spring提出了org.springframework.jdbc.core.JdbcTemplate做爲數據訪問的Helper類。JdbcTemplate是整個spring數據抽象層提供的全部JDBC API最佳實踐的基礎,框架內其它更加方便的Helper類以及更高層次的抽象,所有的構建於JdbcTemplate之上。抓住了JdbcTemplate,就抓住了spring框架JDBC API最佳實踐的核心。
歸納的說,JdbcTemplate主要關注一下兩個事情:
一、封裝全部的基於JDBC的數據訪問的代碼,以統一的格式和規範來使用JDBC API。全部的基於JDBC API的數據訪問所有經過JdbcTemplate,從而避免了容易出錯的數據訪問方式。
二、對SQLException所提供的異常信息在框架內進行統一的轉譯,將基於JDBC的數據訪問異常歸入Spring自身的異常層次之中,統一了數據接口的定義,簡化了客戶端代碼對數據訪問異常的處理。
Spring主要是經過模板方法對基於JDBC的數據訪問代碼進行統一的封裝,因此咱們可先看下模板方法:
模板方法主要是用於對算法的行爲或者邏輯進行封裝,即若是多個類中存在類似的算法邏輯或者行爲邏輯,能夠將這些邏輯提取到模板方法中實現,而後讓相應的子類根據須要實現某些自定義的邏輯。
舉個例子,全部的汽車,不論是寶馬仍是大衆,他們的駕駛流程基本上是固定的。實際上,除了少數的實現細節有所不一樣以外,大部分的流程是相同的,基本上是以下所示的流程說明:
一、點火啓動
二、踩剎車,掛前進的檔位(不一樣的車在這一步會存在差別)
三、放下手動控制器(手剎)
四、踩油門啓動車輛運行
此時,咱們能夠聲明一個模板方法類,將肯定的行爲以模板的形式定義,而將不一樣的行爲留給相應的子類來實現:
數據庫

package com.google.spring.jdbc;

public abstract class Vehicle
{
	
	public final void drive()
	{
		startTheEnginee();//啓動
		putIntoGear(); //前進
		looseHandBrake();//放下手剎
		stepOnTheGasAndGo();//踩油門前進
	}
	
	protected abstract void putIntoGear();
	
	private void startTheEnginee()
	{
		
	}
	
	private void looseHandBrake()
	{
		
	}
	
	private void stepOnTheGasAndGo()
	{
		
	}
}

drive()方法就是咱們的模板方法,它被聲明爲final,表示該類是不能被子類重寫的,車輛的自動擋和手動擋是不一樣的,因此留給了子類去實現:
package com.google.spring.jdbc;

public class VehicleAT extends Vehicle
{

	@Override
	protected void putIntoGear()
	{
		//掛前進檔位

	}

}

package com.google.spring.jdbc;

public class VehicleMT extends Vehicle
{

	@Override
	protected void putIntoGear()
	{
		//踩離合器 掛前進檔位

	}

}

這樣,每一個子類實現特有的邏輯就能夠了。apache

 JdbcTemplate的演化
若是回頭看一下最初的使用JDBC API進行數據訪問的代碼。就會發現,無論這些代碼是誰負責的,也無論數據訪問的邏輯如何,除了小部分的差別以外,全部的這些代碼幾乎都是按照同一個流程走下來的,以下:
一、conn=getDataSource().getConnection();
二、stmt=conn.createStatement()或者ps=conn.prepareStatement();
三、stmt.executeUpdate(sql)或者ps.executeUpdate()  或者進行相應的查詢。
四、stmt.close()  stmt=null
五、catch處理數據庫訪問異常
六、關閉數據庫鏈接避免鏈接泄露致使系統崩潰
對於多個DAO中充斥着幾乎相同的JDBC API的使用代碼,咱們也能夠採用模板方法,多這些代碼進行重構,避免因我的操做不當所出現的種種問題,咱們要作的,就是將一些公共的行爲提取到模板方法中去,而特有的操做,好比每次執行不一樣的更新,或者對不一樣的查詢結果進行不一樣的處理,則放入具體的子類中,這樣,咱們就有個JdbcTemplate的雛形:
編程

package com.google.spring.jdbc;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;

import javax.sql.DataSource;

import org.springframework.dao.DataAccessException;


public abstract class JdbcTemplate
{
	private DataSource dataSource;
	
	
	public DataSource getDataSource()
	{
		return dataSource;
	}


	public void setDataSource(DataSource dataSource)
	{
		this.dataSource = dataSource;
	}


	public final Object execute(String sql)
	{
		Connection conn = null;
		Statement stmt = null;
		try
		{
			conn = this.getDataSource().getConnection();
			stmt = conn.createStatement();
			Object retValue = this.executeWithStatement(stmt, sql);
			return retValue;
		}
		catch (SQLException e)
		{
			throw new RuntimeException(e);
		}
		finally
		{
			closeStatement(stmt);
			closeConnection(conn);
		}
	}
	
	protected abstract Object executeWithStatement(Statement stmt,String sql);
	
	private final DataAccessException translateSQLException(SQLException e)
	{
		DataAccessException dataAccessException = null;
		//進行相應的轉譯
		return dataAccessException;
	}
	
	private final void closeStatement(Statement stmt)
	{
		//關閉Statement
	}
	
	private final void closeConnection(Connection conn)
	{
		//關閉Connection
	}
}

這樣處理以後,JDBC代碼的使用有了規範。可是,只使用模板方法還不足以提供方便的Helper類。頂着abstract的帽子,每次使用都要進行相應的子類化,這也太不靠譜了。因此,spring中的JdbcTemplate除了引入了模板方法以外,還引入了相應的Callback,避免了每次都子類化,好比,當引入了StatementCallback接口之後:
package com.google.spring.jdbc;

import java.sql.Statement;

public interface StatementCallback
{
	public Object doWithStatement(Statement stmt) throws SQLException;
}

這樣這個真正的Helper類就存在了,以下所示:
package com.google.spring.jdbc;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;

import javax.sql.DataSource;

import org.springframework.dao.DataAccessException;


public  class JdbcTemplate
{
	private DataSource dataSource;
	
	
	public DataSource getDataSource()
	{
		return dataSource;
	}


	public void setDataSource(DataSource dataSource)
	{
		this.dataSource = dataSource;
	}


	public final Object execute(StatementCallback callback)
	{
		Connection conn = null;
		Statement stmt = null;
		try
		{
			conn = this.getDataSource().getConnection();
			stmt = conn.createStatement();
			Object retValue = callback.doWithStatement(stmt);
			return retValue;
		}
		catch (SQLException e)
		{
			throw new RuntimeException(e);
		}
		finally
		{
			closeStatement(stmt);
			closeConnection(conn);
		}
	}
	
	
	
	private final DataAccessException translateSQLException(SQLException e)
	{
		DataAccessException dataAccessException = null;
		//進行相應的轉譯
		return dataAccessException;
	}
	
	private final void closeStatement(Statement stmt)
	{
		//關閉Statement
	}
	
	private final void closeConnection(Connection conn)
	{
		//關閉Connection
	}
}

要在相應的DAO中使用該JdbcTemplate,只須要根據狀況提供參數和相應的callback就能夠了,以下所示:
final String sql = "update";
		JdbcTemplate jdbcTemplate = new JdbcTemplate();
		BasicDataSource dataSource = new BasicDataSource();
		jdbcTemplate.setDataSource(dataSource);
		//對dataSource進行setter操做
		StatementCallback callback = new StatementCallback()
		{
			public Object doWithStatement(Statement stmt) throws SQLException
			{
				return new Integer(stmt.executeUpdate(sql)) ;
			}
		};
		
		jdbcTemplate.execute(callback);

這樣,開發人員只須要關注與數據訪問邏輯相關的東西,JDBC底層的細節不須要再考慮了。
上述是spring中JdbcTemplate的中心思想,實際上,JdbcTemplate在實現上要考慮不少的東西,繼承層次以下:

org.springframework.jdbc.core.JdbcOperations接口定義了JdbcTemplate可使用的JDBC操做集合,該接口提供的操做聲明,從查詢到更新無所不有。
JdbcTemplate的直接父類是JdbcAccessor,這是一個抽象類,主要爲子類提供一些公用的屬性:

DataSource:javax.sql.DataSource是JDBC2.0以後引入的接口定義,用來替代java.sql.DriverManager的數據庫鏈接方式,它的角色能夠看作是JDBC的鏈接工廠,因此,基本上如今它應該做爲獲取數據庫資源的統一接口。

SQLExceptionTranslator:JdbcTemplate委託此類進行異常的轉譯。安全

JdbcTemplate中的模板方法可分爲以下的四組:
面向Connection的模板方法:
經過ConnectionCallback接口所公開的Connection進行數據訪問
app

import java.sql.Connection;
import java.sql.SQLException;

import org.springframework.dao.DataAccessException;

public interface ConnectionCallback
{
	Object doInConnection(Connection con) throws SQLException, DataAccessException;
}

public Object execute(ConnectionCallback action) throws DataAccessException {
		Assert.notNull(action, "Callback object must not be null");

		Connection con = DataSourceUtils.getConnection(getDataSource());
		try {
			Connection conToUse = con;
			if (this.nativeJdbcExtractor != null) {
				// Extract native JDBC Connection, castable to OracleConnection or the like.
				conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
			}
			else {
				// Create close-suppressing Connection proxy, also preparing returned Statements.
				conToUse = createConnectionProxy(con);
			}
			return action.doInConnection(conToUse); 		}
		catch (SQLException ex) {
			// Release Connection early, to avoid potential connection pool deadlock
			// in the case when the exception translator hasn't been initialized yet.
			DataSourceUtils.releaseConnection(con, getDataSource());
			con = null;
			throw getExceptionTranslator().translate("ConnectionCallback", getSql(action), ex);
		}
		finally {
			DataSourceUtils.releaseConnection(con, getDataSource());
		}
	}

能夠隨意操做Connection。
面向Statement的模板方法:
該模板方法主要處理基於SQL的數據訪問請求。該組模板方法經過org.springframework.jdbc.core.StatementCallback回調接口,對外公開java.sql.Statement的操做句柄。該方式縮小了回調接口內的權限範圍,可是提升了API使用上的安全性和便捷性。
面向PreparedStatement的模板方法:
對於使用包含查詢參數的SQL請求來講,使用PreparedStatement可讓咱們免於SQL注入的攻擊,而在使用PreparedStatement以前,須要根據傳入的包含參數的SQL對其進行建立,因此,面向PreparedStatement的模板方式會經過org.springframework.jdbc.core.PreparedStatementCreator的回調接口公開Connection以容許PreparedStatement的建立。PreparedStatement建立以後,會公開org.springframework.jdbc.core.PreparedStatementCallback回調接口,以支持其使用PreparedStatement進行數據訪問。
面向CallableStatement的模板方法:
JDBC支持使用CallableStatement進行數據庫存儲過程的訪問,面向CallableStatement的的模板方法會經過org.springframework.jdbc.core.CallableStatementCreator公開的Connection用於建立調用存儲過程的CallableStatement。以後,再經過org.springframework.jdbc.core.CallableStatementCallback公開的CallableStatement的操做句柄,實現基於存儲過程的數據訪問。
每一組的模板方法都 有一個核心的方法實現,其它的屬於同一組的重載的模板方法,會調用這個核心的方法來完成最終的工做。以面向Statement的模板方法爲例,使用StatementCallback回調接口做爲方法參數的execute方法是這組的核心代碼:
public Object execute(StatementCallback action) throws DataAccessException {
		Assert.notNull(action, "Callback object must not be null");

		Connection con = DataSourceUtils.getConnection(getDataSource());
		Statement stmt = null;
		try {
			Connection conToUse = con;
			if (this.nativeJdbcExtractor != null &&
					this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativeStatements()) {
				conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
			}
			stmt = conToUse.createStatement();
			applyStatementSettings(stmt);
			Statement stmtToUse = stmt;
			if (this.nativeJdbcExtractor != null) {
				stmtToUse = this.nativeJdbcExtractor.getNativeStatement(stmt);
			}
			Object result = action.doInStatement(stmtToUse);
			handleWarnings(stmt);
			return result;
		}
		catch (SQLException ex) {
			// Release Connection early, to avoid potential connection pool deadlock
			// in the case when the exception translator hasn't been initialized yet.
			JdbcUtils.closeStatement(stmt);
			stmt = null;
			DataSourceUtils.releaseConnection(con, getDataSource());
			con = null;
			throw getExceptionTranslator().translate("StatementCallback", getSql(action), ex);
		}
		finally {
			JdbcUtils.closeStatement(stmt);
			DataSourceUtils.releaseConnection(con, getDataSource());
		}
	}

其它模板方法會根據自身的簽名,構建相應的StatementCallback實例以調用回調接口中公開的方法,例如:
public void execute(final String sql) throws DataAccessException {
		if (logger.isDebugEnabled()) {
			logger.debug("Executing SQL statement [" + sql + "]");
		}

		class ExecuteStatementCallback implements StatementCallback, SqlProvider {
			public Object doInStatement(Statement stmt) throws SQLException {
				stmt.execute(sql);
				return null;
			}
			public String getSql() {
				return sql;
			}
		}
		execute(new ExecuteStatementCallback());
	}

同一組內的模板方法,能夠根據使用的方便性進行增長,只要在實現的時候,將相應的條件加以對應,改組的回調接口進行封裝,最終調用當前組的核心模板方法便可。
下面來逐一看下這些方法:
public void execute(final String sql):
public void execute(final String sql) throws DataAccessException {
		if (logger.isDebugEnabled()) {
			logger.debug("Executing SQL statement [" + sql + "]");
		}

		class ExecuteStatementCallback implements StatementCallback, SqlProvider {
			public Object doInStatement(Statement stmt) throws SQLException {
				stmt.execute(sql);
				return null;
			}
			public String getSql() {
				return sql;
			}
		}
		execute(new ExecuteStatementCallback());
	}

根據傳入的靜態SQL語句進行更新,無返回值,使用的是Statement框架

 public Object execute(String callString, CallableStatementCallback action) throws DataAccessException:

public Object execute(String callString, CallableStatementCallback action) throws DataAccessException {
		return execute(new SimpleCallableStatementCreator(callString), action);
	}

內部調的是
public Object execute(CallableStatementCreator csc, CallableStatementCallback action)
			throws DataAccessException {

		Assert.notNull(csc, "CallableStatementCreator must not be null");
		Assert.notNull(action, "Callback object must not be null");
		if (logger.isDebugEnabled()) {
			String sql = getSql(csc);
			logger.debug("Calling stored procedure" + (sql != null ? " [" + sql  + "]" : ""));
		}

		Connection con = DataSourceUtils.getConnection(getDataSource());
		CallableStatement cs = null;
		try {
			Connection conToUse = con;
			if (this.nativeJdbcExtractor != null) {
				conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
			}
			cs = csc.createCallableStatement(conToUse);
			applyStatementSettings(cs);
			CallableStatement csToUse = cs;
			if (this.nativeJdbcExtractor != null) {
				csToUse = this.nativeJdbcExtractor.getNativeCallableStatement(cs);
			}
			Object result = action.doInCallableStatement(csToUse);
			handleWarnings(cs);
			return result;
		}
		catch (SQLException ex) {
			// Release Connection early, to avoid potential connection pool deadlock
			// in the case when the exception translator hasn't been initialized yet.
			if (csc instanceof ParameterDisposer) {
				((ParameterDisposer) csc).cleanupParameters();
			}
			String sql = getSql(csc);
			csc = null;
			JdbcUtils.closeStatement(cs);
			cs = null;
			DataSourceUtils.releaseConnection(con, getDataSource());
			con = null;
			throw getExceptionTranslator().translate("CallableStatementCallback", sql, ex);
		}
		finally {
			if (csc instanceof ParameterDisposer) {
				((ParameterDisposer) csc).cleanupParameters();
			}
			JdbcUtils.closeStatement(cs);
			DataSourceUtils.releaseConnection(con, getDataSource());
		}
	}

JdbcTemplate在此提供了一個內部類SimpleCallableStatementCreator:
private static class SimpleCallableStatementCreator implements CallableStatementCreator, SqlProvider {

		private final String callString;

		public SimpleCallableStatementCreator(String callString) {
			Assert.notNull(callString, "Call string must not be null");
			this.callString = callString;
		}

		public CallableStatement createCallableStatement(Connection con) throws SQLException {
			return con.prepareCall(this.callString);
		}

		public String getSql() {
			return this.callString;
		}
	}

根據傳入的sql語句建立一CallableStatement

public Object execute(String sql, PreparedStatementCallback action) throws DataAccessException

public Object execute(String sql, PreparedStatementCallback action) throws DataAccessException {
		return execute(new SimplePreparedStatementCreator(sql), action);
	}

可知其內部調用了
execute(new SimplePreparedStatementCreator(sql), action)方法。
public Object execute(PreparedStatementCreator psc, PreparedStatementCallback action)
			throws DataAccessException {

		Assert.notNull(psc, "PreparedStatementCreator must not be null");
		Assert.notNull(action, "Callback object must not be null");
		if (logger.isDebugEnabled()) {
			String sql = getSql(psc);
			logger.debug("Executing prepared SQL statement" + (sql != null ? " [" + sql + "]" : ""));
		}

		Connection con = DataSourceUtils.getConnection(getDataSource());
		PreparedStatement ps = null;
		try {
			Connection conToUse = con;
			if (this.nativeJdbcExtractor != null &&
					this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativePreparedStatements()) {
				conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
			}
			ps = psc.createPreparedStatement(conToUse);
			applyStatementSettings(ps);
			PreparedStatement psToUse = ps;
			if (this.nativeJdbcExtractor != null) {
				psToUse = this.nativeJdbcExtractor.getNativePreparedStatement(ps);
			}
			Object result = action.doInPreparedStatement(psToUse);
			handleWarnings(ps);
			return result;
		}
		catch (SQLException ex) {
			// Release Connection early, to avoid potential connection pool deadlock
			// in the case when the exception translator hasn't been initialized yet.
			if (psc instanceof ParameterDisposer) {
				((ParameterDisposer) psc).cleanupParameters();
			}
			String sql = getSql(psc);
			psc = null;
			JdbcUtils.closeStatement(ps);
			ps = null;
			DataSourceUtils.releaseConnection(con, getDataSource());
			con = null;
			throw getExceptionTranslator().translate("PreparedStatementCallback", sql, ex);
		}
		finally {
			if (psc instanceof ParameterDisposer) {
				((ParameterDisposer) psc).cleanupParameters();
			}
			JdbcUtils.closeStatement(ps);
			DataSourceUtils.releaseConnection(con, getDataSource());
		}
	}

在建立PreparedStatementCreator實現類的時候,JdbcTemplate爲其默認提供了一個SimplePreparedStatementCreator內部靜態類,可根據傳入的SQL語句建立一個PreparedStatement  代碼以下:
private static class SimplePreparedStatementCreator implements PreparedStatementCreator, SqlProvider {

		private final String sql;

		public SimplePreparedStatementCreator(String sql) {
			Assert.notNull(sql, "SQL must not be null");
			this.sql = sql;
		}

		public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
			return con.prepareStatement(this.sql);
		}

		public String getSql() {
			return this.sql;
		}
	}
其它的模仿方法與此相似,能夠舉一反三!
相關文章
相關標籤/搜索