級別: 中級
張 琦煒 (zhangqiw@cn.ibm.com), 軟件工程師, 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 id="GETMOSTPOPULARBLOG" parameterClass="map" resultMap="blog">
select * from blogs order by rating desc fetch first $size$ rows only
</select>
</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 id="GETMOSTPOPULARBLOG" parameterClass="map" resultMap="blog">
select * from blogs order by rating desc fetch first $size$ rows only
</select>
<select id=" GETMOSTPOPULARBLOG_MYSQL" parameterClass="map" resultMap="blog">
select * from blogs order by rating desc limit 0, $size$
</select>
......
|
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 確保:
- 結
果 SQL 映射文件中不存在重復 ID 的 SQL
映射配置塊(statement、insert、update、delete、sql、resultMap 等)。如果基本的 SQL
映射文件與針對特定數據庫的 SQL 映射文件定制版本之間存在重復 ID 的 SQL 映射配置塊定義,SqlMapStreamMerge
保留后者覆蓋前者;
- 結果 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
<!-- /mysql/SQLMAP.XML -->
<sqlMap namespace="blog">
<select id="GETMOSTPOPULARBLOG" parameterClass="map" resultMap="blog">
select * from blogs order by rating desc limit 0, $size$
</select>
</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 |
|