天天看點

基于iBatis的通用持久層對象

ibatis介紹

  使用ibatis 提供的ORM機制,對業務邏輯實作人員而言,面對的是純粹的Java對象, 這一層與通過Hibernate 實作ORM 而言基本一緻,而對于具體的資料操作,Hibernate 會自動生成SQL 語句,而ibatis 則要求開發者編寫具體的SQL 語句。相對Hibernate等 “全自動”ORM機制而言,ibatis 以SQL開發的工作量和資料庫移植性上的讓步,為系統 設計提供了更大的自由空間。作為“全自動”ORM 實作的一種有益補充,ibatis 的出現顯 得别具意義。

一、為什麼要設計“通用”的東西

  在大多數時候,我們所需要的持久層對象(PO)大多都是一張表(or視圖)對應一個類。按照Hibernate的思想,就是抛開資料庫的束縛,把焦點集中到業務對象中。而很多自動化工具的确讓做到了通過表結構生成對應的對象,or通過對象自動生成表。對于小項目來說,一切都是簡單的;對于有規範設計的項目來說,PO的設計也不是一件困難的工作。但是對于那些業務變動頻繁的項目來說,改動PO可能成了一件很繁重的工作。試想一下,假設某個表需要增加一個字段:對于Hibernate(or iBaits),首先要改配置檔案,然後PO,然後DAO(也許沒有),然後業務邏輯,然後JO,然後界面,etc,貫通了全部層次。

  恩,寫程式的都不喜歡這些重複勞動,但是做企業級應用的誰不是每天在這些工作中打滾。

  研究過iBaits以後,發現有些通用的方法可以解決,就是設計一個通用的持久層對象。

二、基于什麼技術

  iBatis可以使用Map對象作為PO,Hibernate好像也有相關的功能(我沒有細看,不确定)。

  iBatis執行一條指令的過程大概是這樣的:

基于iBatis的通用持久層對象

其中圈圈1、2、3描述了iBatis最重要的三個對象。

圈圈1:statement簡單來說就是存儲sql語句的配置資訊,一個最簡單的statement:

<statement id=”insertTestProduct” >

    insert into PRODUCT (PRD_ID, PRD_DESCRIPTION) values (1, “Shih Tzu”)

</statement>

其中id屬性是這個statement的唯一辨別,全局不能重複。

以上當然是最簡單的了,沒有參數也不需要傳回值,但實際情況下基本都需要傳入參數,下面就是介紹參數。

圈圈2:參數對象主要分兩種類型:parameterMap、parameterClass和Inline Parameter。

  其中parameterMap是配置檔案定義傳入參數表,如下:

<parameterMap id=”insert-product-param” class=”com.domain.Product”>

</parameterMap>

    <statement id=”insertProduct” parameterMap=”insert-product-param”>

        insert into PRODUCT (PRD_ID, PRD_DESCRIPTION) values (?,?);

    </statement>

而parameterClass是傳入參數對象(JavaBean),如下:

<statement id=”statementName” parameterClass=” examples.domain.Product”>

    insert into PRODUCT values (#id#, #description#, #price#)

</statement>

Inline Parameter則是強化版的parameterClass,如下:

<statement id=”insertProduct” parameterClass=”com.domain.Product”>

    insert into PRODUCT (PRD_ID, PRD_DESCRIPTION)

        values (#id:NUMERIC:-999999#, #description:VARCHAR:NO_ENTRY#);

</statement>

其中第一種方法看着就複雜,實際是為了相容老版本留下來的,是以parameterClass是我們最常用的方法。官方文檔對parameterClass介紹很詳細,因為這是核心之一,具體請自己查閱。有3個特性說明一下:

  a. parameterClass對象可以傳入一個Map對象(or Map子類)。本來如果是傳入JavaBean,程式會通過get/set來分析取得參數;而Map是key-value結構的,那程式會直接通過key來分析取參數。

  b. 看以下語句:

<statement id=”statementName” parameterClass=” examples.domain.Product”>

    insert into PRODUCT values (#id#, #description#, #price#, #classify.id#)

</statement>

藍色部分#classify.id#翻譯過來實際是product.getClassify().getId(),classify是Product對象的一個子對象。

  c. 在模闆sql語句中除了“#”以外,還有“$”,它們兩代表的意思當然不同了:

<statement id=”getProduct” resultMap=”get-product-result”>

    select * from PRODUCT order by $preferredOrder$

</statement>

“#”在生成sql語句的過程中,會變成“?”,同時在參數表中增加一個參數;

  “$”則會直接替換成參數對象對應的值,例如上面的preferredOrder的值可能是“price”,則生成的sql語句就是:select * from PRODUCT order by price。

  *需要特别說明的是傳入參數這一部分将會是後面正題“通用持久層對象”的核心,怎麼個通用法,怎麼設計模闆sql語句,都是在這部分上。

圈圈3:結果對象跟參數對象差不多,也有兩種,resultMap和resultClass,如下:

  resultMap就是配置檔案中預定義了要取得的字段:

<resultMap id=”get-product-result” class=”com.ibatis.example.Product”>

    <result property=”id” column=”PRD_ID”/>

    <result property=”description” column=”PRD_DESCRIPTION”/>

</resultMap>

<statement id=”getProduct” resultMap=”get-product-result”>

    select * from PRODUCT

</statement>

resultClass則是通過分析傳回的字段,來填充結果對象:

<statement id="getPerson" parameterClass=”int” resultClass="examples.domain.Person">

    SELECT PER_ID as id, PER_FIRST_NAME as firstName

        FROM PERSON WHERE PER_ID = #value#

</statement>

跟參數對象相反,結果對象一般使用resultMap形式。引用官方的話:使用resultClass的自動映射存在一些限制,無法指定輸出字段的資料類型(如果需要的話),無法自動裝入相關的資料(複雜屬性),并且因為需要ResultSetMetaData的資訊,會對性能有輕微的不利影響。但使用resultMap,這些限制都可以很容易解決。

三、正題來了,怎麼做“通用持久層對象”

1. 表結構:

  每個表都必須包含兩個字段:id和parentId,其他字段按照需求來定義,其他各種索引、限制、關系之類的也按需求定義。

2. 通用的持久層對象,CustomPO:

public class CustomPO {

    protected String moduleTable;  //該PO對應的表名(視圖名)

    protected int id;  //表的id

    protected int parentID;  //父表的id(如果有的話)

    protected Map fieldMap;  //字段Map,核心,用于存儲字段及其值

    public String getModuleTable()

    public void setModuleTable(String moduleTable)

    public int getId()

    public void setId(int id)

    public int getParentID()

    public void setParentID(int parentID)

    public Map getFieldMap()

    public void setFieldMap(Map fieldMap)

    public void copyFieldMap(Map fieldMap)

    //取得字段名清單

    public List getFieldList()

    //設定字段名清單。如果fieldMap沒有相應的字段,則增加,字段值為null;如果有則不增加。

    public void setFieldList(List fieldList)

    //傳回字段的“字段名 - 字段值”清單,使用com.fellow.pub.util.KeyValuePair對象作為存儲

    public List getFieldValueList()

}

那些成員變量的get/set就沒什麼說的,主要說說getFieldValueList()這個方法。該方法傳回一個清單,清單元素是一個key-value結構,簡單來說就是把字段map序列化。在構造模闆sql語句時會展現它的用途。

3. iBatis對象配置檔案CustomPO.xml:

<sqlMap namespace="CustomPO">

  <!--定義别名-->

  <typeAlias alias="customPO" type="com.fellow.component.customPO.CustomPO"/>

    <!--

        通過id查找

        特點:iterate這個fieldList清單,生成要輸出的字段

    -->

  <select id="customPO_findByID" resultClass="java.util.HashMap" parameterClass="customPO">

    SELECT id, parentID

      <iterate property="fieldList" conjunction=",">

        $fieldList[]$

      </iterate>

     FROM $moduleTable$ WHERE id = #id#

  </select>

    <!--

        插入一條新紀錄

        特點:iterate這個fieldValueList清單,分别取得其元素的key和value值

        注意$号和#号的使用方法,還有最後怎麼取得insert後的id值(各種資料庫都可能不同)

    -->

  <insert id="customPO_insert" parameterClass="customPO">

    INSERT INTO $moduleTable$ (parentID

      <iterate property="fieldValueList" prepend="," conjunction=",">

        $fieldValueList[].key$

      </iterate>

      )

    VALUES (#parentID#

      <iterate property="fieldValueList" prepend="," conjunction=",">

        #fieldValueList[].value#

      </iterate>

      )

    <selectKey resultClass="int" keyProperty="id">

      SELECT last_insert_id()

    </selectKey>

  </insert>

    <!--

        更新一條紀錄

        特點:iterate這個fieldValueList清單,分别取得其元素的key和value值

        注意$号和#号的使用方法

    -->

  <update id="customPO_update" parameterClass="customPO">

    UPDATE $moduleTable$ SET

      <iterate property="fieldValueList" conjunction=",">

        $fieldValueList[].key$ = #fieldValueList[].value#

      </iterate>

     WHERE id = #id#

  </update>

    <!--删除一條紀錄-->

  <delete id="customPO_delete" parameterClass="customPO">

    DELETE FROM $moduleTable$ WHERE id = #id#

  </delete>

要注意的地方如下:

  a. 跟一般的ibatis配置檔案不一樣,該配置中沒有包含resultMap,使用的就是resultClass的方式(效率沒那麼高的那種)。當然,也可以使用resultMap,這樣就要為每個表寫自己的配置檔案了。是以,在該設計沒完成前,我暫時先使用resultClass的方式。

  b. 上面隻列舉了最簡單的增删改以及按id查詢,并沒有更複雜的查詢,為什麼呢?因為我還在研究中。。。研究通用的模闆sql的寫法。

4. CustomPO對應的DAO:

  我使用了ibaits提供的DAO架構,很好用,不單支援ibatis的架構,還支援Hibernate、JDBC等等,而且是與ibatis本身獨立的,完全可以單獨使用。以後就不用自己寫DAO架構了。一下是該DAO接口:

public interface ICustomDAO {

    CustomPO findByID(String moduleTable, int id) throws Exception;

    CustomPO findByID(CustomPO po) throws Exception;

    CustomPO findByParentID(String moduleTable, int parentID) throws Exception;

    void insert(CustomPO po) throws Exception;

    void update(CustomPO po) throws Exception;

    void delete(String moduleTable, int id) throws Exception;

}

我沒有把所有的方法都列出來,反正挺簡單的,跟一般的DAO沒什麼分别。

  另外列幾個實作的片斷:

  a. 統一的資料裝填函數,需要手工把id和parentID字段去掉。

    protected void fill(Map result, CustomPO po) {

        Long returnId = (Long) result.get("id");

        Long returnParentID = (Long) result.get("parentID");

        result.remove("id");

        result.remove("parentID");

        if (returnId != null) po.setId(returnId.intValue());

        if (returnParentID != null) po.setParentID(returnParentID.intValue());

        po.setFieldMap(result);

    }

b. 一般的查詢,傳回的是一個map,然後再用fill()函數

    //查詢

     Map result = (Map)this.queryForObject("customPO_findByID", po);

    //處理傳回結果

    if(result == null)

        po = null;

    else

        fill(result, po);

c. 增删改,沒有傳回值,值得一提的是增加操作完成後,po裡面的id會更新,具體看前面相關的statement。

    //增删改

    this.insert("customPO_insert", po);

    this.update("customPO_update", po);

    this.delete("customPO_delete", po);

5. 前面是通用的部分,光是通用是不夠的。是以我另外建立了一套配置檔案,記錄字段對應關系。看看我所定義的一個配置檔案,挺簡單的:

 <struct name="userInfo" table-name="tblUserInfo">

    <field name="昵稱" column="NICK_NAME" type="string" not-null="true" unique="false" />

    <field name="姓名" column="FULL_NAME" type="string" not-null="true" unique="false" />

    <field name="性别" column="SEX" type="string" not-null="false" unique="false" />

</struct>

其中,name是字段名,column是字段對應資料表的字段名,type是字段類型,not-null是是否不能為空,unique是是否唯一。隻有name這個屬性是必填的,column如果不填預設與name相等,type預設為string,not-null和unique預設為false。

  配置檔案的讀取類在這裡就省略了。

  為什麼要自己定義一套這個架構?好像挺多此一舉的,但是沒辦法,ibatis配置檔案的資訊是封閉的,我無法取得。另外我考慮的是:

  a. viewer層:在web開發中,我可以在這套配置架構的基礎上,建立自己的标簽,實作資料自動綁定的功能;GUI開發中也可以做相應的功能。

  b. module層:可以做通用的業務操作,不需要為每個業務都都做獨立的業務邏輯。

四、有什麼優點、缺陷

1. 優點:

 a. “通用”,一切都是為了通用,為了減少重複勞動,一個個項目面對不同的業務,其實說到底都是那些操作。各種持久成架構已經帶給我們不少的友善,但是在實際業務邏輯的處理上好像還沒有一個這樣的架構。

 b. 極大地減少代碼量。前面說了,資料庫改一個字段,PO、DAO、module、JO、viewer、validator全都要改一遍。使用了這套東西,可以把絕大部分的勞動都放在配置檔案和UI上。當然,這是完美的設想,對于很多情況,業務邏輯還是要手工寫的。

 c. 好像沒有c了。

2. 缺點:

 a. 通常通用的東西都缺乏靈活性,在我的實際應用中也發現了這樣那樣的問題,解決方法都是以通用為基本原則。但是如果針對的是某個項目,那就可以針對業務來修改了。

 b. 性能問題。因為使用resultClass,按照文檔所說的,性能沒有resultMap好。當然也可以使用resultMap,如前所說,就要對每個PO寫配置檔案了,工作量也不少。

 c. 也好像沒有c了。

五、後話

  我總是喜歡寫一些通用的東西,總是想把它設計成萬能的。但是經過多次失敗總結出來,我發現通用的東西在很多情況下都等于不能用。但我就是喜歡往通用方面想,這個毛病不知道什麼時候才能改得了。其實在Delphi平台中,我早就實作了相關的東西,但是用delphi總是限制于ADO+Data Module這樣掉牙的模式。現在轉向java,發現有ibatis、hibernate這麼多好的持久層架構,自然有移植必要了。