級(jí)別: 中級(jí)
張 琦煒 (zhangqiw@cn.ibm.com), 軟件工程師, IBM 中國(guó)軟件開發(fā)中心
2007 年 12 月 13 日
iBatis
是一個(gè)開源的對(duì)象關(guān)系映射框架,著重于 POJO 與 SQL 之間的映射關(guān)系。和其它 ORM 框架不同,iBatis 開發(fā)者需要自己編寫和維護(hù)
SQL
語句。為了得到更好的執(zhí)行性能,在實(shí)際開發(fā)中免不了會(huì)使用一些數(shù)據(jù)庫(kù)方言。隨之而來的一個(gè)問題是,如何在增加對(duì)新的數(shù)據(jù)庫(kù)支持的同時(shí)盡可能避免對(duì)已有應(yīng)用
程序代碼的修改?本文提供了一個(gè)簡(jiǎn)單有效的方法,通過擴(kuò)展 iBatis 來透明地支持多數(shù)據(jù)庫(kù)方言。
iBatis 簡(jiǎn)介
iBatis
是一個(gè)開源的對(duì)象關(guān)系映射程序,著重于 POJO 與 SQL 之間的映射關(guān)系。使用時(shí),開發(fā)者提供一個(gè)被稱為 SQL 映射的 XML
文件,定義程序?qū)ο笈c SQL 語句間的映射關(guān)系, iBatis 會(huì)根據(jù) SQL 映射文件的定義,運(yùn)行時(shí)自動(dòng)完成 SQL 調(diào)用參數(shù)的綁定以及
JDBC ResultSet 到 Java POJO 之間的轉(zhuǎn)換。下面是一個(gè)簡(jiǎn)單的例子,相比其它 ORM 工具,iBatis
相對(duì)簡(jiǎn)單,更容易上手。
清單 1. POJO 對(duì)象
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 應(yīng)用中的多數(shù)據(jù)庫(kù)支持
在 iBatis 應(yīng)用中,開發(fā)者仍需自己編寫具體的 SQL 語句,iBatis 只是隱藏和簡(jiǎn)化了 JDBC 的相關(guān)調(diào)用。
實(shí)際開發(fā)中,我們不免需要就 SQL 語句針對(duì)各種特定的數(shù)據(jù)庫(kù)進(jìn)行特殊優(yōu)化,以期獲取更好的執(zhí)行性能,隨之而來的一個(gè)問題是,如何應(yīng)對(duì)新的數(shù)據(jù)庫(kù)平臺(tái)支持的需求。
一般的做法是,修改 SQL 映射文件,提供一些新的針對(duì)新數(shù)據(jù)庫(kù)平臺(tái)的 SQL 語句版本,然后修改程序代碼,添加相應(yīng)調(diào)用。繼續(xù)上面的例子。上面的例子中,對(duì)于 SQL 語句 GETMOSTPOPULARBLOG 的定義,我們使用了 DB2 特有的 SQL 方言“FETCH FIRST n ROWS ONLY ”,對(duì)于這樣的程序,如果希望增加對(duì) 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.搜索程序代碼,在每一個(gè)調(diào)用 iBatis “GETMOSTPOPULARBLOG ”的地方,增加檢測(cè) MYSQL 數(shù)據(jù)庫(kù)引擎的代碼,并添加對(duì)“GETMOSTPOPULARBLOG_MYSQL ”的 iBatis 調(diào)用。
清單 6. 增加檢測(cè)數(shù)據(jù)庫(kù)引擎的代碼
......
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);
}
......
|
每增加一個(gè)新的數(shù)據(jù)庫(kù)支持,增加了一些新
的針對(duì)新數(shù)據(jù)庫(kù)平臺(tái)的 SQL 語句版本,我們就不得不搜索源代碼,找出所有受到影響的 iBatis
調(diào)用,修改并增加針對(duì)新數(shù)據(jù)庫(kù)的特殊調(diào)用。代碼維護(hù)時(shí),每次涉及使用數(shù)據(jù)庫(kù)方言的 SQL
語句,我們也都必須記住小心謹(jǐn)慎地處理所有相關(guān)的數(shù)據(jù)庫(kù)特化調(diào)用。這樣的工作乏味且容易出錯(cuò)。
本文,我們?cè)噲D在分析 iBatis 源碼的基礎(chǔ)上,通過適當(dāng)擴(kuò)展 iBatis,提供一個(gè)高效方便的解決方案。
擴(kuò)展 SqlMapConfigParser
在 iBatis 應(yīng)用中,SqlMapConfigParser 負(fù)責(zé)解析 iBatis 配置文件,加載所有的 SQL 映射文件,生成 SqlMapClient 實(shí)例,這是持久化調(diào)用的入口。
SqlMapConfigParser 的實(shí)現(xiàn)并不復(fù)雜。成員函數(shù) parser
將傳入的配置文件 XML 輸入流交給一個(gè) XML 解析器。XML 解析器解析 XML 輸入,并針對(duì)每一個(gè) XML Fragment
調(diào)用合適的處理器處理。所有的處理器都在 SqlMapConfigParser 類實(shí)例初始化時(shí)預(yù)先被注冊(cè)到 XML 解析器上,其中,對(duì)于
iBatis 配置中的 SQL 映射聲明,只是簡(jiǎn)單地調(diào)用類 SqlMapParser 中的 parser 方法,解析并加載相應(yīng)的 SQL 映射定義文件。
清單 7. SqlMapConfigParser 實(shí)現(xiàn)
public class SqlMapConfigParser {
//XML 解析器
protected final NodeletParser parser = new NodeletParser();
public SqlMapConfigParser() {
......
// 注冊(cè) XML 處理器
addSqlMapNodelets();
...... // more
}
public SqlMapClient parse(Reader reader) {
......
// 調(diào)用 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); // 調(diào)用 SqlMapParser.parser 方法
// 解析并加載 SQL 映射文件
......
}
}
);
}
......
}
|
我們繼承 iBatis 原有的配置文件解析器實(shí)現(xiàn) SqlMapConfigParser,重寫其中對(duì) SQL 映射聲明的處理。
首先,我們重寫 SqlMapConfigParser 的成員函數(shù) addSqlMapNodelets 。對(duì)于從 XML 解析器傳入的 SQL 映射聲明節(jié)點(diǎn),我們并不立即進(jìn)行解析處理,而只是將它們記錄下來。
清單 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
映射聲明被放到最后處理,此時(shí) SqlMapClient
實(shí)例已經(jīng)基本構(gòu)造完畢,至少,我們可以安全地調(diào)用它的相關(guān)方法,打開數(shù)據(jù)庫(kù)連接,查詢數(shù)據(jù)庫(kù)引擎相關(guān)信息。對(duì)于每個(gè) SQL
映射聲明,SqlMapConfigParserEx 調(diào)用其成員函數(shù)方法 handleSqlMapNode 進(jìn)行相應(yīng)的 SQL 映射文件解析和加載處理,數(shù)據(jù)庫(kù)引擎支持的 SQL 方言版本信息作為參數(shù)被一并傳入。
清單 9. 重寫 parse 方法
public interface DialectMapping {
public String getDialect(String productName);
// 返回?cái)?shù)據(jù)庫(kù)平臺(tái)支持的 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;
}
......
}
|
對(duì)于傳入的 SQL 映射聲明,除了解析并加載 SQL 映射聲明中指定的 SQL 映射文件,handleSqlMapNode 還根據(jù)傳入的 SQL 方言版本信息,以一定的路徑規(guī)則,尋找針對(duì)該 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);
// 尋找并試圖讀取針對(duì) 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 映射文件,以及為特定數(shù)據(jù)庫(kù)的定制版本,
// 合成一個(gè) SQL 映射文件 XML 數(shù)據(jù)流交給 SqlMapParser 解析處理
Reader reader = new InputStreamReader(sqlMapStreamMerger.merge());
new SqlMapParser(vars).parse(reader);
}
......
}
|
成員函數(shù) handleSqlMapNode
將找到的 SQL 映射文件,包括 SQL 映射聲明中指定的基本的 SQL 映射文件,以及以一定路徑規(guī)則找到的針對(duì)特定數(shù)據(jù)庫(kù)的 SQL
映射文件定制版本,通過 SqlMapStreamMerge 對(duì)象整合成一個(gè) SQL 映射文件,才遞交給 SqlMapParser
解析處理。SqlMapStreamMerge 確保:
- 結(jié)
果 SQL 映射文件中不存在重復(fù) ID 的 SQL
映射配置塊(statement、insert、update、delete、sql、resultMap 等)。如果基本的 SQL
映射文件與針對(duì)特定數(shù)據(jù)庫(kù)的 SQL 映射文件定制版本之間存在重復(fù) ID 的 SQL 映射配置塊定義,SqlMapStreamMerge
保留后者覆蓋前者;
- 結(jié)果 SQL 映射文件中的配置塊按引用依賴順序有序排列。即所有的 resultMap 聲明都位于引用它們的 statement 聲明之前,被繼承的 resultMap 聲明都位于繼承的 resultMap 聲明之前等。
先 Merge 再解析,這是必要的,因?yàn)?SqlMapParser 本身并不支持 SQL 映射定義的方法重寫。
使用
使用擴(kuò)展的 SqlMapConfigParser 實(shí)現(xiàn) —— SqlMapConfigParserEx,可以大大簡(jiǎn)化應(yīng)用程序中多數(shù)據(jù)庫(kù)支持問題的解決。
還是之前那個(gè)例子。
首先,我們使用 SqlMapConfigParserEx 替換程序中的 SqlMapConfigParser 使用。
清單 11. 在應(yīng)用代碼中使用擴(kuò)展的 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);
...
|
現(xiàn)在,要增加對(duì) MySQL 的支持,只需建立一個(gè)新的 SQL 映射文件 /mysql/SQLMAP.XML,重寫 SQLMAP.XML 中 GETMOSTPOPULARBLOG 的 SQL 定義。Java 代碼可以繼續(xù)保持?jǐn)?shù)據(jù)庫(kù)平臺(tái)透明性,無需作出任何修改。
清單 12. 針對(duì) 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>
|
運(yùn)行
時(shí),SqlMapConfigParserEx 會(huì)自動(dòng)檢測(cè)數(shù)據(jù)庫(kù)引擎版本信息,讀取文件 /mysql/SQLMAP.XML,使用其中的(針對(duì)
MYSQL 定制的)GETMOSTPOPULARBLOG 定義覆蓋 SQLMAP.XML 中的 DB2 方言版本,從而確保程序行為的正確性。
我們支持,針對(duì)新的數(shù)據(jù)庫(kù)平臺(tái),對(duì) SQL 映射文件中的任意配置進(jìn)行定制 / 重寫,甚至包括 <parameterMap>、<resultMap>、<cacheModel> 等,盡管在實(shí)際應(yīng)用中,這樣的需求并不常見。
關(guān)于 iBatis 2.1.5
上
述分析只適合 iBatis 2.2.0 之后的版本。在 iBatis 2.1.5 中,addSqlMapNodelets 是
SqlMapConfigParser 的私有成員函數(shù),無法在子類中重寫。附件中,我們給出了針對(duì) iBatis 2.1.5 的
SqlMapConfigParserEx 實(shí)現(xiàn),大致思想類似,這里就不再詳述。
結(jié)束語
iBatis
作為一個(gè) ORM 框架,以其簡(jiǎn)單易用,支持更為靈活的數(shù)據(jù)庫(kù) / 系統(tǒng)設(shè)計(jì),正在得到越來越多的關(guān)注。iBatis
應(yīng)用中,開發(fā)者需要自己編寫具體的 SQL 語句,針對(duì)特定的數(shù)據(jù)庫(kù)進(jìn)行 SQL 優(yōu)化,處理跨數(shù)據(jù)庫(kù)平臺(tái)移植問題等。本文,針對(duì) iBatis
應(yīng)用中的多數(shù)據(jù)庫(kù)支持問題,通過擴(kuò)展 iBatis 的現(xiàn)有實(shí)現(xiàn),給出了一個(gè)較為簡(jiǎn)單高效的解決方法。
下載
描述 | 名字 | 大小 | 下載方法 |
本文代碼下載(for iBatis 2.1.5) |
code_for_ibatis215.zip |
14 KB |
HTTP |
本文代碼下載(for iBatis 2.2.0) |
code_for_ibatis220.zip |
12 KB |
HTTP |
|