摘要:
本文推薦的一個叫Amber的框架提供了一種相反的輕量級實現。這種實現利用Java注解(annotations)來管理JavaBeans的CRUD周期(Create Read Update Delete)。事務處理被交還給數據庫,而XML映射描述符則被注解代替。本文所面向的讀者是那些對不使用XML描述符來有效操縱數據庫感興趣的Java中級開發者。
計算機業中有一條不成文的說法:面向對象軟件和關系數據庫之間的數據共享,最好通過對象/關系(O/R)映射框架來進行,而這種框架是實體關系(ER)模型依賴于面向對象模型的。本文推薦的一個叫Amber的框架提供了一種相反的輕量級實現。這種實現利用Java注解(annotations)來管理JavaBeans的CRUD周期(Create Read Update Delete)。事務處理被交還給數據庫,而XML映射描述符則被注解代替。本文所面向的讀者是那些對不使用XML描述符來有效操縱數據庫感興趣的Java中級開發者。
動機普通O/R映射框架是非常強大的;但是在介紹如何設計和部署時,一些問題卻很少被提及。我們將列出這些缺點,并針對這些問題來演示一個叫Amber的框架。
1.????????OO驅動的數據模型導致了過于簡單的ER模型。
2.????????XML描述符使得維護困難。
3.????????在O/R層進行事務處理非常困難。
4.????????現有框架的學習曲線相對陡峭。
在兩種模型之間交換數據,比如ER模型和OO模型,必須克服所謂的阻抗不匹配。對于大多數O/R模型工具來說,對象模型處于支配地位。大體上,這意味著Java持久層負責從現有的對象模型生成ER模型。這個主意非常引人注目,因為當商務模型確定以后,開發團隊就再也不需要擔心持久化的問題了。
對于常規的O/R工具而言,ER模型是一個結果,一個產物,頂多是一個容器。而商務過程實際上是按ER模型設計的,這就導致了兩者之間的不協調。這樣的話,ER模型的調整就非常困難,甚至是不可能的,因為O/R框架可能會在任何時候重構ER模型。同樣,當商務過程發生改變時,O/R域的調整會自動重構ER域,于是ER模型變得令人費解,并且有時性能會下降到臨界點。
還有另一個問題。會被持久化的類需要在外部XML描述(映射)文件中部署。初看不錯,但是當我們處理現存的系統時,這很快就成了煩人的事。只要發生了一點變動,就得有不只一個地方需要修改,也就是源代碼和映射文件。
最后,現有的O/R框架是為了處理事務而設計的。綜合來看,這不是必須的,因為存儲容器(例如關系數據庫)是非常傻的容器。盡管我們不得不進行事務處理,但那并不是我們想要的。這些應該是數據庫的事。
介紹AmberAmber從相反的角度來解決數據交換的問題。它采用ER模型為參考來確立OO結構。它還采用存儲過程作為數據庫訪問主要方式,存儲過程提供了訪問數據庫的唯一途徑,并且完全的建立起事務處理機制。最后中間層會被實現為一系列存儲過程的集合。這意味著ER模型的專家,數據庫管理員要負責設計和優化包括存儲過程在內的一系列問題,于是比起自動創建的系統,新的系統能夠擁有更好的結構,更快的速度和更安全的訪問。因此,許多難題迎刃而解。
•????????事務能夠(或者說應該)被封裝進存儲過程。
•????????讀操作僅返回結果集合。
•????????寫操作只需要調用存儲過程,而不是在Java代碼中嵌入SQL。
•????????使用存儲過程,就不會因為SQL注入而導致安全漏洞。
當然,這意味著通常在Java代碼中處理的事被轉移到存儲過程中了。這樣不會有人犯錯了。這對Java開發者來說有莫大的好處。
映射Amber的核心在于,不管被提交到數據庫的查詢是什么,查詢結果都是一列Java對象。當然,這是從Java開發者的角度來看的。那么剩下的問題只是把字段映射到對象的屬性。以及在把數據寫入數據庫時,把Java對象的屬性映射到存儲過程的參數。
Amber把結果集映射到JavaBean,并用相同的機制在增刪改時把bean的內容映射到參數。對于JavaBean的相關信息和定義,請查看資源那一段。
這種做法用到了Java語言的新特性,這個叫做注解的特性是從J2SE 5.0開始使用的。
注解,在JSR 175中也叫做“元數據”,是一種輔助代碼,可以用來提供類,方法,屬性的詳細信息。在Javadoc API中,元數據本來是為了用來內聯文檔的。所以,在不干擾正常代碼的前提下,注解可以用來描述代碼的具體作用。如果你想知道關于注解更多的信息以及作用,請參考Tiger: A Developer's Notebook,或者看看我寫的一篇更有趣的文章" Annotations to the Rescue"。
一步一步來我們來解決一個小的持久化問題。從數據庫讀取一列Jedi對象,我們假設返回的結果集看起來像下面的表格。請注意,我們接下來的討論并不依賴于這些表格,盡管這些例子實際上都是基于這些表格的。一般來說,我們得到的表列數據是通過使用少量的SQL連接多個表或者視圖來得到的。(當然,先向星球大戰的愛好者們告罪。)

我們先定義一個叫Jedi的簡單類。
public class Jedi {
?? private Integer _id;
?? private String _name;
?? private Double _forceRating;
?? private Integer _age;
?? private Boolean _alive;
?? @ColumnAssociation(name="jedi_id")
?? public void setId( Integer id ) {
??????_id = id;
?? }
?? @ColumnAssociation(name="name")
?? public void setName( String name ) {
??????_name = name;
?? }
?? @ColumnAssociation(name="force_rating")
?? public void setForceRating( Double fr ) {
??????_forceRating = fr;
?? }
?? @ColumnAssociation(name="age")
?? public void setAge( Integer age ) {
??????_age = age;
?? }
?? @ColumnAssociation(name="alive")
?? public void setAlive( Boolean alive ) {
??????_alive = alive;
?? }
?? @ParameterAssociation(name="@jedi_id",
?? index=0, isAutoIdentity=true)
?? public Integer getId() {
??????return _id;
?? }
?? @ParameterAssociation(name="@name", index=1)
?? public String getName() {
??????return _name;
?? }
?? @ParameterAssociation(name="@force_rating",
?? index=2)
?? public Double getForceRating() {
??????return _forceRating;
?? }
?? @ParameterAssociation(name="@age", index=3)
?? public Integer getAge() {
??????return _age;
?? }
?? @ParameterAssociation(name="@alive", index=4)
?? public Boolean getAlive() {
??????return _alive;
?? }
}
這里發生了什么?你在類中看到getter和setter方法上面有兩種注解。
注解@ColumnAssociation用來把JavaBean中的setter方法和結果集中的一個字段連接起來,這樣從數據庫中得到的表列數據就能夠被Amber 寫入bean的屬性里。注解@ColumnAssociation只適用于setter方法,因為Amber使用這些注解以及從數據庫中讀取的相應值來尋找和調用那些方法。
同樣,getter方法需要@ParameterAssociation注解來把JavaBean的屬性和調用增刪改操作時的參數連接起來。這個注解只適用于getter方法,因為Amber使用getter方法來把值填到參數里。因為JDBC的緣故,需要提供參數的索引。這個也許的多余的,取決于數據庫以及存儲過程是否需要,不過為了完整性,以及遵循JDBC API規范,最好還是提供一下。
必須提供一個無參數的構造函數,因為這個類會被自動構造(通過反射)。在上面的類中,沒有無參數構造函數是允許的,因為我們沒有提供其他的構造函數,但是當我們增加了額外的構造函數時,就必須提供一個明確的給Amber。
這個JavaBean的作用是從數據庫里讀出數據以及寫回數據庫。完全不需要外部的描述文件。注意,我們也可以用這種方式建立任何類,不只是JavaBean。
你也許會奇怪:為什么用注解?為什么不是像JavaBean那樣通過屬性名來隱式關聯?我們這么做,是為了讓我們的設計保持一定的自由度。換句話說,我們的Java代碼不需要依賴于ER模型設計的字段名。如果你已經習慣于操作表,你也許不同意這點,但是當你使用的存儲過程里需要連接表和視圖時,你就不得不使用別名。
Amber的連接器和JDBC在我們開始讀寫數據庫之前,我們需要與數據庫建立連接。Amber使用一個Connector來訪問數據庫。簡單說來,這就是把數據庫驅動和連接字符串結合使用而已。在應用中,我們使用一個ConnectorFactory來管理可用連接。像下面的代碼那樣,我們使用一個本地的type-4驅動來初始化一個到SQL server的連接。我們假設服務器的名字是localhost,數據庫的名字是jedi,用戶名是use,密碼是theforce,為了簡單一點,我在下面的代碼中省略了全部的異常處理。
String driverClassName =
?? "com.microsoft.jdbc.sqlserver.SQLServerDriver";
String url =
?? "jdbc:microsoft:sqlserver://" +
?? "localhost;databasename=jedi;" +
?? "user=use;pwd=theforce";
Amber's Connector is associated with a String, alias under which it remains accessible from the ConnectorFactory. Here, we're going to use the alias starwars.
Amber的Connector使用一個String作為別名來從ConnectorFactory獲取連接,接下來,我們將使用別名starwars。
ConnectorFactory.getInstance().add(
?? "starwars", driverClassName, url
);
因為Connector是對JDBC連接的輕量級封裝,所以我們可以像以前一樣操作這個連接。
讀取封裝在Connector外面的是一個BeanReader對象,它需要一個Connector和一個Class來告訴reader從數據庫讀出的bean是什么類型的。現在讀取一列Jedi對象就只需要下面幾行了。
Connector connector =
?? ConnectorFactory.createConnector( "starwars" );
BeanReader reader =
?? new BeanReader( Jedi.class, connector );
Collection<Jedi> jediList =
?? reader.executeCreateBeanList(
??????"select * from jedi"
?? );
這段代碼使用了一種叫泛型的新特性,這種特性是從J2SE 5.0開始使用的。Collection聲明的那行代碼表明jediList一律由Jedi類型的對象組成。編譯器在這里會發出警告,reader只有在運行時刻才知道會產生什么類型的對象。因為在J2SE 5.0中,泛型在執行的時候會把類型信息抹掉,所以可能導致不安全的類型轉換。非常遺憾的,因為同一原因,我們不能把BeanReader寫成BeanReader<Jedi>。簡單說,就是Java的反射和泛型不能混合使用。
那么復合結構會如何呢?好吧,我們有幾種方法可以處理這個問題。比如,我們在Jedi和Fighter (例如,每個Jedi有好幾艘太空戰斗機)之間有一個一對多的關系。在數據庫中,Fighter的數據看起來像下面那樣。

換句話說,Luke有兩艘戰斗機(X-和B-Wing),Yoda則擁有一艘Star Destroyer,而Obi Wan已經死掉了。
數據之間的關系在OO域中有幾種方法可以模型化。我們只挑選最簡單的那種。所以我們需要Jedi類可以擁有一組Fighter對象作為成員。下面的Fighter類是為了讓Amber使用而建立的。
public class Fighter {
?? private Integer _id;
?? private Integer _jediId;
?? private String _name;
?? private Double _firepowerRating;
?? private Boolean _turboLaserEquipped;
?? @ColumnAssociation(name="fighter_id")
?? public void setId( Integer id ) {
??????_id = id;
?? }
?? @ColumnAssociation(name="jedi_id")
?? public void setJediId( Integer jediId ) {
??????_jediId = jediId;
?? }
?? @ColumnAssociation(name="name")
?? public void setName( String name ) {
??????_name = name;
?? }
?? @ColumnAssociation(name="firepower_rating")
?? public void setFirepowerRating( Double firepowerRating ) {
??????_firepowerRating = firepowerRating;
?? }
?? @ColumnAssociation(name="turbo_laser_equipped")
?? public void setTurboLaserEquipped(
??????Boolean turboLaserEquipped ) {
??????_turboLaserEquipped = turboLaserEquipped;
?? }
?? @ParameterAssociation(name="@fighter_id",
??????index=0,isAutoIdentity=true)
?? public Integer getId() {
??????return _id;
?? }
?? @ParameterAssociation(name="@jedi_id",index=1)
?? public Integer getJediId() {
??????return _jediId;
?? }
?? @ParameterAssociation(name="@name",index=2)
?? public String getName() {
??????return _name;
?? }
?? @ParameterAssociation(name="@firepower_rating",
??????index=3)
?? public Double getFirepowerRating() {
??????return _firepowerRating;
?? }
?? @ParameterAssociation(name="@turbo_laser_equipped",
??????index=4)
?? public Boolean getTurboLaserEquipped() {
??????return _turboLaserEquipped;
?? }
}
下面的是改進后的Jedi類。它新增加了一個List<Fighter>類型的成員。下面的J2SE 5.0代碼表明鏈表只包含Fighter類型的對象。新增加的代碼用粗體表示。
public class Jedi {
?? private Integer _id;
?? private String _name;
?? private Double _forceRating;
?? private Integer _age;
?? private Boolean _alive;
??
?? private ArrayList<Fighter> _fighterList =
??????new ArrayList<Fighter>();
??
?? @ColumnAssociation(name="jedi_id")
?? public void setId( Integer id ) {
??????_id = id;
?? }
?? @ColumnAssociation(name="name")
?? public void setName( String name ) {
??????_name = name;
?? }
?? @ColumnAssociation(name="force_rating")
?? public void setForceRating( Double forceRating ) {
??????_forceRating = forceRating;
?? }
?? @ColumnAssociation(name="age")
?? public void setAge( Integer age ) {
??????_age = age;
?? }
?? @ColumnAssociation(name="alive")
?? public void setAlive( Boolean alive ) {
??????_alive = alive;
?? }
?? @ParameterAssociation(name="@jedi_id",
??????index=0, isAutoIdentity=true)
?? public Integer getId() {
??????return _id;
?? }
?? @ParameterAssociation(name="@name", index=1)
?? public String getName() {
??????return _name;
?? }
?? @ParameterAssociation(name="@force_rating",
??????index=2)
?? public Double getForceRating() {
??????return _forceRating;
?? }
?? @ParameterAssociation(name="@age", index=3)
?? public Integer getAge() {
??????return _age;
?? }
?? @ParameterAssociation(name="@alive", index=4)
?? public Boolean getAlive() {
??????return _alive;
?? }
?? public ArrayList<Fighter> getFighterList() {
??????return _fighterList;
?? }
?? public void setFighterList( ArrayList<Fighter> fighterList ) {
??????_fighterList = fighterList;
?? }
}
從數據庫讀取Jedis的代碼看起來像下面這樣:
Connector connector =
?? ConnectorFactory.getInstance().createConnector( "starwars" );
BeanReader jediReader =
?? new BeanReader( Jedi.class, connector );
BeanReader fighterReader =
?? new BeanReader( Fighter.class, connector );
Collection<Jedi> jediList =
?? reader.executeCreateBeanList( "select * from jedi" );
for( Jedi jedi : jediList ) {
?? String query =
??????"select * from fighter where jedi_id = " + jedi.getId();
?? Collection<Fighter> fighters =
??????fighterReader.executeCreateBeanList( query );
?? jedi.setFighterList(
??????new ArrayList<Fighter>( fighters ) );
}
瞧,這就是Jedi們擁有的戰斗機了。請注意,我們并沒有敲出把Fighter讀進Jedi的代碼。因為Jedi和Fighter會嚴格的匹配。你會說上面的代碼在依賴注入模式中只是一些部件。也許我是在說大話,我只想說:把互相依賴的東西分開,并且使分布在各處的代碼共同工作。如果你想在這方面知道得更多,請看Martin Fowler的"Inversion of Control Containers and the Dependency Injection pattern"。
寫入現在,該寫入了。把改變了的Jedi寫入數據庫只需要下面幾行代碼。
Connector connector =
?? ConnectorFactory.getInstance().createConnector( "starwars" );
BeanWriter writer =
?? new BeanWriter( Jedi.class, connector );
writer.executeStringUpdate(
?? sampleBean, "UpdateJedi" );
這里,數據庫訪問通過生成SQL查詢字符串。最下面一行代碼生成執行字符串并發送到數據庫,修改了使用1000作為id的Jedi(就是Obi Wan)的狀態(假設我們把屬性alive改為true,把forceRating改為6.0)。
UpdateJedi
?? @name='Obi Wan Kenobi', @jedi_id=1000,
?? @alive=1, @force_rating=6.0, @age=30
如果你想建立一個新的Jedi,我們只需要簡單的構造一個新的Jedi并用下面的代碼寫入數據庫。
Jedi newJedi = new Jedi();
newJedi.setName( "Mace Windu");
newJedi.setAge( 40 );
newJedi.setAlive( false );
newJedi.setForceRating( 9.7 );
Connector connector =
????ConnectorFactory.getInstance().createConnector( "starwars" );
BeanWriter writer =
????new BeanWriter( Jedi.class, connector );
writer.executeStringInsert(
????newJedi, "InsertJedi" );
你會注意到,我們使用了不同的方法和存儲過程來寫入數據。最后字符串會是這樣。
InsertJedi
?? @name='Mace Windu', @alive=0,
?? @force_rating=9.7, @age=40
發生了什么?我們假設屬性jediId是由數據庫自動生成的。實際上,在上面定義的Jedi類中,我們指定@ParameterAssociation的屬性isAutoIdentity=true來達成這一點。因為數據庫會給bean提供主鍵,所以參數@jedi_id就省略了。
這里需要注意一下。因為jediId是由數據庫提供的,所以這個數據一定會通過存儲過程InsertJedi傳回數據庫。隨后,方法executeStringInsert返回一個JDBC的ResultSet,用來返回ID或者剛插入的數據行。這個信息可以手動處理,不過Amber提供了輔助函數來把新的ID注入到新對象中。
比起操作的透明度,讀寫時使用字符串來處理的類型安全問題更容易讓人擔心。因為把參數轉化成字符串后,類型信息就丟失了。然而,這種技術有一個很大的優勢:任何查詢字符串都會被記錄下來,數據庫管理員可以通過分析來找出錯誤原因,并且準確知道應用調用了什么或者從數據庫查詢了什么。這種類型的透明使得調試更加容易。
如果Jedi的戰斗機列表改變了,還是手動更新數據庫比較好。取決于Fighter列表發生的變化,比較粗魯的做法是刪除這個Jedi的全部戰斗機列表,然后把新的列表寫回數據庫中。假設我們手里有一個jedi對象和一列新的Fighter對象,我們接下來需要把新列表寫進fighters中。更進一步,我們假設通過存儲過程InsertFighter把一個新的Fighter對象寫進數據庫。
Connector connector =
?? ConnectorFactory.createConnector( "starwars" );
BeanWriter writer =
?? new BeanWriter( Fighter.class, connector );
connector.execute(
?? "delete from fighters where jedi_id = " + jedi.getId() );
for( Fighter fighter : fighters ) {
?? fighter.setJediId( jedi.getId() );
?? connector.executeStringInsert(
??????fighter, "InsertFighter" )
}
這段代碼處理一整套的執行字符串,每個字符串中的name分別對應著fighters表中的fighter:
InsertFighter @jediId=..., @name="...";
你也許注意到了,這個方法并沒動用事務。像上面說的那樣,這里并沒使用異常處理,如果delete操作失敗了,會產生一個SQLException,而后面的循環根本不會被執行。可是如果是其他情況呢?比如接下來的InsertFighter調用出錯了呢?這時事務是必須的,最好把操作放在存儲過程里面。如果我想在事務中從Fighter對象獲取全部參數以及Jedi ID并處理“新”戰斗機呢?這個話題值得在另一篇文章中討論。
局限和缺點像任何工具或者技術一樣,我們討論的方法具有一定的局限性。
•????????因為不使用XML描述符,所以當數據庫和對象域之間的接口發生改變時,就會出問題。實際上,當改變只發生在名字而不是類型時,或者沒有屬性/字段增減時,使用XML描述符比Amber好一點。如果不是上述情況,兩種系統都需要重新編譯和部署。
•????????復合管理不是自動化的。事實上,當你比較Amber和大的O/R框架時,你會發現有很多東西都不是自動化的。在把數據庫作為啞存儲設備或者用表連接中間層的商業設定中,Amber并沒有太大的用處。另一方面,你可以說Amber適合依賴注入風格的設計,以及數據之間的松耦合,這通常認為比隱式依賴要好。
•????????最后,注解分析以及自省機制的運行開銷比較大。在一個與數據庫有大量交互(比如,一個用于并發用戶交互的中間件)而不是單用戶或者少量用戶偶爾交互的系統中,Amber會導致性能問題。
結論這篇文章示范了一種相對于傳統的O/R映射相反的R/O映射。所謂的面向對象和關系系統之間的阻抗不匹配,在把關系數據模型定義成對象域的引用模型,以及使用存儲過程這一工具來操作數據庫(尤其是寫操作)之后,這個復雜的映射任務被簡化了。這種映射是通過注解這種Java 1.5的新語言特性來實現的。我們通過Amber框架來支持和演示了這種方法。
Amber是一個小型框架,易于學習和使用。只需要處理幾個非常接近JDBC的類。數據庫和JavaBean之間的連接通過注解來實現,不需要任何XML描述符,因為XML對人來說可讀性不高。而數據庫和應用之間的映射也都在bean類之中。Amber也提供了一種強制檢測機制來驗證內容,不過為了節約篇幅,就不在這篇文章中討論了。
Amber只做了一件事并且做得很好:把數據庫的列以及查詢參數映射到JavaBean的屬性。不多,也不少。Amber不是銀彈,也沒有解決那些龐大的工業O/R框架才能處理的問題。
Amber已經在一個商業環境中證明了它的價值。在Impetus,我們為一家德國最大的郵購公司提供了銷售人員解決方案,系統基于Java,使用了MS SQL Server,而我們使用Amber處理了全部的數據庫交互。自從今年春天(自從J2SE 5.0的到來)以來,我們沒有改變一點API,而且使用中也沒出現什么大問題。
版權聲明:任何獲得Matrix授權的網站,轉載時請務必保留以下作者信息和鏈接作者:Norbert Ehreke
deafwolf(作者的blog:
http://blog.matrix.org.cn/page/deafwolf)
原文:
http://www.matrix.org.cn/resource/article/44/44381_Amber+Object+Mapping.html關鍵字:relational;mapping;Amber