自己動手:做個數據庫訪問層(一)
說資料庫是資訊系統裡最重要的部分,應當沒幾個人反對。最簡單的訪問資料庫的方式就是用程式直連資料庫,通過Sql進行操作,相信這也是每個程式設計師最初學的方法。但隨著程式規模的增大,再一條條語句去寫的話開發效率就有些低了,因此才有了很多框架去幫助我們操作資料庫,比較流行的有Mybatis和Hibernate,也就是所謂的ORM。對於這兩個工具我也不是很熟悉,只是接觸過網上的一些教程,從網上接觸的教程來看,感覺還是有些太複雜了,尤其是對於分頁和複雜條件查詢。因此就想自己動手做一個,不求大而全,只希望能完成以下功能:
1. 不用寫Sql,能直接儲存Java物件,還有更新與刪除。
2. 支援多步查詢,比如傳遞一個Sql陣列,第一行的結果可以在第二行的查詢中使用,這樣就不用手動寫複雜的儲存過程,生成臨時表了,也不用寫複雜的子查詢,程式碼更清晰,比如下面的語句
+zuZhiList select Id from ZuZhi where TypeId = 1 select r.* From RenYuan r join @zuZhiList z on z.Id = r.ZuZhiId
3. 支援條件查詢,比如下面的語句
+zuZhiList select Id from ZuZhi where 1=1 {@typeId: and TypeId = @typeId} select r.* From RenYuan r join @zuZhiList z on z.Id = r.ZuZhiId
執行時以HashMap<String, Object>儲存命名引數,如果該Map裡沒有名為typeId的引數,或者值為Null,那麼就不拼接" and TypeId = @typeId"這部分。
4. 支援自動分頁,自動分析Sql語句,如果有分頁關鍵字,就拆分成兩條,一條返回總數,一條返回分頁記錄,比如以下語句
Select * From RenYuan where ZuZhiId = 1 page 0, 10
在執行時會被翻譯成兩條語句
Select count(*) totalCount From RenYuan where ZuZhiId = 1 Select * From RenYuan where ZuZhiId = 1 limit 0, 10
5. 能支援多種資料庫,如SqlServer,MySql,Oracle
實現思路是這樣的:
0. 寫幾個基礎的支援方法,能夠連線資料庫,執行Sql,如果是查詢的話還可以返回多個結果集。
1. 用反射去做,拼接Sql。
2. 如果Sql以加號開頭,就建立一個臨時表,臨時表的欄位如果Sql裡有定義就用Sql裡定義的,沒有的話就預設為(Id int),然後把後面的select裡的內容插入到臨時表中。把建立的臨時表名稱儲存在一個集合裡,後面的Sql裡的引數名稱如果在這個集合裡,就替換成臨時表的名稱,沒有的話說明是一個普通引數。
3. 查詢語句裡用大括號包著的部分,再從第一部分裡取出引數名稱,查詢命名引數裡有沒有該Key或者值為不為空,如果不為空,就拼接第二部分的Sql,否則就忽略。
4. 同理,查詢關鍵字,找到From後面的部分,在前面新增select count
5. 根據不同的語法,替換成不同的內容。
以下是第0部分的實現:
定義一個抽象類BaseRepository,將來根據不同的資料庫再定義相應的Repository如MySqlRepository,SqlServerRepository,該類裡包含兩個抽象方法,一個是getDataSource(),即獲取連線池的連線,該屬性將由Spring注入進來。另外一個是getScriptParser(),返回不同資料庫對應的Sql處理類,如MySqlScriptParser,SqlServerScriptParser,另外還有一個DaoUtil類,主要用於處理引數賦值,結果集對映等輔助工作。
public abstract class BaseRepository { //獲取資料庫連線,由Spring注入進來 protected abstract DataSource getDataSource(); //獲取Sql分析例項 protected abstract ScriptParser getScriptParser(); //輔助類 public abstract DaoUtil getDaoUtil(); //引數Sql即執行的一條Sql,對於Oracle只能是單條語句,對於MySql和SqlServer,可以是用分號分割開的多條語句,paras即Sql裡用?表示的佔位引數的值 protected DataSet executeRetDataSet(String sql, Object... paras) { //DataSet為輔助類,包含多個DataTable,DataTable也是輔助類,包含一個名為rows的ArrayList<ArrayList<String, Object>>物件,是從C#借鑑過來的 DataSet result = new DataSet(); Connection conn = null; Statement stmt = null; try { conn = DataSourceUtils.getConnection(this.getDataSource()); ResultSet rs = null; if (paras.length == 0) { stmt = conn.createStatement(); stmt.execute(sql); } else { //此處是根據反射,檢視引數的型別,進行特殊處理,如boolean要對應成資料庫裡的1或者0,Date型別要呼叫Statement的setTimestamp方法 PreparedStatement pstmt = getDaoUtil().getCallableStatement(conn, sql, paras); pstmt.execute(); stmt = pstmt; } do { //處理結果集 if (stmt.getUpdateCount() == -1) { rs = stmt.getResultSet(); if (rs != null) { result.tables.add(getDaoUtil().mapResultSetToDataTable(rs)); rs.close(); } } } while (stmt.getMoreResults() || stmt.getUpdateCount() > -1); stmt.close(); } catch (Exception ex) { throw new RuntimeException(ex); } finally { if (stmt != null) { JdbcUtils.closeStatement(stmt); } if (conn != null) { DataSourceUtils.releaseConnection(conn, this.getDataSource()); } } return result; } protected void executeNoRet(String sql, Object... paras) { Connection conn = null; Statement stmt = null; try { conn = DataSourceUtils.getConnection(this.getDataSource()); if (paras.length == 0) { stmt = conn.createStatement(); stmt.execute(sql); } else { PreparedStatement pstmt = getDaoUtil().getCallableStatement(conn, sql, paras); pstmt.execute(); stmt = pstmt; } stmt.close(); } catch (Exception ex) { throw new RuntimeException(ex); } finally { if (stmt != null) { JdbcUtils.closeStatement(stmt); } if (conn != null) { DataSourceUtils.releaseConnection(conn, this.getDataSource()); } } } //該方法接收一個Sql陣列,paras為命名引數,對於同一個引數,Sql裡可能出現多次,但paras只需要出現一次就可以。ScriptParser會把Sql裡以@開頭的引數替換成問號,並且組裝出數量順序一致的用於執行語句的引數陣列。 public DataSet executeQuery(ArrayList<String> scripts, Map<String, Object> paras) { //TupleTow為輔助類,類似其他語言裡的元組 TupleTwo<String, ArrayList<Object>> ret = getScriptParser().parseScripts(scripts, paras); return executeRetDataSet(ret.getItem1(), ret.getItem2().toArray()); } public <T> void save(T item) { Class<T> cls = (Class<T>) item.getClass(); save(cls, item); } public <T> void save(Class<T> cls, T item) { throw new NotImplementedException(); } public <T> T get(Class<T> cls, int id) { throw new NotImplementedException(); } }