?? 實(shí)體對(duì)象的抽象以及一種基于數(shù)據(jù)庫(kù)的實(shí)現(xiàn)(轉(zhuǎn)).txt
字號(hào):
作者:sure160
email: sure160@china.com
日期:6/13/2001 12:46:36 PM
簡(jiǎn)介:本文探討了關(guān)系數(shù)據(jù)庫(kù)中的實(shí)體對(duì)象在面向?qū)ο笳Z(yǔ)言中如何抽象、實(shí)現(xiàn),并提出一種實(shí)現(xiàn)方案。
一、為什么要使用實(shí)體對(duì)象的概念
實(shí)體對(duì)象指的是可永久存儲(chǔ)的數(shù)據(jù)對(duì)象,通常可以用關(guān)系數(shù)據(jù)庫(kù)的一張數(shù)據(jù)表或一張主表和與之連接的幾張子表來(lái)表示。為什么要引入實(shí)體對(duì)象的概念呢?我可以肯定您是看過(guò)關(guān)于面向?qū)ο髢?yōu)點(diǎn)的長(zhǎng)篇大論的,不過(guò)我覺(jué)得有必要再嘮叨兩句,因?yàn)椴皇撬杏肅++或JAVA編程的人都是使用面向?qū)ο蟮姆绞较雴?wèn)題的。
在現(xiàn)實(shí)世界中,對(duì)象比它的屬性要穩(wěn)定,所以數(shù)據(jù)要抽象成對(duì)象。比如由于需求的修改,一個(gè)圖書(shū)管理系統(tǒng)要顯示更多的書(shū)目詳細(xì)信息,圖書(shū)的屬性發(fā)生了變化,而圖書(shū)這個(gè)對(duì)象并沒(méi)有變,和其他對(duì)象(例如書(shū)架、借閱者)之間的關(guān)系也沒(méi)有變。用一個(gè)數(shù)據(jù)對(duì)象來(lái)保存書(shū)目信息的程序修改起來(lái)要簡(jiǎn)單的多。
把實(shí)體對(duì)象如何永久儲(chǔ)存的方法封裝到對(duì)象中可以實(shí)現(xiàn)使用對(duì)象的外部程序的與數(shù)據(jù)庫(kù)無(wú)關(guān),便于程序的移植。
在程序設(shè)計(jì)中使用實(shí)體對(duì)象概念把面向?qū)ο蠓治觥⒚嫦驅(qū)ο笤O(shè)計(jì)、面向?qū)ο缶幋a著幾個(gè)步驟連貫和一致。
可以減少外部程序中復(fù)雜性,使用這些實(shí)體對(duì)象的程序只需要簡(jiǎn)單的調(diào)用insert()或update()的方法,而不用去寫(xiě)討厭的SQL語(yǔ)句。
但是,由于歷史原因,即使象Java這樣的新興語(yǔ)言,也沒(méi)有在所有地方完全利用面向?qū)ο蟮乃枷搿W屛覀兛纯碕DBC吧,這是一個(gè)號(hào)稱用對(duì)象封裝的數(shù)據(jù)庫(kù)接口。可是它封裝的是什么呢?連接、語(yǔ)句、結(jié)果集和存儲(chǔ)過(guò)程。這其實(shí)并不是我們?cè)O(shè)計(jì)系統(tǒng)所關(guān)心的東西,我們關(guān)心的是數(shù)據(jù)對(duì)象本身,而不是它從數(shù)據(jù)庫(kù)中提取的方法。當(dāng)然,JDBC本身也不該受到太多的指責(zé),因?yàn)樗緛?lái)的目標(biāo)就是封裝、屏蔽關(guān)系型數(shù)據(jù)庫(kù)之間的差異,而不是企圖實(shí)現(xiàn)一個(gè)面向?qū)ο蟮臄?shù)據(jù)庫(kù)。
EJB第一次引起我的注意就是因?yàn)樗岢龅腅ntity Bean,也就是實(shí)體對(duì)象。它對(duì)實(shí)體對(duì)象提供了一套完整的實(shí)現(xiàn)思路,但是我認(rèn)為它太復(fù)雜了,主要原因是EJB想要做的事太多了。大部分情況下我并不需要分布式處理,我也不需要把實(shí)體對(duì)象存儲(chǔ)到文件中去。我需要的僅僅是在一個(gè)本地運(yùn)行的基于數(shù)據(jù)庫(kù)的程序。
二、EJB如何實(shí)現(xiàn)的實(shí)體對(duì)象
剛才提到了EJB已經(jīng)實(shí)現(xiàn)了實(shí)體對(duì)象,那么讓我們看看它是如何實(shí)現(xiàn)的。EJB中Entity Bean的對(duì)象實(shí)際上是對(duì)數(shù)據(jù)對(duì)象的一種完美的抽象,在這里我們幾乎看不到數(shù)據(jù)庫(kù)管理系統(tǒng)的作用。一個(gè)實(shí)體對(duì)象有幾種狀態(tài),在內(nèi)存中、在磁盤(pán)緩存中、或者在數(shù)據(jù)庫(kù)中,實(shí)體對(duì)象的這些狀態(tài)通常我們是不關(guān)心的,EJB的容器在必要的情況下會(huì)自動(dòng)轉(zhuǎn)換對(duì)象的狀態(tài),也就是說(shuō)自動(dòng)把它存到數(shù)據(jù)庫(kù)中,或從數(shù)據(jù)庫(kù)中取出。我們要訪問(wèn)一個(gè)數(shù)據(jù)對(duì)象,要向一個(gè)對(duì)象容器提出申請(qǐng),由它返回一個(gè)對(duì)象實(shí)例供我們使用。一個(gè)Entity Bean對(duì)應(yīng)于數(shù)據(jù)表中的一行。如果我們?cè)L問(wèn)的是同一行數(shù)據(jù),對(duì)象容器返回的是不同的對(duì)象,但都指向同一個(gè)Entity Bean,并把我們所有的方法請(qǐng)求都發(fā)送給這個(gè)Entity Bean。實(shí)際上EntityBean的容器基本實(shí)現(xiàn)了一個(gè)面向?qū)ο蟮臄?shù)據(jù)庫(kù)。它這種實(shí)現(xiàn)方法帶來(lái)幾個(gè)顯著的問(wèn)題:
運(yùn)行效率非常低。JAVA的速度慢是個(gè)老問(wèn)題了,EJB的速度慢不光是因?yàn)榇罅看a用JAVA實(shí)現(xiàn),而且由于它的結(jié)構(gòu),要根據(jù)數(shù)據(jù)表中的某一個(gè)屬性查出一行數(shù)據(jù),必須首先用SQL查詢查找到這一行的主鍵(Primary Key),然后通過(guò)主鍵來(lái)找到這個(gè)Bean,如果這個(gè)Bean不在內(nèi)存中--很不幸,這種情況經(jīng)常發(fā)生,那么實(shí)際上是執(zhí)行了兩次SQL查詢才找到一行數(shù)據(jù)。
容器本身要管理事務(wù),以防數(shù)據(jù)的污讀、污寫(xiě)、死鎖等等一系列問(wèn)題。本來(lái)DBMS管理這類問(wèn)題已經(jīng)有很多年經(jīng)驗(yàn)了,已經(jīng)相當(dāng)完美的解決了這些問(wèn)題,可是EJB不得不通過(guò)一個(gè)Transcation Server來(lái)管理這些問(wèn)題。這使得容器的代碼變得極其復(fù)雜,另外編程人員也不得不重新熟悉這些接口。
由于bean 中的數(shù)據(jù)是否存儲(chǔ)在數(shù)據(jù)庫(kù)里是由容器管理的,那么其他程序訪問(wèn)數(shù)據(jù)庫(kù)會(huì)帶來(lái)數(shù)據(jù)同步的問(wèn)題。因此,在EJB架構(gòu)中,外部程序不能直接訪問(wèn)數(shù)據(jù)庫(kù),只能通過(guò)EJB訪問(wèn)。
三、我們實(shí)現(xiàn)的目標(biāo)
我們只想把數(shù)據(jù)庫(kù)對(duì)象更好的封裝起來(lái),為什么要購(gòu)買(mǎi)別人昂貴的代碼?為什么要為我們不會(huì)用到的分布式去犧牲大量的性能。我們想要盡量使用DBMS的功能,以實(shí)現(xiàn)最佳的性能和最簡(jiǎn)化的代碼。
事務(wù)處理最好還是用DBMS來(lái)管理,因?yàn)樗幌蚬艿煤芎茫沂聞?wù)處理的代碼很復(fù)雜,我不打算自己來(lái)完成。
我不打算用同一個(gè)對(duì)象來(lái)指向數(shù)據(jù)庫(kù)的一行。數(shù)據(jù)庫(kù)的一行數(shù)據(jù)在程序中可能有多個(gè)對(duì)象,這些對(duì)象都應(yīng)該是臨時(shí)對(duì)象,而不是永久對(duì)象。他們的共享問(wèn)題由DBMS本身加鎖來(lái)解決。
由于我們只是為訪問(wèn)DBMS提供了一個(gè)接口,外部程序完全可以不通過(guò)這個(gè)接口來(lái)訪問(wèn)數(shù)據(jù)庫(kù)。
實(shí)現(xiàn)后,使用實(shí)體對(duì)象的代碼要簡(jiǎn)單。例如,數(shù)據(jù)庫(kù)里有一個(gè)表account,它只有兩個(gè)字段accountid和name,我們把它作為一個(gè)實(shí)體對(duì)象Account,假設(shè)我們要完成從數(shù)據(jù)庫(kù)查詢、修改、插入,使用它的代碼片段如下:
Connection conn=ConnectionPool.getConn(); //也可以通過(guò)標(biāo)準(zhǔn)的DriverManager得到數(shù)據(jù)庫(kù)連接,這是完全一樣的
Conn.setAutoCommit(false); //如果不使用事務(wù),這一行可以省去
Account a1=new Account(conn);
a1.getByAccountId(1); //查找到相應(yīng)記錄
System.out.println(a1.name);
a1.getByAccountIdForUpdate(1); //如果企圖修改一個(gè)對(duì)象,必須通過(guò)forUpdate系列的方法得到這個(gè)對(duì)象
a1.name="new name";
a1.update(); //修改原有記錄
Account a2=new Account(conn);
a2.accountId=3;
a2.name="姚大";
a2.insert(); //插入一條新記錄
conn.commit();
實(shí)體對(duì)象的屬性通過(guò)方法修改是比較理想的方式,如用getName(),setName()兩個(gè)方法訪問(wèn)Name屬性。這還可以解決屬性之間相互關(guān)聯(lián)的問(wèn)題。例如,表中有一個(gè)地區(qū)代碼和地區(qū)名稱兩個(gè)域,必須保持一致,這就可以考慮在屬性設(shè)置方法中實(shí)現(xiàn)。在這里我們?yōu)榱撕?jiǎn)單,直接通過(guò)屬性修改,在一般情況下,我覺(jué)得也是可以接受到。
有一點(diǎn)必須注意的就是多線程程序所帶來(lái)的數(shù)據(jù)完整性問(wèn)題。對(duì)于我們常使用的從數(shù)據(jù)庫(kù)讀取數(shù)據(jù)到對(duì)象中->修改對(duì)象屬性->更新到數(shù)據(jù)庫(kù)這個(gè)流程,非常容易出現(xiàn)數(shù)據(jù)完整性破壞問(wèn)題。比如一個(gè)進(jìn)程中甲對(duì)象讀取數(shù)據(jù)后,另一個(gè)進(jìn)程中乙對(duì)象又修改了同一數(shù)據(jù),這時(shí)甲對(duì)象再次更新數(shù)據(jù)庫(kù)會(huì)帶來(lái)污寫(xiě)。解決的辦法是對(duì)象增加一個(gè)方法給數(shù)據(jù)庫(kù)的這條記錄加鎖。例如Account.getByAccountId函數(shù)改為Account.getByAccountIdForUpdate,這個(gè)函數(shù)中相應(yīng)的sql語(yǔ)句(ORACLE數(shù)據(jù)庫(kù))改為"select AccountId,name from account where accountId=? for update",這樣數(shù)據(jù)取出后就自動(dòng)加鎖,這個(gè)鎖將會(huì)在事務(wù)提交或回滾時(shí)釋放。for update在SQLServer相應(yīng)的語(yǔ)法為holdlock。
說(shuō)到數(shù)據(jù)庫(kù)加鎖的問(wèn)題就不能不考慮到數(shù)據(jù)庫(kù)死鎖的可能。想完全避免死鎖是很困難的,只有盡量降低這種可能性。方法是:1、盡量少使用forUpdate這種函數(shù),只有在更改數(shù)據(jù)庫(kù)數(shù)據(jù)時(shí)才使用。查詢的時(shí)候不要用,如果查詢后根據(jù)某個(gè)條件有可能修改,那么在查詢時(shí)不加鎖,在修改前重新調(diào)用forUpdate函數(shù)加鎖。2、盡量以某個(gè)特定的順序加鎖。例如有表A和表B,兩個(gè)程序都要同時(shí)更新這兩個(gè)表,最好都是先訪問(wèn)表A,再訪問(wèn)表B。
Oracle有個(gè)很好的功能就是自動(dòng)檢測(cè)死鎖。如果發(fā)生死鎖,會(huì)回滾一個(gè)事務(wù),并返回一個(gè)SQL錯(cuò)誤,我們的程序要檢測(cè)這個(gè)異常,處理程序中可能的錯(cuò)誤。
上面說(shuō)到的對(duì)象中,只有g(shù)etByXXX()這樣的方法,這種方法只會(huì)返回唯一的對(duì)象,如果想要返回一組對(duì)象,這時(shí)需要一個(gè)輔助類來(lái)實(shí)現(xiàn)。這個(gè)輔助類稱為對(duì)象瀏覽器EntityBrowser。再實(shí)體對(duì)象中返回一個(gè)對(duì)象瀏覽器的方法一般命名為getAllByXXX()
使用對(duì)象瀏覽器EntityBrowser要列出所有account表中的id和name的代碼如下,其實(shí)這個(gè)對(duì)象瀏覽器和Java定義的Enumeration接口的最大區(qū)別就是它有一個(gè)close方法。請(qǐng)看下面的代碼片斷,它輸出所有的Accout對(duì)象。
Connection conn=ConnectionPool.getConn();
Account a1=new Account(conn);
EntityBrowser browser=a1.getAll();
System.out.println("==Account List==");
While (browser.hasMoreElement()) {
Account a=(Account)browser.nextElement();
System.out.print(a.accountId);
System.out.print("--");
System.out.println(a.name);
}
browser.close();
以上就是我們要實(shí)現(xiàn)的實(shí)體對(duì)象,下面介紹如何編寫(xiě)一個(gè)實(shí)體對(duì)象。
四、實(shí)現(xiàn)的代碼解釋
首先,下面的代碼定義一個(gè)所有實(shí)體對(duì)象的基礎(chǔ)類EntityObject,這是一個(gè)抽象類,不能直接使用,但為其它實(shí)體對(duì)象定出了一個(gè)結(jié)構(gòu)。
Import java.sql.*;
/**所有實(shí)體對(duì)象基礎(chǔ)類*/
abstract public class EntityObject {
protected java.sql.Connection _conn;
private boolean _dbStored;
/**實(shí)體對(duì)象需要用一個(gè)數(shù)據(jù)庫(kù)連接初始化,這樣可以利用這個(gè)連接做事務(wù)提交或回滾*/
public EntityObject(Connection conn) {_conn=conn;_dbStored=false;}
/**這個(gè)方法用來(lái)插入新記錄,子類必須重定義這個(gè)方法*/
public void insert() throws SQLException{ _dbStored=true;}
/**這個(gè)方法用來(lái)修改數(shù)據(jù)庫(kù)原有記錄,子類必須重定義這個(gè)方法*/
public void update() throws SQLException {_dbStored=true; }
/**這個(gè)方法用來(lái)刪除數(shù)據(jù)庫(kù)原有記錄,子類必須重定義這個(gè)方法*/
public void delete() throws SQLException {_dbStored=false; }
/**這個(gè)方法用來(lái)把數(shù)據(jù)庫(kù)select語(yǔ)句得出的結(jié)果映射到對(duì)象的屬性中去,子類必須重定義這個(gè)方法*/
public void _setAttribute(ResultSet rs) throws SQLException {_dbStored=true; }
/**這個(gè)方法可以判斷這個(gè)對(duì)象是否已存在數(shù)據(jù)庫(kù)中*/
public boolean isDbStored() {return _dbStored;}
/**這個(gè)方法將實(shí)體對(duì)象設(shè)為不存在數(shù)據(jù)庫(kù)中,所有的getByxxx()方法首先要調(diào)用這個(gè)方法,使本對(duì)象無(wú)效,才能用select語(yǔ)句得到符合條件的對(duì)象*/
public void clearDbStored() {_dbStored=false;}
}
下面我們就可以定義一個(gè)實(shí)際的實(shí)體類。考慮一種簡(jiǎn)單情況,實(shí)體對(duì)象在關(guān)系數(shù)據(jù)庫(kù)中只用一張表表示。這樣的實(shí)體對(duì)象定義最簡(jiǎn)單。例如,數(shù)據(jù)庫(kù)里有一個(gè)表account,它只有兩個(gè)字段accountid和name,下面定義它的實(shí)體類。實(shí)體類必須重載insert,update,_setAttribute方法,然后根據(jù)需要增加getByXXX()方法,根據(jù)某個(gè)屬性從數(shù)據(jù)庫(kù)查找一個(gè)對(duì)象。
Import java.sql.*;
public class Account extends EntityObject {
/*這里定義實(shí)體對(duì)象類所有的屬性*/
public int accountId;
public String name;
//最好將實(shí)體對(duì)象中的屬性全部定義成私有,然后定義getXXX(), setXXX()兩個(gè)方法來(lái)訪問(wèn)這個(gè)屬性,這樣的封裝性最好,但略過(guò)于麻煩。如果數(shù)據(jù)結(jié)構(gòu)預(yù)料會(huì)經(jīng)常修改,最好用這個(gè)辦法。否則,定義成公有屬性也可。
Public Account(Connection conn){ super(conn); }
//重定義這個(gè)方法,執(zhí)行實(shí)際的sql命令
public void insert() throws SQLException {
?? 快捷鍵說(shuō)明
復(fù)制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號(hào)
Ctrl + =
減小字號(hào)
Ctrl + -