淺談JDBC DAO的設(shè)計理念
JDBC DAO中Connection的含義
Connection表示了一個和數(shù)據(jù)庫的鏈接,底層需要有操作系統(tǒng)的Socket支持,所以Connection是一種資源,既然是一種資源,就需要按照建立,打開,使用,關(guān)閉的順序合理的使用。
Connection是Java數(shù)據(jù)庫操作的基礎(chǔ),是進行一系列操作的基礎(chǔ),所有的派生的操作,例如Statement,PreparedStatement,ResultSet等都由Connection直接或者間接的衍生。
如何獲得Connection呢?
方法一,使用DriverManager類來獲取,前提條件是數(shù)據(jù)庫驅(qū)動程序需要在classpath下(即使用數(shù)據(jù)庫鏈接的程序按照Java的方式可以訪問到)。
Connectionconn=DriverManager.getConnection("jdbc:oracle:thin:@192.168.0.1:1521:ORCL",user,pwd);
方法二,使用數(shù)據(jù)庫連接池來獲取
什么是數(shù)據(jù)庫連接池呢,數(shù)據(jù)庫連接池是標(biāo)準(zhǔn)JavaEE容器的一種服務(wù),例如Webspher,Weblogic,Tomcat等,容器預(yù)先建立一些數(shù)據(jù)庫鏈接,以便應(yīng)用程序使用的時候從中借取,注意有借有還,當(dāng)應(yīng)用程序使用完了之后會將數(shù)據(jù)庫鏈接還回連接池。(數(shù)據(jù)源配置請參考其他文檔)
使用連接池的好處是,可以預(yù)先建立鏈接,減小在數(shù)據(jù)庫獲取上的相對時間。
使用連接池獲取數(shù)據(jù)庫鏈接的方式為:
- InitialContextctx=newInitialContext();
- DataSourceds=(DataSource)ctx.lookup("java:comp/env/jdbc/DataSource");
- Connectionconn=ds.getConnection();
由于在配置數(shù)據(jù)庫連接池的時候已經(jīng)定義了URL,用戶名,密碼等信息,所以在程序中使用的時候不需要傳入這些信息。
JDBC DAO中ConnectionManager定義
Connection用來專門管理數(shù)據(jù)庫鏈接,通常情況下ConnectionManager只有一個方法,調(diào)用這個方法將返回一個Connection的實例。通過ConnectionManager可以封裝Connection的獲取方式(例如開發(fā)的時候使用DriverManager,運用的時候使用DataSource的方式,但是不需要修改ConnectionManager之外的其他代碼)和追加Connection獲取之前之后的操作(例如針對Connection的屬性的設(shè)置)。
下面的代碼是一個ConnectionManager的代碼示例:
- packagecom.jpleasure.jdbc.dao;
- importjava.sql.Connection;
- importjava.sql.DriverManager;
- importjava.sql.SQLException;
- publicclassConnectionManager{
- publicstaticConnectiongetConnection()throwsDaoException{
- Connectionconn=null;
- try{
- conn=DriverManager.getConnection("","","");
- }catch(SQLExceptione){
- thrownewDaoException("cannotgetdatabaseconnection",e);
- }
- returnconn;
- }
- }
如果需要從開發(fā)模式變?yōu)檫\用模式,只需要將上述代碼修改為:
- packagecom.jpleasure.jdbc.dao;
- importjava.sql.Connection;
- importjava.sql.DriverManager;
- importjava.sql.SQLException;
- publicclassConnectionManager{
- publicstaticConnectiongetConnection()throwsDaoException{
- Connectionconn=null;
- try{
- Contextctx=newInitialContext();
- DataSourceds=(DataSource)ctx.lookup("jdbc/dsname");
- conn=ds.getConnection();
- }catch(NamingExceptione){
- thrownewDaoException("cannotfinddatasource",e);
- }catch(SQLExceptione){
- thrownewDaoException("cannotgetdatabaseconnection",e);
- }
- returnconn;
- }
- }
- 如果需要預(yù)先設(shè)定Connection的一些屬性,也可以在上述代碼中設(shè)定,例如:
- packagecom.jpleasure.jdbc.dao;
- importjava.sql.Connection;
- importjava.sql.DriverManager;
- importjava.sql.SQLException;
- publicclassConnectionManager{
- publicstaticConnectiongetConnection()throwsDaoException{
- Connectionconn=null;
- try{
- Contextctx=newInitialContext();
- DataSourceds=(DataSource)ctx.lookup("jdbc/dsname");
- conn=ds.getConnection();
- conn.setAutoCommit(false);
- }catch(NamingExceptione){
- thrownewDaoException("cannotfinddatasource",e);
- }catch(SQLExceptione){
- thrownewDaoException("cannotgetdatabaseconnection",e);
- }
- returnconn;
- }
- }
- CommonDao定義
- 屬性和構(gòu)造方法
- 通常情況下,CommonDao要有一個Connection的引用。所有一個CommonDao的實例的所有方法的調(diào)用都需要依賴于這個Connection。需要一個Connection的另外一個原因是如果各個方法需要保證在一個事務(wù)環(huán)境中(上下文中),必須保證所有的操作都在一個Connection上。
- 構(gòu)造方法通常需要將類型為Connection的屬性實例化,例如:
- packagecom.jpleasure.jdbc.dao;
- importjava.sql.Connection;
- publicclassCommonDao{
- privateConnectionconn;
- publicCommonDao()throwsDaoException{
- this.conn=ConnectionManager.getConnection();
- }
- }
事務(wù)方法
begin()
開始一個事務(wù),調(diào)用CommonDao的begin方法之后,所以的后續(xù)操作將會在一個事務(wù)環(huán)境內(nèi),要么全部提交,要么全部回滾。
commit()
提交一個事務(wù),必須在begin調(diào)用之后調(diào)用。且和rollback方法互斥。
rollback()
回滾一個事務(wù),必須在begin方法調(diào)用之后調(diào)用。且和commit方法互斥。
事務(wù)的實現(xiàn)有兩種方法,一種是使用基于單一Connection的事務(wù),另外一種方法是使用容器的JTA(JavaTransactionAPI)。需要注意的是***種方法可以在任何環(huán)境下使用,但是只能是針對單一的數(shù)據(jù)庫鏈接。第二種方法智能在支持JTA的JavaEE容器中使用(例如Websphere,Weblogic等,Tomcat默認不支持),但是支持多個Connection實例。
***種方法代碼為:
- packagecom.jpleasure.jdbc.dao;
- importjava.sql.Connection;
- importjava.sql.SQLException;
- publicclassCommonDao{
- privateConnectionconn;
- publicCommonDao()throwsDaoException{
- this.conn=ConnectionManager.getConnection();
- }
- publicvoidbegin()throwsDaoException{
- if(conn!=null){
- try{
- conn.setAutoCommit(false);
- }catch(SQLExceptione){
- thrownewDaoException("cannotbegintransaction",e);
- }
- }else{
- thrownewDaoException("connectionnotopened!");
- }
- }
- publicvoidcommit()throwsDaoException{
- try{
- if(conn!=null&&!conn.getAutoCommit()){
- conn.commit();
- conn.setAutoCommit(true);
- }else{
- if(conn==null){
- thrownewDaoException("connectionnotopened!");
- }else{
- thrownewDaoException("firstbeginthencommitplease!");
- }
- }
- }catch(SQLExceptione){
- thrownewDaoException("cannotcommittransaction!",e);
- }
- }
- publicvoidrollback()throwsDaoException{
- try{
- if(conn!=null&&!conn.getAutoCommit()){
- conn.rollback();
- conn.setAutoCommit(true);
- }else{
- if(conn==null){
- thrownewDaoException("connectionnotopened!");
- }else{
- thrownewDaoException("firstbeginthenrollbackplease!");
- }
- }
- }catch(SQLExceptione){
- thrownewDaoException("cannotrollbacktransaction!",e);
- }
- }
- }
第二種我們在使用DAO的實例中介紹如何使用(@TODO)
新建兩個DAO,做不同的操作,使用JTA保證事務(wù)完整。
查詢方法
查詢方法也許是CommonDao最常用的方法,查詢方法需要將數(shù)據(jù)庫的結(jié)果返回給畫面。返回值我們一般不使用ResultSet,因為ResultSet依賴于Connection,如果Connection關(guān)閉,ResultSet將不再有效,所以我們通常將ResultSet轉(zhuǎn)變?yōu)橐粋€List之后返回。
在說明查詢方法之前,我們先說說如何將數(shù)據(jù)庫中的內(nèi)容放在List中,我們使用一個List表示一個查詢結(jié)果集合,使用一個Map表示集合中的一行,Map的key表示數(shù)據(jù)庫表的字段名字,Value表示數(shù)據(jù)庫字段的內(nèi)容。代碼為:
- privateListconvert(ResultSetrs)throwsDaoException{
- //recordlist
- ListretList=newArrayList();
- try{
- ResultSetMetaDatameta=rs.getMetaData();
- //columncount
- intcolCount=meta.getColumnCount();
- //eachrecord
- while(rs.next()){
- MaprecordMap=newHashMap();
- //eachcolumn
- for(inti=1;i<=colCount;i++){
- //columnname
- Stringname=meta.getColumnName(i);
- //columnvalue
- Objectvalue=rs.getObject(i);
- //addcolumntorecord
- recordMap.put(name,value);
- }
- //adrecordtolist
- retList.add(recordMap);
- }
- }catch(SQLExceptionex){
- thrownewDaoException("cannotconvertresultsettolistofmap",ex);
- }
- returnretList;
- }
為了避免Sql注入的安全問題,我們通常使用PreparedStatement,在使用PreparedStatement的時候涉及到如何將傳入?yún)?shù)設(shè)置到PreparedStatement上面,參看以下的共通方法:
- privatevoidapply(PreparedStatementpstmt,Listparams)throwsDaoException{
- try{
- //ifparamsexist
- if(params!=null&¶ms.size()>0){
- //parametersiterator
- Iteratorit=params.iterator();
- //parameterindex
- intindex=1;
- while(it.hasNext()){
- Objectobj=it.next();
- //ifnullset""
- if(obj==null){
- pstmt.setObject(index,"");
- }else{
- //elsesetobject
- pstmt.setObject(index,obj);
- }
- //nextindex
- index++;
- }
- }
- }catch(SQLExceptionex){
- thrownewDaoException("cannotapplyparameter",ex);
- }
- }
- 接著我們繼續(xù)說我們的查詢方法,有了上述兩個方法,我們的查詢方法就非常簡單了:
- publicListquery(Stringsql,Listparams)throwsDaoException{
- Listresult=null;
- PreparedStatementpstmt=null;
- ResultSetrs=null;
- try{
- pstmt=conn.prepareStatement(sql);
- this.apply(pstmt,params);
- rs=pstmt.executeQuery();
- result=this.convert(rs);
- }catch(SQLExceptionex){
- thrownewDaoException("cannotexecutequery",ex);
- }finally{
- if(rs!=null){
- try{
- rs.close();
- }catch(SQLExceptione){
- //nothing
- }
- }
- if(pstmt!=null){
- try{
- pstmt.close();
- }catch(SQLExceptione){
- //nothing
- }
- }
- }
- returnresult;
- }
特殊的查詢方法(返回單值)
有時候為了方便使用,我們需要返回單值的產(chǎn)尋方法,例如selectmax(id)fromtable_a,selectcount(id)fromtable_b等。以下的代碼使用了上述通用的查詢方法,代碼為:
- publicObjectqueryOne(Stringsql,Listparams)throwsDaoException{
- Listlist=this.query(sql,params);
- if(list==null||list.size()==0){
- thrownewDaoException("datanotexist");
- }else{
- Maprecord=(Map)list.get(0);
- if(record==null||record.size()==0){
- thrownewDaoException("datanotexist");
- }else{
- returnrecord.values().toArray()[0];
- }
- }
- }
更新,刪除,插入方法
由于在JDBC中這三個方法都是用了一個execute完成,所以這里我們也使用一個方法來完成這些功能。代碼為:
- publicintexecute(Stringsql,Listparams)throwsDaoException{
- intret=0;
- PreparedStatementpstmt=null;
- try{
- pstmt=conn.prepareStatement(sql);
- this.apply(pstmt,params);
- ret=pstmt.executeUpdate();
- }catch(SQLExceptionex){
- thrownewDaoException("",ex);
- }finally{
- if(pstmt!=null){
- try{
- pstmt.close();
- }catch(SQLExceptione){
- //nothing.
- }
- }
- }
- returnret;
- }
批處理方法(查詢)
有些時候為了便于操作,需要一次查詢多條SQL語句,我們稱之為批處理,實現(xiàn)參看以下方法,其中為了和query方法做區(qū)分,將參數(shù)和返回值都改為了數(shù)組形式。
- publicList[]queryBatch(String[]sqlArray,List[]paramArray)throwsDaoException{
- Listrets=newArrayList();
- if(sqlArray.length!=paramArray.length){
- thrownewDaoException("sqlsizenotequalparametersize");
- }else{
- for(inti=0;iStringsql=sqlArray[i];
- Listparam=paramArray[i];
- Listret=this.query(sql,param);
- rets.add(ret);
- }
- return(List[])rets.toArray();
- }
- }
批處理方法(更新)
有些時候需要一次更新多條Sql語句,為了便于操作,添加了批處理更新操作,參看以下代碼,為了和更新方法區(qū)分,將參數(shù)和返回值都改為了數(shù)組形式。
- publicint[]executeBatch(String[]sqlArray,List[]paramArray)throwsDaoException{
- Listrets=newArrayList();
- if(sqlArray.length!=paramArray.length){
- thrownewDaoException("sqlsizenotequalparametersize");
- }else{
- for(inti=0;iintret=this.execute(sqlArray[i],paramArray[i]);
- rets.add(newInteger(ret));
- }
- int[]retArray=newint[rets.size()];
- for(inti=0;iretArray[i]=((Integer)rets.get(i)).intValue();
- }
- returnretArray;
- }
- }
資源釋放
由于CommonDao有一個Connection的屬性,且Connection屬于稀缺資源,所以在CommonDao不需要在使用的時候需要顯示的關(guān)閉Connection。代碼如下:
- publicvoidclose()throwsDaoException{
- try{
- if(conn!=null&&conn.getAutoCommit()){
- conn.close();
- }else{
- if(conn==null){
- thrownewDaoException("cannotclosenullconnection,firstnewthenclose");
- }else{
- thrownewDaoException("transactionisrunning,rollbakcorcommitbeforcloseplease.");
- }
- }
- }catch(SQLExceptionex){
- thrownewDaoException("Cannotclosecommondao");
- }
- }
JDBC工具類(JDBCUtilClass)
在上述的代碼中我們看到有很多的無用的處理,例如:
- if(pstmt!=null){
- try{
- pstmt.close();
- }catch(SQLExceptione){
- //nothing.
- }
- }
為什么要有這些處理呢?說先這些處理發(fā)生的位置都是在正常處理完成之后,這些處理(例如pstmt.close())即使失敗也沒有影響,這個時候我們需要做上述的無用處理,這正是JDBCAPI的一個小小的瑕疵。我們通常使用一個特殊的靜態(tài)工具來來做補充,例如:
- packagecom.jpleasure.jdbc.dao;
- importjava.sql.Connection;
- importjava.sql.PreparedStatement;
- importjava.sql.ResultSet;
- importjava.sql.SQLException;
- publicclassJDBCUtil{
- publicvoidsafelyClose(Connectionconn){
- if(conn!=null){
- try{
- conn.close();
- }catch(SQLExceptione){
- //
- }
- }
- }
- publicvoidsafelyClose(PreparedStatementpstmt){
- if(pstmt!=null){
- try{
- pstmt.close();
- }catch(SQLExceptione){
- //
- }
- }
- }
- publicvoidsafelyClose(ResultSetrs){
- if(rs!=null){
- try{
- rs.close();
- }catch(SQLExceptione){
- //
- }
- }
- }
- }
JDBC DAO中異常處理
也許細心的你已經(jīng)發(fā)現(xiàn)了一個問題,為什么所有拋出異常的地方我們都是將SQLException包裝在了DaoException之內(nèi)拋出呢,為什么不直接拋出SQLException呢?有兩個原因,***,可以細化,分類Exception拋出合適的異常,添加合適的消息,第二,隔離和Dao和業(yè)務(wù)邏輯的耦合,可以方便的修改Dao層而不會影響到業(yè)務(wù)邏輯層。另外需要注意,DaoExcetion中可以包含SQLException,這個時候可以為客戶提供更詳細的錯誤信息,例如ORA-12524等內(nèi)容,但是很少見到。
- packagecom.jpleasure.jdbc.dao;
- publicclassDaoExceptionextendsException{
- publicDaoException(){
- super();
- }
- publicDaoException(Stringmessage,Throwablecause){
- super(message,cause);
- }
- publicDaoException(Stringmessage){
- super(message);
- }
- publicDaoException(Throwablecause){
- super(cause);
- }
- }
【編輯推薦】