1. 程式人生 > >自己動手:做個數據庫訪問層(一)

自己動手:做個數據庫訪問層(一)

       說資料庫是資訊系統裡最重要的部分,應當沒幾個人反對。最簡單的訪問資料庫的方式就是用程式直連資料庫,通過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();
    }
}