(本文的版權屬作者本人,歡迎轉載,但必須註明出處和原作者)
1 權責劃分:業務開發組和數據庫管理組 對一個項目來講,開發團隊在邏輯上劃分爲兩塊:業務開發組和數據庫管理組。兩者各有特點,各有責任,但相互之間界限很清晰,不會有什麼糾纏。下面用表格說明一下二者的區別:
人員構成 業務開發組 系統分析員、程序員。 數據庫管理組 DBA、運行維護人員。一般一到兩個人就可以,並可橫跨多個項目
工作內容 業務開發組 設計數據類圖,設計業務邏輯接口並用代碼實現(一般在類似sessionbean的類中) 數據庫管理組 通過數據類圖映射數據表,一般只需在JDO自動生成的表結構上作少許調整
所需知識和工具 業務開發組 UML、Java、JSP、Ant。工具可爲任何IDE。 數據庫管理組 JDO原理、UML中的類圖、連接池配置。工具包括PowerDesigner、數據庫訪問工具、具體使用的JDO產品的一些細節
相互的責任範?lt;BR>業務開發組 向數據庫管理組提交數據部分 的UML實體類圖,及某些細節(如某屬性是否很長的字符串)。在對方配置好PersistenceManagerFactory連接池後在代碼中調用。 數據庫管理組 根據UML類圖及業務開發組對某些細節的建議建立相應的數據庫(基本上自動完成),在服務器上配置相應的JDO PersistenceManagerFactory(一個J2EE Connector,就象配置數據庫連接池一樣)
工作量 業務開發組 與業務相關,因爲主要代碼量在業務邏輯上 數據庫管理組 一般情況下不大,但在從舊數據庫中導入數據時可能需要一些功夫,不過是一次性的工作
涉及數據結構變動的功能變更時所需工作 業務開發組 一方面調整UML實體類圖,提交給數據庫開發組;另一方面根據新功能需求改寫業務邏輯代碼 數據庫管理組 根據新的UML實體類圖調整數據庫結構(有的JDO產品可自動完成)。服務器配置不變。
由於面向數據庫管理組的工作內容比較簡單,只是量的問題,下面的介紹就儘量不涉及數據庫管理組的工作,而只面向業務開發組。
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());
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; }
有了這個類以後,我們只需要將Topic.content的類型換成List,而其訪問器的接口不變,仍是String,只是內容變一下:(並在JDO描述符中指明該List的元素類型是String)
public class Topic { … List content; … 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 {
public PersistenceManagerRetriever(java.util.Properties p) { pmf = JDOHelper.getPersistenceManagerFactory(p); }
public PersistenceManagerFactory pmf() { return pmf; }
public PersistenceManager pm() { return (PersistenceManager)get(); }
public void cleanup() { PersistenceManager pm = pm(); if(pm == null) return;
try { if(!pm.isClosed()) { Transaction ts = pm.currentTransaction(); if(ts.isActive()) { log.warn(+pmf.getConnectionURL()++ts); ts.rollback(); } pm.close(); }
} catch(Exception ex) { log.error(+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(+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產品的配置與使用,以及一些技巧。 本文的版權屬於筆者本人,但歡迎轉載,前提是註明出處和原作者。另外,歡迎在我的專欄中查看我的另幾篇文章,並提出寶貴意見!
|