(轉載)擴展 iBatis 以透明支持多種數據庫

擴展 iBatis 以透明支持多種數據庫

developerWorks
<script language="JavaScript" type="text/javascript"> </script>
文檔選項
</noscript>
<noscript>

未顯示需要 JavaScript 的文檔選項

<script language="JavaScript" type="text/javascript"> </script>
將打印機的版面設置成橫向打印模式

打印本頁

將此頁作爲電子郵件發送

將此頁作爲電子郵件發送

樣例代碼


級別: 中級

張 琦煒 ([email protected]), 軟件工程師, IBM 中國軟件開發中心

2007 年 12 月 13 日

iBatis 是一個開源的對象關係映射框架,着重於 POJO 與 SQL 之間的映射關係。和其它 ORM 框架不同,iBatis 開發者需要自己編寫和維護 SQL 語句。爲了得到更好的執行性能,在實際開發中免不了會使用一些數據庫方言。隨之而來的一個問題是,如何在增加對新的數據庫支持的同時儘可能避免對已有應用 程序代碼的修改?本文提供了一個簡單有效的方法,通過擴展 iBatis 來透明地支持多數據庫方言。

iBatis 簡介

iBatis 是一個開源的對象關係映射程序,着重於 POJO 與 SQL 之間的映射關係。使用時,開發者提供一個被稱爲 SQL 映射的 XML 文件,定義程序對象與 SQL 語句間的映射關係, iBatis 會根據 SQL 映射文件的定義,運行時自動完成 SQL 調用參數的綁定以及 JDBC ResultSet 到 Java POJO 之間的轉換。下面是一個簡單的例子,相比其它 ORM 工具,iBatis 相對簡單,更容易上手。


清單 1. POJO 對象
                
public class BlogData implements Serializable {
protected String id;
protected String name;
protected int rating = 0;
public String getId() {
return id;
}
public String getName() {
return name;
}
public int getRating() {
return rating;
}
public void setId(String id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setRating(int rating) {
this.rating = rating;
}
}


清單 2. SQL 映射文件—— SQLMAP.XML
                
<sqlmap namespace="blog">
<resultmap id="blog" class="BlogData">
<result property="id" column="id">
<result property="name" column="name">
<result property="rating" column="rating">
</resultmap>
<insert id="SAVEBLOG" parameterclass="BlogData">
insert into blogs(id, name, rating) values(#id#, #name#, #rating#)
</insert>
<update id="UPDATEBLOG" parameterclass="BlogData">
update blogs set name = #name#, rating = #rating# where id = #id#
</update>
<delete id="REMOVEBLOG" parameterclass="string">
delete from blogs where id = #id#
</delete>
select * from blogs order by rating desc fetch first $size$ rows only
</sqlmap>


清單 3. iBatis 配置文件—— SQLMAPCONFIG.XML
                
<sqlmapconfig>
<settings usestatementnamespaces="true">
<transactionmanager type="JDBC" commitrequired="true">
<datasource type="JNDI">
<property name="DataSource" value="java:comp/env/jdbc/db">
</datasource>
</transactionmanager>
//SQL 映射聲明
<sqlmap resource="SQLMAP.XML">
</sqlmapconfig>


清單 4. SQL 訪問示例
                
String cfg = "SQLMAPCONFIG.XML";
Reader r = Resources.getResourceAsReader(cfg);
SqlMapClient client = new SqlMapConfigParser().parse(r);
...
// 插入
BlogData o = new BlogData();
o.setId("id");
o.setName("test");
client.insert("SAVEBLOG", o);
...
// 更新
o.setRating(10);
client.update("UPDATEBLOG", o);
...
// 刪除
client.delete("REMOVEBLOG", "id");
...
// 查詢
Map params = new HashMap();
params.put(size, 5);
List l = client. queryForList("GETMOSTPOPULARBLOG", params, 0, 5);
...





回頁首


iBatis 應用中的多數據庫支持

在 iBatis 應用中,開發者仍需自己編寫具體的 SQL 語句,iBatis 只是隱藏和簡化了 JDBC 的相關調用。

實際開發中,我們不免需要就 SQL 語句針對各種特定的數據庫進行特殊優化,以期獲取更好的執行性能,隨之而來的一個問題是,如何應對新的數據庫平臺支持的需求。

一般的做法是,修改 SQL 映射文件,提供一些新的針對新數據庫平臺的 SQL 語句版本,然後修改程序代碼,添加相應調用。繼續上面的例子。上面的例子中,對於 SQL 語句 GETMOSTPOPULARBLOG 的定義,我們使用了 DB2 特有的 SQL 方言“FETCH FIRST n ROWS ONLY”,對於這樣的程序,如果希望增加對 MYSQL 的支持,按照一般的做法,需要:

1.修改 SQLMAP.XML,增加語句定義“GETMOSTPOPULARBLOG_MYSQL”。


清單 5. 增加語句定義
                
......
select * from blogs order by rating desc fetch first $size$ rows only
select * from blogs order by rating desc limit 0, $size$
......

2.搜索程序代碼,在每一個調用 iBatis “GETMOSTPOPULARBLOG”的地方,增加檢測 MYSQL 數據庫引擎的代碼,並添加對“GETMOSTPOPULARBLOG_MYSQL”的 iBatis 調用。


清單 6. 增加檢測數據庫引擎的代碼
                
......
SqlMapClient client = ...;
......
// 查詢
Map params = new HashMap();
params.put(size, 5);
List l = null;
Connection conn = client.getCurrentConnection();
String prodName = conn.getMetaData().getDatabaseProductName().toLowerCase();
if (prodName.indexOf("mysql") > - 1) {
//Microsoft SQL Server
l = client. queryForList("GETMOSTPOPULARBLOG_MYSQL", params);
} else {
l = client. queryForList("GETMOSTPOPULARBLOG", params);
}

......

每增加一個新的數據庫支持,增加了一些新 的針對新數據庫平臺的 SQL 語句版本,我們就不得不搜索源代碼,找出所有受到影響的 iBatis 調用,修改並增加針對新數據庫的特殊調用。代碼維護時,每次涉及使用數據庫方言的 SQL 語句,我們也都必須記住小心謹慎地處理所有相關的數據庫特化調用。這樣的工作乏味且容易出錯。

本文,我們試圖在分析 iBatis 源碼的基礎上,通過適當擴展 iBatis,提供一個高效方便的解決方案。





回頁首


擴展 SqlMapConfigParser

在 iBatis 應用中,SqlMapConfigParser 負責解析 iBatis 配置文件,加載所有的 SQL 映射文件,生成 SqlMapClient 實例,這是持久化調用的入口。

SqlMapConfigParser 的實現並不複雜。成員函數 parser 將傳入的配置文件 XML 輸入流交給一個 XML 解析器。XML 解析器解析 XML 輸入,並針對每一個 XML Fragment 調用合適的處理器處理。所有的處理器都在 SqlMapConfigParser 類實例初始化時預先被註冊到 XML 解析器上,其中,對於 iBatis 配置中的 SQL 映射聲明,只是簡單地調用類 SqlMapParser 中的 parser 方法,解析並加載相應的 SQL 映射定義文件。


清單 7. SqlMapConfigParser 實現
                
public class SqlMapConfigParser {
//XML 解析器
protected final NodeletParser parser = new NodeletParser();

public SqlMapConfigParser() {
......

// 註冊 XML 處理器
addSqlMapNodelets();
...... // more
}

public SqlMapClient parse(Reader reader) {
......
// 調用 XML 解析器解析傳入的配置文件 XML 輸入流
parser.parse(reader);
return vars.client;
}

protected void addSqlMapNodelets() {
//XML 處理器,處理 XPath:"/sqlMapConfig/sqlMap",即 SQL 映射聲明
parser.addNodelet("/sqlMapConfig/sqlMap", new Nodelet() {
public void process(Node node)throws Exception {
Properties attributes = NodeletUtils.parseAttributes(node);
String resource = attributes.getProperty("resource");
Reader reader = Resources.getResourceAsReader(resource);
new SqlMapParser(vars).parse(reader); // 調用 SqlMapParser.parser 方法
// 解析並加載 SQL 映射文件
......
}
}
);
}
......
}

我們繼承 iBatis 原有的配置文件解析器實現 SqlMapConfigParser,重寫其中對 SQL 映射聲明的處理。

首先,我們重寫 SqlMapConfigParser 的成員函數 addSqlMapNodelets。對於從 XML 解析器傳入的 SQL 映射聲明節點,我們並不立即進行解析處理,而只是將它們記錄下來。


清單 8. 重寫 addSqlMapNodelets 方法
                
public class SqlMapConfigParserEx extends SqlMapConfigParser {
List sqlMapNodeList = new ArrayList();
.......
protected void addSqlMapNodelets() {
//XML 處理器,處理 XPath:”/sqlMapConfig/sqlMap”,即 SQL 映射聲明
parser.addNodelet("/sqlMapConfig/sqlMap", new Nodelet() {
public void process(Node node)throws Exception {
sqlMapNodeList.addNode(node);
}
}
);
}
......
}

這些 SQL 映射聲明被放到最後處理,此時 SqlMapClient 實例已經基本構造完畢,至少,我們可以安全地調用它的相關方法,打開數據庫連接,查詢數據庫引擎相關信息。對於每個 SQL 映射聲明,SqlMapConfigParserEx 調用其成員函數方法 handleSqlMapNode 進行相應的 SQL 映射文件解析和加載處理,數據庫引擎支持的 SQL 方言版本信息作爲參數被一併傳入。


清單 9. 重寫 parse 方法
                
public interface DialectMapping {
public String getDialect(String productName);
// 返回數據庫平臺支持的 SQL 方言信息
}

public class SqlMapConfigParserEx extends SqlMapConfigParser {
List sqlMapNodeList = new ArrayList();
DialectMapping dialectMapping = ...;
.......
public SqlMapClient parse(Reader reader) {
......
super.parse(reader);
String sqlDialect = null;
SqlMapClient client = vars.client;
Connection conn = client.getDataSource().getConnection();
DatabaseMetaData dbMetaData = conn.getMetaData();
String productName = dbMetaData.getDatabaseProductName();
sqlDialect = dialectMapping.getDialect(productName);
conn.close();

for (Iterator iter = sqlMapNodeList.iterator(); iter.hasNext();) {
handleSqlMapNode((Node)iter.next(), sqlDialect);
}

return client;
}
......
}

對於傳入的 SQL 映射聲明,除了解析並加載 SQL 映射聲明中指定的 SQL 映射文件,handleSqlMapNode 還根據傳入的 SQL 方言版本信息,以一定的路徑規則,尋找針對該 SQL 方言的 SQL 映射文件定製版本,將它們一併解析加載。


清單 10. handleSqlMapNode 方法
                
public interface SqlMapStreamMerger {
public void addInput(InputStream input);
public InputStream merge();
......
}

public class SqlMapConfigParserEx extends SqlMapConfigParser {
SqlMapStreamMerger sqlMapStreamMerger = ...;
.......

public void handleSqlMapNode(Node node, String sqlDialect) {
Properties attributes = NodeletUtils.parseAttributes(node);
String resource = attributes.getProperty("resource");
// 讀取 SQL 映射聲明指定的 SQL 映射文件
InputStream is = Resources.getResourceAsStream(resource);
sqlMapStreamMerger.addInput(is);
// 尋找並試圖讀取針對 SQL 方言 sqlDialect 的 SQL 映射文件定製版本
int idx = resource.lastIndexOf('/');
resource = resource.substring(0, idx) + "/" + sqlDialect +
resource.substring(idx);
is = Resources.getResourceAsStream(resource);
if (is != null) {
sqlMapStreamMerger.addInput(is);
}
// 將讀取到的 SQL 映射文件,包括基本的 SQL 映射文件,以及爲特定數據庫的定製版本,
// 合成一個 SQL 映射文件 XML 數據流交給 SqlMapParser 解析處理
Reader reader = new InputStreamReader(sqlMapStreamMerger.merge());
new SqlMapParser(vars).parse(reader);
}

......
}

成員函數 handleSqlMapNode 將找到的 SQL 映射文件,包括 SQL 映射聲明中指定的基本的 SQL 映射文件,以及以一定路徑規則找到的針對特定數據庫的 SQL 映射文件定製版本,通過 SqlMapStreamMerge 對象整合成一個 SQL 映射文件,才遞交給 SqlMapParser 解析處理。SqlMapStreamMerge 確保:

  1. 結 果 SQL 映射文件中不存在重複 ID 的 SQL 映射配置塊(statement、insert、update、delete、sql、resultMap 等)。如果基本的 SQL 映射文件與針對特定數據庫的 SQL 映射文件定製版本之間存在重複 ID 的 SQL 映射配置塊定義,SqlMapStreamMerge 保留後者覆蓋前者;
  2. 結果 SQL 映射文件中的配置塊按引用依賴順序有序排列。即所有的 resultMap 聲明都位於引用它們的 statement 聲明之前,被繼承的 resultMap 聲明都位於繼承的 resultMap 聲明之前等。

先 Merge 再解析,這是必要的,因爲 SqlMapParser 本身並不支持 SQL 映射定義的方法重寫。





回頁首


使用

使用擴展的 SqlMapConfigParser 實現 —— SqlMapConfigParserEx,可以大大簡化應用程序中多數據庫支持問題的解決。

還是之前那個例子。

首先,我們使用 SqlMapConfigParserEx 替換程序中的 SqlMapConfigParser 使用。


清單 11. 在應用代碼中使用擴展的 SqlMapConfigParserEx
                
String cfg = "SQLMAPCONFIG.XML";
Reader r = Resources.getResourceAsReader(cfg);

// old code
// SqlMapClient client = new SqlMapConfigParser().parse(r);

// new code
SqlMapClient client = new SqlMapConfigParserEx().parse(r);
...

現在,要增加對 MySQL 的支持,只需建立一個新的 SQL 映射文件 /mysql/SQLMAP.XML,重寫 SQLMAP.XML 中 GETMOSTPOPULARBLOG 的 SQL 定義。Java 代碼可以繼續保持數據庫平臺透明性,無需作出任何修改。


清單 12. 針對 MySQL 的配置文件 /mysql/SQLMAP.XML
                

<sqlmap namespace="blog">
select * from blogs order by rating desc limit 0, $size$
</sqlmap>

運行 時,SqlMapConfigParserEx 會自動檢測數據庫引擎版本信息,讀取文件 /mysql/SQLMAP.XML,使用其中的(針對 MYSQL 定製的)GETMOSTPOPULARBLOG 定義覆蓋 SQLMAP.XML 中的 DB2 方言版本,從而確保程序行爲的正確性。

我們支持,針對新的數據庫平臺,對 SQL 映射文件中的任意配置進行定製 / 重寫,甚至包括 <parametermap>、<resultmap>、<cachemodel> 等,儘管在實際應用中,這樣的需求並不常見。





回頁首


關於 iBatis 2.1.5

上 述分析只適合 iBatis 2.2.0 之後的版本。在 iBatis 2.1.5 中,addSqlMapNodelets 是 SqlMapConfigParser 的私有成員函數,無法在子類中重寫。附件中,我們給出了針對 iBatis 2.1.5 的 SqlMapConfigParserEx 實現,大致思想類似,這裏就不再詳述。





回頁首


結束語

iBatis 作爲一個 ORM 框架,以其簡單易用,支持更爲靈活的數據庫 / 系統設計,正在得到越來越多的關注。iBatis 應用中,開發者需要自己編寫具體的 SQL 語句,針對特定的數據庫進行 SQL 優化,處理跨數據庫平臺移植問題等。本文,針對 iBatis 應用中的多數據庫支持問題,通過擴展 iBatis 的現有實現,給出了一個較爲簡單高效的解決方法。






回頁首


下載

描述名字大小下載方法
本文代碼下載(for iBatis 2.1.5) code_for_ibatis215.zip 14 KB HTTP
本文代碼下載(for iBatis 2.2.0) code_for_ibatis220.zip 12 KB HTTP
244986.html

禮物 2008-12-08 11:59 發表評論
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章