JDO快速入門
JDO是以Sun公司爲首所制定的Java Community Process(Java標準制定組織,JCP)的一部分。JDBC仍然保留使用是因爲它比起JDO允許程序員在數據庫訪問上有更大的控制權。除JDO和 JDBC外的另一個選擇是Enterprise JavaBeans (EJB)。
一、爲什麼要用JDO
Java開發人員已經有好幾種存取數據庫的方法:序列化,JDBC,面向對象映射工具,面向對象數據庫,以及實體EJB。那爲什麼還要介紹其他的存儲架構呢?答案是,上面每一種實現存儲的方案都存在一定的限制。JDO正在嘗試解決這些限制。
序列化:是Java建立的一種傳輸機制,它能夠把對象的信息轉換成一系列的字節碼,這些字節碼可以被傳輸到網絡或者存儲到一個文件中。序列化的使用非常簡單,但他還是有限制的。它必須立即存取對象的特徵,而且它不適合存取大批量的數據。在更改一個對象的屬性時如果有錯誤發生它無法實現“回滾”,因此不適於應用程序對數據完整性的要求,而且不能實現多個線程或程序異步讀寫數據。所有這些不足都使得序列化無法滿足大多數數據存儲要求。
JDBC:許多程序員使用 JDBC API來操作關係數據庫。JDBC克服了許多序列化中存在的缺點:它可以操作大批量的數據,有確保數據一致性的機制,支持信息的併發存取,可以使用已經非常成熟的SQL語言。不幸的是,JDBC使用起來並不像序列化那麼簡單。JDBC使用的關係範例無法用於存儲對象,因此你不得不放棄在代碼中使用面向對象原則存儲數據。
面向對象映射工具:由軟件廠商創建的架構可以爲你實現對象和關係數據庫之間的映射。這種對象-關係映射支持使你專注於對象模型的設計而不必關心面向對象和關係數據庫之間的匹配。不幸的是每一種對象-關係映射產品都有一套他自己廠商實現的標準。你不得不使自己的代碼遷就於某一個單獨廠商的實現。假如這個廠商提高產品價格或者停止對bug更改的支持,使你準備放棄它而用其他的廠商實現架構時,你就必須重寫你的代碼。
面向對象的數據庫:比對象關係數據庫映射更好的選擇使使用一些軟件廠商開發了一種新的把對象存儲到數據庫的方法。這種面向對象的數據庫使用起來常常比對象關係映射軟件簡單。ODMG組織成立的目的之一就是創建一種訪問對象數據庫的標準API。多數廠商都遵崇ODMG組織的要求,因此由於廠商實現不同帶來的麻煩也解決了。但是,一些企業對於從關係數據庫轉向對象數據庫顯得猶豫不決,因爲有大量的數據存儲在傳統的關係數據庫中。雖然一些數據庫分析工具可以用於面向對象數據庫與關係數據庫之間的移植,然而大量的數據存儲使用的仍然是關係數據庫。
實體EJB:Java平臺的企業級應用中引入了實體EJB。實體EJB是一個組件,他描述了數據庫中的持久性數據信息。EJB使用類似於對象-關係映射的辦法,它提供了一個持久性數據的面向對象的表示。不同於對象關係軟件,EJB對於關係數據庫沒有限制;它描述的持久性信息可以來自一個企業信息系統(EIS)或者其他的存儲設備。而且,EJB要求遵循一個嚴格標準,實現它的廠商必須遵循這個標準。不幸的是,EJB標準在面向對象方面稍微有些欠缺,比如一些高級的特性:繼承、多態和複合關係等。另外,EJB的代碼編寫很複雜,而且它是一個重量級組建需要消耗應用服務器很多的資源來運行。但是,EJB中的會話 Bean和消息驅動Bean有很多優勢,所以JDO規範詳細定義了JDO如何與他們進行集成。
JDO:JDO集成了很多上述持久性機制的特性,這使得在JDO中創建一個持久化(persistence)類就像創建一個序列化類一樣簡單。JDO支持批量數據的存儲,數據一致性,併發處理和JDBC的查詢功能。就像對象-關係映射軟件和對象數據庫一樣,它允許使用面向對象的高級特性比如“繼承”。它避免了像EJB中實體Bean一樣必須依賴於來自廠商定義的嚴格規範。同EJB一樣,JDO也不規定任何特定的後端數據庫。
但是,這裏還是要說一下,世界上沒有“萬靈丹”。所以,使用JDO並不是對於每一個應用程序都是有好處的。很多應用程序完全可以使用其他更理想的存儲機制。
二、JDO架構
下面我開始對JDO的架構作一個簡單的介紹。
下圖顯示了JDO架構主要的幾部分:
JDOHelper :javax.jdo.JDOHelper類擁有一些靜態的助手(helper)方法。這個方法可以獲得一個持久對象的生命週期還可以用來創建一個與具體實現廠商無關的PersistenceManagerFactory的實例,這裏使用了工廠(factory)模式。
PersistenceManagerFactory:javax.jdo.PersistenceManagerFactory類可以通過JDOHelper類的助手方法獲得,這是一個標準的工廠類,他可以創建PersistenceManager類。
PersistenceManager:javax.jdo.PersistenceManager接口是應用程序經常要使用的一個主要的JDO接口。每一個PersistenceManager負責控制一組持久化對象而且他還可以創建新的持久化對象或刪除現有的持久化對象。Transaction和 PersistenceManager之間存在這一對一的關係,同時PersistenceManager又是Extent和Query的工廠類,也就是說這兩個對象可以通過PersistenceManager創建。
PersistenceCapable:用戶定義的持久化類都必須擴展實現PersistenceCapable接口。大多數JDO實現的供應商都提供一種“增強器”(enhancer)的功能,它可以向你要實現的持久化類中增加PersistenceCapable接口的實現。也就是說,其實你根本不會自己去實現這個接口。
Transaction:每一個PersistemceManager和javax.jdo.Transaction都是一一對應的。Transactions用來處理事務,它使得持久化數據可以成批的一次性添加到數據表中,如果出現異常就將數據回滾。
Extent:java.jdo.Extent是映射數據庫中具體表的類的一個邏輯視圖。Extent可以擁有自己的子類,它通過PersistenceManager獲得。
Query:java.jdo.Query接口用具體的廠商JDO來實現,它負責處理JDO查詢語言(JDOQL),這些JDOQL最終被解釋爲實際的數據庫SQL語言。同樣這個接口也是通過PersistenceManager獲得的。
下面的例子顯示的JDO接口如何操作並執行一個查詢並更新持久化對象。
例子:JDO接口的交互
//通過助手類獲得PersistenceManagerFactory
PersistenceManagerFactory factory=
JDOHelper.getPersistenceManagerFactory(System.getProperties());
//通過PersistenceManagerFactory獲得PersistenceManager對象
PersistenceManager pm=factory.getPersistenceManager();
//創建並開始一個事務
Transaction tx=pm.currentTransaction();
tx.begin();
//查詢employee表中每週工作時間大於40小時的研究人員
Extent ex=pm.getExtent(Employee.class,false);
//獲得一個Query
Query query=pm.newQuery();
//設置這個query作用的範圍,即查詢的是那個表或記錄集
query.setCandidates(ex);
query.setFilter(division.name == /Research/ + && avgHours > 40);
Collection result=(Collection)query.execute();
Employee emp;
for(Iterator itr=result.iterator();itr.hasNext();){
emp=(Employee)itr.next();
emp.setSalary(emp.getSalary()*2);
}
//提交記錄釋放資源
tx.commit();
pm.close();
factory.close();
上面的代碼片斷包括了JDO幾個主要的接口,在此你可以對JDO各個接口的使用方法有一個粗略的印象,以後實際的應用中JDO接口也都是這樣使用的。
三、JDO的異常
JDO不會拋出通常的運行時異常,比如NullPointerExceptions、 IllegalArgumentException等它只拋出JDOException異常。JDOExcetion的結構如下圖所示,這是一個繼承的層次結構,從他們的字面含義就可以看出它們的用途,在這裏就不詳細說了,要想了解JDO異常的層次結構可以參考它們的JavaDoc。
四、使用JDO的好處
·簡便性(Portability):使用JDO API編寫的程序可以在不同開發商的多種可用的實現上運行,不用修改一行代碼,甚至不用重新編譯。
·透明地訪問數據庫(Transparent database access):應用程序開發者編寫代碼透明地訪問底層數據存儲,而不需要使用任何數據庫特定代碼。
·易用性(Ease of use):JDO API允許開發者只需要關注他們自己範圍內的數據模型(Domain Object Model,DOM),而持久化的細節就留給JDO實現。
·高性能(High Performance):Java應用程序開發者不需要擔心數據訪問的性能優化,因爲這個任務已經委派給了JDO實現,它通過改善數據訪問的模式以獲得最佳性能。
·和EJB集成(Integration with EJB):應用程序可以利用EJB的特徵,例如遠程信息處理、自動分佈式事務協調和貫穿整個企業級應用使用同樣的DOMs實現安全性。
五、使用JDO,vs. EJB和JDBC
JDO並不意味着要取代JDBC。它們是兩種以各自獨一無二的能力互相補充的技術,具有不同技術背景和開發目的開發者可以使用二者中的一個。例如。JDBC通過直接的數據庫訪問控制和緩存管理,提供給開發者更大的彈性。JDBC是一種在工業界被廣泛認可的成熟技術。另一方面,JDO,通過隱藏SQL提供給開發者更大的簡便性。它將Java平臺開發者從必須熟悉或學習SQL中解脫出來,而將精力集中在 DOM上,同時JDO管理在持久存儲中對象存儲的字段到字段的細節。
JDO被設計成EJB的補充。CMP爲容器提供簡便的持久化,而JDO可以以兩種方式集成到EJB 中:
(1)通過會話Bean,它含有JDO Persistence-capable類(會話Bean的持久化助手類)用來實現依賴對象;
(2)通過實體Bean,它含有被用作BMP和CMP代理的 JDO Persistence-capable類。
你可以學習更多關於JDO和JDBC之間的關係,還有EJB2.0 CMP和JDO之間的關係。
六、POJO之路
JDO和 EJB之間在持久化模型上顯著的差別曾經在開發者中間引起了混亂。作爲迴應,Sun微系統正領導一個社區項目爲Java技術社區創建POJO持久化模型。這個項目在JSR-220的贊助下執行,由Linda DeMichiel領導。JDO2.0(JSR-243)的專家組成員被邀請加入到EJB3.0(JSR-220)專家組中。
創建POJO持久化模型的目的是爲所有使用Java SE和Java EE平臺的Java應用程序開發者提供一個對象—關係(object-relational)映射工具。值得注意的是Oracle正以co- specification lead的身份加入到Sun EJB3.0。EJB3.0的公衆評論草案已經可以得到。
JSR-243(JDO2.0)遵循了那些來自於JSRs220和243規範的領導寫給Java技術社區的信件所描述的輪廓。
JDO2.0並不打算作爲EJB3.0持久化特定API的集中,而是作爲JDO1.0.2的發展。但是JDO的POJO持久化模型和EJB3.0之間的類似處,使得JDO的客戶當使用JDO2.0滿足立即的需求時,可以很容易的接受EJB3.0持久化模型。另外,JSR-243打算將JDOQL用作一種關於EJB3.0持久化數據的可選查詢語言。這種語言了已經被更新從而可以更好地對EJB3.0使用。
要了解更多關於持久化模型的知識,請查看EJB/JDO持久化FAQ。
七、JDO Class類型
在JDO中一共有三種類型的類:
·Persistence-capable:這種類型代表那些實例可以被持久化到一個數據存儲中的類。請注意,這些類在JDO環境中被使用之前,需要通過JDO元數據規範進行加強。
·Persistence-aware:這些類操縱persistence-capable類。JDOHelper類包含了一些方法,它們允許詢問一個persistence-capable類的實例的持久化狀態。請注意,這些類使用最小化的JDO元數據加強。
·Normal:這些不可被持久化,並且對持久化一無所知。另外它們不需要JDO元數據。
八、JDO實例的生命週期
JDO管理一個對象從創建到刪除的生命週期。在它的生命週期,JDO實例不斷地轉換它的狀態,直到最後被Java虛擬機(JVM)作爲垃圾回收。狀態的轉換使用PersistenceManager類的方法完成,包括 TransactionManager——例如makePersistent()、makeTransient()、deletePersistent ()——和提交或者回滾更改。
表1顯示JDO規範定義的10種狀態。前面的七種是必須的,後面的三種是可選的。如果一個實現不支持某些操作,那麼就不會獲得三種可選的狀態。
表1 JDO生命週期
狀態 | 描述 |
Transient | 任何使用開發者定義的構造函數創建的對象,都不包括持久化環境。一個瞬時實例沒有JDO身份。 |
Persistent-new | 被應用程序組件請求的任何對象都變爲持久的,通過使用PersistenceManager類的makePersistent()。這樣的一個對象將會擁有一個分配的JDO身份。 |
Persistent-dirty | 在當前事務中被改變的持久對象。 |
Hollow | 代表在數據存儲中特定數據的持久對象,但是在它的實例中沒有包含值。 |
Persistent-clean | 代表在數據存儲中的特定事務數據的持久對象,並且它們的數據在當前事務處理中還沒有被改變。 |
Persistent-deleted | 代表在數據存儲中的特定數據的持久對象,並且在當前事務處理中已經被刪除。 |
Persistent-new-deleted | 在同一個事務處理中最近被持久化和刪除的持久對象。 |
Persistent-nontransactional | 代表數據存儲中的數據的持久對象,當前它們的值已經被裝載,但是還沒有事務處理一致。 |
Transient-client | 代表一個瞬時事務處理實例的持久對象,它們的數據在當前事務中還沒有被改變。 |
Transient-dirty | 代表一個瞬時事務處理實例的持久對象,它們的數據在當前事務中已經被改變。 |
下圖顯示了JDO實例各狀態之間的轉換。
本文稍後的代碼片斷,將示範如何執行我們剛剛討論的操作。
九、JDO參考實現
JDO參考實現,來自於Sun微系統,已經可用,一同發行的還有一種被稱爲fstore的基於文件的存儲機制。Sun已經把JDO捐獻給開源社區。JDO1.0和JDO2.0將會作爲Apache JDO項目的一部分進行開發。但是由於時間的限制,JDO2.0的參考實現並不是作爲Apache項目建立的,而是作爲一個JPOX 發行。一些商業實現也是可用的。
十、JDO編程模型
JDO定義了兩種類型的接口:JDO API(在javax.jdo包中)和JDO服務提供者接口(SPI)(在javax.jdo.spi包中)。JDO API面向應用程序開發者,而JDO SPI面向容器提供者,和JDO賣主。
一個應用程序包含兩個主要的接口:
·PersistenceManagerFactory代表了應用程序開發者用來獲得 PersistenceManager實例的訪問點。這個接口的實例可以被配置和序列化以備後來使用。然而,需要注意的是,一旦第一個 PersistenceManager實例從PersistenceManagerFactory中被獲得,這個工廠就不再是可配置。你可以使用下面的代碼來獲得PersistenceManagerFactory。
// 爲JDO實現和數據存儲設置一些屬性
Properties props = new Properties();
props.put(...);
// 得到一個PersistenceManagerFactory
PersistenceManagerFactory pmf = JDOHelper.getPersistenceManagerFactory (props);
·PersistenceManager是JDO-aware應用部分的主要接口。它提供了方法來持久化一個對象,也可以重新得到持久對象和將它們從持久存儲中移除。可以使用下面的方法獲得PersistenceManager。
PersistenceManager pm = pmf.getPersistenceManager ();
一旦獲得了PersistenceManager對象後,應用程序就可以一些任務,例如:持久化一個對象、從持久數據中獲得一個對象、從持久數據中刪除一個對象、更新一個對象等等。
接下來的代碼片斷示範瞭如何持久化一個對象,它更新一個對象的狀態從Transient到Hollow。
Employee emp = new Employee("Sarah Jones", 23, 37000.00);
Transaction tx;
try {
tx = pm.currentTransaction();
tx.begin();
pm.makePersistent(emp);
tx.commit();
} catch (Exception e) {
if(tx.isActive()) {
tx.rollback();
}
}
從持久數據中獲得一個對象同樣簡單,你可以使用Extent(一個信息的持有者)或者Query(提供了更精確的過濾)。下面是一個使用Extent的例子:
try {
tx = pm.currentTransaction();
tx.begin();
Extend ex = pm.getExtent(Employee.class, true);
Iterator i = ex.iterator();
while(i.hasNext()) {
Employee obj = (Employee) i.next();
}
tx.commit();
} catch (Exception e) {
if(tx.isActive()) {
tx.rollback();
}
}
最後,從持久數據中刪除一個對象也可以簡單完成,首先獲得一個從持久數據中獲得一個對象,然後調用deletePersistent(obj)方法。
十一、查詢對象
JDO規範要求開發商必須提供使用JDOQL的查詢能力,JDOQL是一種面向圍繞被持久化對象的查詢語言。PersistenceManager類定義了構造Query實現類的實例的方法。一個查詢過濾器可以被指定爲一個布爾表達式,就像SQL的布爾操作符。
生命週期開發:在你的應用程序中使用JDO
可以通過以下六個步驟建立一個JDO應用:
1. 設計你的範圍內的將會正常使用的類。對一個要求持久化的類的唯一要求就是它要有一個默認構造函數,訪問權限可能是private。
2. 使用元數據定義持久化定義:在這個步驟中,你編寫元數據,指定那些類和字段應該被持久化等等。這個文件可以包含對於一個類或一個或者多個包含持久類的包的持久化信息。一個類的元數據文件的名稱是這個類的名字加上“.jdo”後綴,注意,這個文件必須放在和.class文件相同的目錄中。對於整個包的元數據文件的必須包含在一個稱作package.jdo的文件中。元數據文件可以使用XDoclet或手動開發。下面是一個簡單的對於兩個類的元數據文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE jdo SYSTEM "jdo.dtd">
<jdo>
<package name="com.xyz.hr">
<class name="Employee" identity-type="application" objectidclass="EmployeeKey">
<field name="name" primary-key="true">
<extension vendor-name="sunw" key="index" value="btree"/>
</field>
<field name="salary" default-fetch-group="true"/>
<field name="dept">
<extension vendor-name="sunw" key="inverse" value="emps"/>
</field>
<field name="boss"/>
</class>
<class name="Department" identity-type="application" objectidclass="DepartmentKey">
<field name="name" primary-key="true"/>
<field name="emps">
<collection element-type="Employee">
<extension vendor-name="sunw" key="element-inverse" value="dept"/>
</collection>
</field>
</class>
</package>
</jdo>
3. 編譯這些類,並且使用JDO加強器來加強它們。任何persistence-capable類的實例在被JDO持久化引擎管理之前必須被加強。JDO字節碼加強器通過對類定義特定的改變來裝換這個類,使得任何持久實例可以和數據存儲中的數據描述保持同步。和參考實現一起發行的JDO加強器,能夠從Sun微系統得到,可以使用如下的方式運行:
prompt> java -classpath
%JDO-HOME%/lib/jdo.jar;%JDO-HOME%/lib/jdori.jar;
%JDO-HOME%/jdori-enhancer.jar com.sun.jdori.enhancer.Main -d
/enhanced -s . -f path/tp/package.jdo path/to/theclasses.class
注意:對JDO加強器最重要的參數是一個.jdo文件的名字和.class文件的名字。另外,
·-d選項指定輸出文件的目標文件夾;
·-s選項指定jdo和class文件的源文件夾;
·-f選項強制重寫輸出文件。
如果忽略這個步驟,那麼當你運行應用程序和持久化一個對象時將會拋出ClassNotPersistenceCapableException異常。
4. 爲被持久化的類建立數據庫表。如果你已經有了一個數據庫方案,那麼這一步是可選的。基本上,你必須建立表、索引和在JDO元數據文件中爲類定義的外鍵。有些JDO實現包含一個方案工具,可以根據JDO元數據文件產生所有的這些東西。
5. 編寫代碼來持久化你的對象。在這個步驟中,你要指定那些類在什麼時間被實際持久化。正如前面提到的,最初的步驟是獲得一個PersistenceManager的使用權。
6. 運行你的應用程序。使用java命令,並且包含必要的.jar文件在你的classpath中。
十二、JDO對開發的幫助有哪些 - 實例解析
1.權責劃分:業務開發組和數據庫管理組
對一個項目來講,開發團隊在邏輯上劃分爲兩塊:業務開發組和數據庫管理組。兩者各有特點,各有責任,但相互之間界限很清晰,不會有什麼糾纏。下面用表格說明一下二者的區別:
人員構成
業務開發組系統分析員、程序員。
數據庫管理組DBA、運行維護人員。一般一到兩個人就可以,並可橫跨多個項目
工作內容
業務開發組設計數據類圖,設計業務邏輯接口並用代碼實現(一般在類似sessionbean的類中)
數據庫管理組通過數據類圖映射數據表,一般只需在JDO自動生成的表結構上作少許調整
所需知識和工具
業務開發組UML、Java、JSP、Ant。工具可爲任何IDE。
數據庫管理組JDO原理、UML中的類圖、連接池配置。工具包括PowerDesigner、數據庫訪問工具、具體使用的JDO產品的一些細節
相互的責任範圍
業務開發組向數據庫管理組提交數據部分的UML實體類圖,及某些細節(如某屬性是否很長的字符串)。在對方配置好PersistenceManagerFactory連接池後在代碼中調用。
數據庫管理組根據UML類圖及業務開發組對某些細節的建議建立相應的數據庫(基本上自動完成),在服務器上配置相應的JDOPersistenceManagerFactory(一個J2EEConnector,就象配置數據庫連接池一樣)
工作量
1).業務開發組與業務相關,因爲主要代碼量在業務邏輯上。
2).數據庫管理組一般情況下不大,但在從舊數據庫中導入數據時可能需要一些功夫,不過是一次性的工作。
3).涉及數據結構變動的功能變更時所需工作。
4).業務開發組一方面調整UML實體類圖,提交給數據庫開發組;另一方面根據新功能需求改寫業務邏輯代碼。
5).數據庫管理組根據新的UML實體類圖調整數據庫結構(有的JDO產品可自動完成)。服務器配置不變。
6).由於面向數據庫管理組的工作內容比較簡單,只是量的問題,下面的介紹就儘量不涉及數據庫管理組的工作,而只面向業務開發組。
2.UML實體類圖
UML實體類圖是項目中涉及到數據的部分,這些數據不會隨着程序中止而丟失,稱作可持續的(Persistent),所有數據庫中的數據都是可持續的。
而我們在設計的時候,最開始應該分析出系統有哪些實體類(即可持續的數據類),從而畫出實體類圖。在這個最初級的類圖上面,可以不包含任何屬性,但必須包含實體類之間的關係,這樣才能一眼看出系統的大概輪廓。
下面就是一個簡單的示範實體類圖,是一個論壇中的主要實體的關係圖(原圖沒有有,用孫賓同學的作品抵擋一下。後來發現,這個文章本來就是孫賓同學的作品!)。
簡單地說,項目可以說是一些具有相互關係的實體類加上處理業務邏輯的控制類,以及輸入/輸出數據的邊界類組成,另外可能附加一些接口或特殊服務,如短信/郵件發送或面向第三方的數據訪問接口等等。
有了上面這個圖,DBA就比較清楚數據庫中會有什麼樣的數據表,表之間如何關聯了。但數據庫中的表與實體類並不是一一對應的。比如對實體類圖中的某個多多對應關係,數據庫中必須有一個額外的表來對應,有些實體的某部分屬性可能會放在另一個額外表中以加強性能。
下一步就是在這個圖的基礎上爲實體類添加屬性,然後給每個屬性加上訪問器(accessors,即getXXX()/isXXX()和setXXX() 等),以及一些必須的方法(比如getAge(),通過當前日期和生日得出年齡)。這樣,才成爲一個完整的實體類圖。下圖就是一個增添了普通屬性的實體類圖(真的找不到圖了-_-)。
接下來,加入對普通屬性的訪問器方法,可能再給加一個Member.getAge()方法,這個實體類圖就算是完成了。這些過程都比較簡單,並且有很多工具可以自動完成,這裏不再多說。
有一點要着重說明的是,對實體類,只要給出這個圖,然後用工具生成對應的Java類代碼,這些類的代碼就算是完成了,以後不用再在其中寫代碼了。
3 透明的存儲
對開發人員來說,主要工作集中在業務邏輯的實現上,這就需要寫一些控制類,來實現這些邏輯。這些控制類一般可以 XxxSession的方式來命名,表示面向某一類使用者的控制類,比如MemberSession,完成會員登錄後的一些功能; AdminSession用於完成管理員登錄後的一些功能。在這些控制類中的一個方法中,只需要通過JDO規範的接口類(javax.jdo.*)來獲取對前面的實體的訪問,從而完成業務功能。一個典型的方法如下:
MemberSession的發表主題貼的方法:
public Topic postTopic(String title,String content, String forumId) { //業務邏輯過程開始 javax.jdo.PersistenceManager pm = getPersistenceManagerFactory().getPersistenceManager(); pm.currentTransaction().begin(); //先生成一個主題,設置基本屬性 Topic topic = new Topic(); topic.setTitle(title); topic.setContent(content); topic.setPostTime(new Date()); //獲取相關的論壇和當前登錄的會員 //下面用到的this.logonMemberId是本MemberSession對象生成時必須提供的會員標識。 //本MemberSession對象一般是在登錄的時候生成的。 Forum forum = (Forum)pm.getObjectById(pm.newObjectIdInstance(Forum.class,forumId)); Member author = (Member)pm.getObjectById(pm.newObjectIdInstance(Member.class, this.logonMemberId)); //設置該主題的論壇和作者 topic.setForum(forum); topic.setAuthor(author); //標記爲需要存儲 pm.makePersistent(topic); //順便更改論壇和作者的一些相關屬性 forum.setTopicCount(forum.getTopicCount()+1); author.setPostCount(author.getPostCount()+1); //業務邏輯過程完成 pm.currentTransaction().commit(); pm.close(); }
這樣,這個方法就算寫完了。我們可以看到,只要將與實體類相關的代碼放在pm.currentTransaction()的開始和提交之間就可以了。
唯 一中間需要與JDO打交道的就是對新生成的對象(topic)需要調用一下pm.makePersistent(),但實際上在很多情況下,只要從pm中 取出的對象指向這個對象(比如:author.getPostTopics().add(topic)),就根本不需要這條語句(當然寫上也沒錯),因爲 pm會根據可達性(Reachability)的原則將當前已經在數據庫中的對象能直接或間接指到的新生成的那些對象都存儲起來。
以上的代碼說明了我們不必對每個發生變化的對象調用更新函數,因爲JDO的pm會自動跟蹤這些變化,並將確實發生改變的對象同步到數據庫。這就是“透明的存儲”。
4 靈活的查詢:JDOQL vs SQL
JDOQL是JDO中使用的查詢語言,是對象式的查詢語言,很象OQL,也很象EJBQL,但沒有EJBQL那種只能靜態存在的缺點。
對象式查詢語言的優點有很多文章都有介紹,這裏不再說明。只說明一點:JDOQL完全基於UML實體類圖,不必理會具體數據庫中的任何內容。
下面舉一些例子,說明這種靈活性。
4.1 例:查找某作者發表過貼子的所有論壇
我們給出的參數只有作者的姓名,希望得到的是所有的他發表過主題或回覆過主題的論壇。我們需要這樣的JDOQL條件:首先查詢的目標是Forum類,然後是JDOQL的過濾串
this == _topic.forum && (_topic.author.name == “<作者姓名>” || _topic.contains(_reply) && _reply.author.name == “<作者姓名>”)
然後,聲明用到的變量:Topic _topic; Reply _reply;
再執行SQL即可。一般的JDO產品會將這個查詢儘可能優化地翻譯爲:
select a.<可預定義的最常用字段組> from FORUM a, TOPIC b, REPLY c, MEMBER d
where a.FORUM_ID = b. FORUM_ID and (b.MEMBER_ID = d. MEMBER_ID and d.NAME=’<作者姓名>’ or b.TOPIC_ID = c. TOPIC_ID and c.MEMBER_ID = d.MEMBER_ID and d.NAME = ‘<作者姓名>’)
從上面,我們可以看到,JDOQL無論在可讀性還是可維護性上都遠遠好於SQL。我們還可以將作者姓名作爲一個綁定參數,這樣會更簡單。
如果直接操作SQL的話會變得很麻煩,一方面要注意實體類中的屬性名,一方面又要注意在數據庫中的對應字段,因爲多數情況下,兩者的拼寫由於各種因素(如數據庫關鍵字衝突等)會是不一樣的。
從這個例子擴展開去,我們可以進一步:
4.2 例:查找某作者發表過貼子的所有論壇中,總貼數大於100並且被作者收入自己的收藏夾的那些論壇
很簡單,將過濾串這樣寫:
this == _topic.forum && (_topic.author == _author || _topic.contains(_reply) && _reply.author == _author) && _author.name == ‘<作者姓名>’ && postCount > 100 && _author.favoriteForums.contains(this)
這一次多了一個用到的變量:Member _author。其底層的SQL大家可以自己去模擬。
5 長字符串
我們經常會遇到用戶輸入的某個信息文字串超出了規定的數據字段的大小,導致很麻煩的處理,尤其是一些沒有必要限制長度的字符串,比如一篇主題文章的內容,有可能幾萬字,這迫使我們將其分作很多子記錄,每條子記錄中放一部分。所有這些,都使我們的代碼量加大,維護量加大。
現在有了JDO,我們的代碼就簡單多了,我們可能儘量利用JDO提供的透明存儲功能,通過一些簡單的工具類實現:原理是將其分割爲字符串子串。
package jdo_util;
import java.util.*;
public class StringHelper {
public static List setLongString(String value) {
if(value == null) return null;
int len = value.length();
int count = (len+partSize-1)/partSize;
List list = new ArrayList(count);
for(int i = 0; i < count; i++) {
int from = i*partSize;
list.add(value.substring(from,Math.min(from+partSize,len)));
}
return list;
}
public static String getLongString(List list) {
if(list == null) return null;
StringBuffer sb = new StringBuffer();
for(Iterator itr = list.iterator(); itr.hasNext(); ) sb.append(itr.next());
s = sb.toString();
return s;
}
private static int partSize = 127; //字符串片斷的大小。針對不同的數據庫可以不同,如Oracle用2000
}
有了這個類以後,我們只需要將Topic.content的類型換成List,而其訪問器的接口不變,仍是String,只是內容變一下:(並在JDO描述符中指明該List的元素類型是String)
public class Topic {
…
List content; //原先是String類型
…
public String getContent() {
return StringHelper.getLongString(content);
}
public void setContent(String value) {
content = StringHelper.setLongString(value);
}
}
這樣,就解決了長字符串的問題,而其它相關的代碼完全不需要改,這就支持了無限長的主題內容。
最後,唯一的缺陷是對內容進行關鍵字查詢的時候需要將
content.startsWith(‘%<關鍵字>’)
變爲
content.contains(s) && s.startsWith(‘%<關鍵字>’)
並且,可能查詢結果不太準(比如正好跨越兩個子串部分)。慶幸的是,一般這種對很長的字符串字段的查詢需求不是太多。
需要說明的是,採用傳統的SQL同樣也會需要對拆分的字符串進行額外的查詢,並具有同樣的缺點。
另外,這個功能需要JDO產品支持規範中的一個可選選項:javax.jdo.option.List,主要的幾個JDO產品都支持。比如KodoJDO和JDOGenie。
6 資源回收:pm.close()
我們採用傳統SQL寫代碼時,最危險的就是資源釋放問題,這在基於WEB的應用中尤其重要。因爲與JDBC相關的資源不是在Java虛擬機中分配的,而是在系統底層分配的,Java的垃圾回收機制鞭長莫及,導致系統內存慢慢耗光而死機。
在JDBC中需要主動釋放的資源有:Connection、Statement、PreparedStatement、ResultSet,在每個對這些類型的變量賦值的時候,都必須將先前的資源釋放掉。無疑是一件繁瑣而又容易被忽略的事情。
在JDO 中,事情變得簡單多了,所有的資源在pm.close()的時候會自動釋放(除非JDO產品增加了一些對PreparedStatement和 ResultSet的Cache),這是JDO規範的要求。因此,只要我們記住在對實體類處理完畢時調用pm.close()就行了。比如下面的代碼:
PersistenceManager pm = null
try {
pm = getPersistenceManagerFactory().getPersistenceManager();
//做一些數據類的處理工作
} finally{
pm.close();
}
有些人可能就是不喜歡調用它,覺得煩,因爲每次要用時都要打開一個PM,而用完時都要關閉,如果JDO產品沒有PM連接池的話,性能可能受到影響。這樣,我們可以利用下面的繼承java.lang.ThreadLocal的工具類完成這一點:
public class PersistenceManagerRetriever extends ThreadLocal { /** * 根據配置信息初始化一個PersistenceManager獲取器 * @param p */ public PersistenceManagerRetriever(java.util.Properties p) { pmf = JDOHelper.getPersistenceManagerFactory(p); } /** * 獲取相關的PersistenceManagerFactory * @return 一個PersistenceManagerFactory對象 */ public PersistenceManagerFactory pmf() { return pmf; } /** * 獲取一個與當前線程相關的PersistenceManager * @return 一個PersistenceManager對象 */ public PersistenceManager pm() { return (PersistenceManager)get(); } /** * 釋放所有與本線程相關的JDO資源 */ public void cleanup() { PersistenceManager pm = pm(); if(pm == null) return; try { if(!pm.isClosed()) { Transaction ts = pm.currentTransaction(); if(ts.isActive()) { log.warn("發現一個未完成的Transaction ["+pmf.getConnectionURL()+"]!"+ts); ts.rollback(); } pm.close(); } } catch(Exception ex) { log.error("釋放JDO資源時出錯:"+ex,ex); } finally { set(null); } } public Object get() { PersistenceManager pm = (PersistenceManager)super.get(); if(pm == null || pm.isClosed()) { pm = pmf.getPersistenceManager(); set(pm); if(log.isDebugEnabled()) log.debug("retrieved new PM: "+pm); } return pm; } public static final Logger log = Logger.getLogger(PersistenceManagerRetriever.class); private PersistenceManagerFactory pmf; }
這樣,只要在一個線程中(比如一次頁面請求),在所有的需要PM的地方,都只需直接調用
persistenceManagerRetriever.pm();
即可,並且,只在最後用完後才調用一次persistenceManagerRetriever.cleanup()以關閉它。
這個persistenceManagerRetriever可以在某個系統類的初始化代碼中加入:
PersistenceManagerRetriever persistenceManagerRetriever = new PersistenceManagerRetriever(properties);
而關閉當前線程相關的PM的語句(persistenceManagerRetriever.cleanup())可以配置一個JspFilter來完成它,比如:
public static class JspFilter implements javax.servlet.Filter { public void doFilter( javax.servlet.ServletRequest request, javax.servlet.ServletResponse response, javax.servlet.FilterChain chain) throws javax.servlet.ServletException,java.io.IOException { try { chain.doFilter(request,response); } finally { if(pmRetriever != null) pmRetriever.cleanup(); } } public void init(javax.servlet.FilterConfig filterConfig) throws javax.servlet.ServletException {} public javax.servlet.FilterConfig getFilterConfig() { return null; } public void setFilterConfig(javax.servlet.FilterConfig fc) {} public void destroy() {} }
然後我們將其配置在WebApp的描述符中:
<filter> <filter-name>jdo_JspFilter</filter-name> <filter-class>…xxx.jdo_util.JspFilter</filter-class> </filter> <filter-mapping> <filter-name>jdo_JspFilter</filter-name> <url-pattern>*.jsp</url-pattern> </filter-mapping>
這樣,我們在JSP中的代碼更簡單:
… persistenceManagerRetriever.pm().currentTransaction().begin();
//調用一些處理業務邏輯的XxxSession.someMethodThatUsesPM()方法,這些方法中直接用persistenceManagerRetriever.pm()來取得PM。
persistenceManagerRetriever.pm().currentTransaction().commit();
不用處理異常,JspFilter自會處理。
7 ID與對象模型
對 象標識字段,實際上只是一個數據庫範疇的字段,在對象模型中實際上是不需要這些屬性的。也就是說,在Java應用中,一個對象的標識就是在內存中的地址, 不併是這個對象本身的屬性,因爲根據這個內存地址就可以唯一地確定這個對象。比如一個編輯矢量地圖的Java程序,從文件中讀入各個地圖元素(對象)後, 這些對象就有了一個唯一的內存地址,所以不需要給每個對象加一個類似“ID”之類的屬性並寫入文件。
JDO也採用了這樣的概念,ID獨立於對象之外,並不屬於對象的一部分。前面的論壇實體類圖中我們可以看到,每個類中都沒有類似“id”之類的屬性。那麼,JDO怎樣控制與數據庫中的主鍵的對應呢?這就是兩個常用的工具類方法:
Object PersistenceManager.getObjectId(Object obj)
Object PersistenceManager.getObjectById(Object obj, boolean validate)
這樣,可以隨時獲得某個實體對象的ID,也可以在任何時候通過一個ID找出該對象。第一個方法還可以用javax.jdo.JDOHelper.getObjectId()代替。
JDO 規範建議的模式中,這些ID都是由JDO產品自動生成的,項目應用中只在需要傳遞對象的引用的時候才使用,比如在兩個頁面間傳送。並且,這些ID類都是可 以與String互轉的,這就方便了JSP間的傳遞。這種由JDO產品來控制的ID叫做datastore identity,在數據表中的字段名一般是“JDO_ID”。
如果實在是想自己控制對象在數據庫中的ID,JDO也提供用戶自定義的ID,這 時,該ID作爲對象的一個屬性存在,可以是任何類型,int, Date, String, 或其它自定義的複合類型(如兩個屬性合起來作ID)。這種類型的ID叫做application identity。
就個人而言,我建議在新的項目中採用datastore identity,這樣可省下很多時間。而在實體類中,也可以寫一些替代的方法來保持與application identity保持兼容,如:
public class SomePersistentClass { … public String getId() { return JDOHelper.getObjectById(this).toString(); } public static SomePersistentClass getById(String id) { PersistenceManager pm = persistenceManagerRetriever.pm(); return pm.getObjectById(pm.newObjectIdInstance(SomePersistentClass.class, id)); } }
這種方式對兩種類型的ID都有效。注意,這個類本身有這兩個方法,但並沒有一個ID屬性。
8 緩衝與Optimistic Transaction
緩衝是JDO中的一個亮點。雖然JDO規範並沒有嚴格要求一個JDO產品必須實現什麼樣的緩衝,但幾乎每一個JDO產品,尤其是商業化產品,都有比較完善的緩衝體系,這個體系是不同的JDO產品相互競爭的重點之一。
主要的JDO產品包含下列緩衝:
1. PM連接池。對PersistenceManager進行緩衝,類似JDBC連接池,在調用pm.close()的時候並不關閉它,而是等待下一次調用或超時。
2. PreparedStatement緩衝。如果JDO底層發現一個JDOQL語句與前面用過的某句相同,則不會重新分析並生成一個新的 PreparedStatement,而是採用緩衝池中的已有的語句。對PreparedStatement的緩衝也是JDBC3.0規範中的一項功能。 而JDO底層發現如果配置的是符合JDBC3.0規範的驅動時,會採用驅動的緩衝,否則採用自己的緩衝。
3. ResultSet緩衝。這種緩衝的實現的JDO產品不多,目前好象只有KodoJDO 2.5.0 beta實現了。其機制是如果第二次請求執行同樣JDOQL語句、同樣參數的查詢時,JDO底層從上一次執行結果中取出該集合,直接返回,大大增強性能。 不過比較耗資源,因爲是採用JDBC2.0中的ScrollableResultSet實現。
一般我們在對數據庫進行更新操作時,都會對 數據庫進行鎖定操作,設定不同的隔離級別,可以完成不同程度的鎖定,比如鎖記錄、鎖字段、鎖表、鎖庫等等。而JDO中可以在具體JDO產品的廠商擴展 (Vendor Extension)標記中設定。另外,JDO規範還提供了一種對數據庫完全沒有鎖定的方式: javax.jdo.option.OptimisticTransaction,它是一項可選選項,也就是說,並不強制JDO廠商實現它,不過主要的幾 個廠商的JDO產品都實現了這個功能。
OptimisticTransaction的機制原理是:在每個對象的數據庫記錄中增加一個交易控制字 段,然後所有的對象更改在Java虛擬機的內存中完成,當提交的時候,會檢查每個被改過的對象的在從數據庫中取出後是否被其它外部程序改過,這就是通過這 個控制字段完成的。一般這個字段的實現方式有以下幾種:
1. 存放最近一次更改的時間,字段名多取作“JDO_LAST_UPDATE_TIME”
2. 存放歷史上被更改過的次數,字段名多取作“JDO_VERSION”
在OptimisticTransaction 的一次Transaction中,JDO底層不會對數據庫進行鎖定,這就保證了時間跨度較長的transaction不會影響其它線程(請求)的執行,只 是如果更新操作比較多,訪問量又比較大的話,Transaction提交失敗的的機率也會相應變大。
9 JDBC2.0和JDBC3.0
JDO只是一種對象級的包裝,是建立在JDBC的基礎上的,兩者不能相互替代。實際上,JDBC的規範從1.0到2.0,再到3.0,一直在做功能和性能方面的改進。JDO 產品當然不會放過這些,一般的JDO產品,會檢測底層配置的JDBC驅動是符合哪個規範,並會盡量採用驅動本身的功能來實現具體的操作。對代碼開發人員來 說,我們大多數情況下只能掌握JDBC1.0的操作,和少量的2.0的操作,只有一些很精通JDBC的高手纔會用到JDBC3.0中的高級功能。因此,採 用JDO也可以幫助我們在不瞭解JDBC3.0規範的情況下提高性能和效率。
換句話說,JDBC技術本身就是一件很複雜的東西,要想優化性能的 話,很多JDBC技術和數據庫技術是需要使用的,比如inner join, left/right outer join, Batch update,等等。這些對開發人員的技術要求很高,一方面要精確理解每種技術的應用範圍和實際使用的注意事項,另一方面代碼也會比較複雜。因此,既然有 衆多的有經驗的JDO廠商在做這些事情,我們又何必再花功夫呢?
以上我介紹了JDO對我們的數據庫項目開發的比較明顯的幾個好處,以後的文章中,我會繼續寫關於JDO使用中的概念性的問題和具體JDO產品的配置與使用,以及一些技巧。