Java企業級版本,或者說Java EE(以前叫J2EE),對於開發服務器端的應用來說是一個強大的但卻又過於複雜的的平臺。從它誕生之日起,過於複雜一直是對使用Java EE猶豫不決的一個重要因素。在JavaWorld的以前的一篇文章”簡化之路”中,我指出了那些讓Java EE應用變複雜的因素,其中很多都是與當前的EJB 2.1規範有關。
在過去的三年中,Java開放源代碼社區,Java社區進程(JCP)以及主要的Java EE供應商,一直致力於讓Java EE更簡單。舉例來說:新的設計範例,比如POJO服務,服務攔截器和依賴注入,已經可以在實際應用中用來簡化Java EE的開發了。還有,新的工具和框架,比如Hibernate, AOP(aspect-oriented programming,面向方面編程),Struts,Xdoclet和Spring, 也已經被廣泛用於同一目的。
簡化不是功能的減少
簡化一個編程模型並沒有減少它的功能。簡化只是把複雜的邏輯隱藏到了框架代碼或可重用的組件中去了。根本上,它是把複雜的東西從需要應用開發者直接管理的地方轉移到了大多數開發者看不到的地方。
上述的模板和工具讓初學者更容易上手,同時也提高了有經驗的Java開發者的生產力,現在它們正在被JCP合併到下一代的Java EE標準中(比如:EJB 3.0)。由Java開發人員Raghu Kodali最近所做的研究顯示:將Java EE的示例程序RosterApp從EJB 2.1轉到EJB 3.0可以減少百分之五十以上的代碼。
Java注釋是EJB3.0背後的關鍵,它將POJO服務,POJO持久化和依賴注入一起綁定爲一個完整的企業級中間件解決方案。這篇文章中,我使用了一個示例應用:JBoss EJB 3.0 TrailBlazer,來演示使用注釋開發輕量級的EJB 3.0 POJO應用。TrailBlazer的應用使用EJB 3.0中不同的工具和API重復實現了一個投資計算器。示例程序完全可以在JBoss 應用服務器4.0.3版本中運行,並且與最新的EJB 3.0標準完全兼容(完成時)。
讓我們來開始體驗一下注釋驅動編程模型的好處吧。
EJB 3.0的注釋驅動編程模型
從開發者的觀點來看,EJB 3.0廣泛地使用了Java 注釋.注釋有兩個關鍵優勢:它們取代了過多的XML配置文件並且消除了嚴格組件模型需求。
注釋 vs XML
基於XML的佈署描述和注釋一起都可以用來在Java EE應用中配置服務的相關屬性。它們的區別在於:XML文檔是與代碼分開處理的,特別是在運行時刻,而注釋是與代碼編譯在一起的並被編譯器檢查的。對於開發者來說這就有了一些重要的含義,正如我下面所列出的:
冗長:XML配置文件是出了名的冗長的。爲了配置代碼,XML文件必須復制許多信息:比如代碼中類名字和方法名字。Java注釋則不同,它是代碼的一部分,不需要額外的引用就可以指明配置信息。
強壯性:在XML文件中重復的代碼信息引入了多處出錯的可能。比如,如果你寫錯了方法的名字,那應用直到運行時刻纔會出錯垮掉。也就是說,XML配置文件的強壯性就不如注釋,注釋是被編譯器檢查的,並和其它代碼一起被處理的。
靈活性:既然XML文件是在代碼之外被單獨處理的,那也就是說基於XML的配置信息不是“硬編碼”的,是可以以後修改的。部署的靈活性對系統管理員來說是非常非常重要的特性。
注釋是簡單易用的,已證明對大多數應用來說足夠了。XML文件更複雜,但能被用來處理更高級的問題。EJB 3.0允許你通過注釋來配置大多數的應用。EJB 3.0也支持用XML文件來覆蓋默認的注釋,及配置像數據庫聯接這樣的外部資源。
除了替換和簡化XML描述符,注釋也允許我們廢除困擾EJB 1.x, EJB 2.x的嚴格組件模型。
POJO vs 嚴格組件
EJB 組件是容器管理(container-managed)的對象。容器在運行時刻操作Bean的狀態和行爲。爲了讓行爲發生,EJB 2.1規範定義了一個Bean必須遵守的嚴格的組件模型。每一個EJB類必須從某一種抽象類中繼承,並爲容器提供了回調的鉤子。既然Java只支持單繼承,嚴格組件模型就限制了開發者使用EJB組件創建一個複雜對象結構的能力。當把複雜的應用數據映射到實體 Bean中的時候,正如我們在第二部分中看到的,這會成爲一個很大的問題。
在EJB 3.0中,所有的容器服務都可以通過使用注釋的POJO應用來配置和交付。大多數情況下,並不需要特殊的組件類。讓我們通過JBoss EJB 3.0 TrailBlazer示例看一下如何在EJB 3.0中使用注釋。
開發藕合鬆散的服務對象
像Java EE這樣的企業級中間件的一個最重要的好處是允許開發者使用藕合鬆散的組件來開發應用。這些組件僅僅通過他們自己發布的商業接口來藕合。因此這些組件的實現類可以在不改變應用其餘部分的情況下改變自己的實現。這將會使應用更加強壯,更容易測試,更易移植。EJB 3.0使得在POJO中創建藕合鬆散的商業組件變得更簡單了。
Session bean
在EJB 3.0應用中,藕合鬆散的服務組件的典型應用是Session Bean。一個Session Bean至少有一個接口(也就是:商業接口),其它應用組件通過它獲得服務。下面的代碼爲我們的投資計算器服務提供了商業接口。它只有一個方法,根據給定的起始年齡,終止年齡,增長率,月存金額,計算出總投資額。
public interface Calculator {
public double calculate (int start, int end,
double growthrate, double saving);
}
Session bean類簡單地實現了商業接口。你必須通過使用Stateless或Stateful注釋來告訴EJB 3.0容器這個POJO類是一個Session Bean。有狀態(Stateful)的session bean在不同的服務請求間維護着客戶的狀態。相反地,對於無狀態(Stateless)的session bean,每次的請求都是被隨機挑選的session bean實例處理的。這些行爲是與EJB 2.1規範中的有狀態和無狀態session bean的定義是一致的。EJB 3.0容器算出何時實例化Bean對象,並通過商業接口讓其可用。下面是session bean實現類的代碼:
@Stateless
public class CalculatorBean implements Calculator {
public double calculate (int start, int end,
double growthrate, double saving) {
double tmp = Math.pow(1. + growthrate / 12.,
12. * (end - start) + 1);
return saving * 12. * (tmp - 1) / growthrate;
}
}
你也可以爲一個session bean指明多個接口-一個爲本地客戶服務,一個爲遠程客戶服務。只要使用@Local和@Remote注釋來區分。下面的代碼片斷顯示了同時實現了本地和遠程接口的CalculatorBean。如果你沒有@Local和@Remote注釋,session bean接口默認爲本地接口。
@Stateless
@Local ({Calculator.class})
@Remote ({RemoteCalculator.class})
public class CalculatorBean implements Calculator, RemoteCalculator {
public double calculate (int start, int end,
double growthrate, double saving) {
double tmp = Math.pow(1. + growthrate / 12., 12. * (end - start) + 1);
return saving * 12. * (tmp - 1) / growthrate;
}
public String getServerInfo () {
return "This is the JBoss EJB 3.0 TrailBlazer";
}
}
Session bean用戶通過JNDI得到bean的一個存根(Stub)對象。容器所提供的存根對象實現了session bean的商業接口。所有針對存根的調用都被引向了容器,由容器調用相應的實現類中的接口。對於有狀態的的session bean,你必須自己在客戶端緩存存根對象,這樣在每次的後續調用時,容器才知道要提供相同的的bean實例。下面的片斷顯示如何調用session bean.在後面,你將會學到獲取存根對象的更簡單的方法。
InitialContext ctx = new InitialContext();
cal = (Calculator) ctx.lookup(Calculator.class.getName());
double res = cal.calculate(start, end, growthrate, saving);
Session bean生命週期的管理
爲達到藕合鬆散的目的,應用把session bean實例的創建、緩存、銷燬全部交給EJB 3.0容器(也就是,反向控制設計模式)。應用只和bean的商業接口打交道。
但如果應用需要對session對象更好的控制呢?比如說,應用可能需要在創建session bean的時候初始化數據庫聯接,而在銷燬bean時關閉外部的聯接。上述這些,你都可能通過在bean類中定義生命週期的回調方法來實現。這些方法將會被容器在生命週期的不同階段調用(如:創建或銷燬時)。通過使有下面所列的注釋,EJB 3.0允許你將任何方法指定爲回調方法。這不同於EJB 2.1,EJB 2.1中,所有的回調方法必須實現,即使這是空的。EJB 3.0中,bean可以有任意數量,任意名字的回調方法。
@PostConstruct:當bean對象完成實例化後,使用了這個注釋的方法會被立即調用。這個注釋同時適用於有狀態和無狀態的session bean。
@PreDestroy:使用這個注釋的方法會在容器從它的對象池中銷燬一個無用的或者過期的bean實例這前調用。同時適用於有狀態和無狀態的session bean.
@PrePassivate:當一個有狀態的session bean實例空閒過長的時間,容器將會鈍化它,並把它的狀態保存下來。使用這個注釋的方法會在容器鈍化bean實例之前調用。適用於有狀態session bean。
@PostActivate:當客戶端再次使用已經被鈍化的的有狀態session bean時,新的實例被創建,狀態被恢復。使用此注釋的session bean會在bean的激活完成時調用。
@Init:這個注釋指定了有狀態session bean初始化的方法。它區別於@PostConstruct注釋在於:多個@Init注釋方法可以同時存在於有狀態session bean 中,但每個bean實例只會有一個@Init注釋的方法會被調用。這取決於bean是如何創建的(細節請看EJB 3.0規範)。@PostConstruct在@Init之後被調用。
另一個有用的生命週期方法注釋是@Remove,特別是對於有狀態session bean。當應用通過存根對象調用使用了@Remove注釋的方法時,容器就知道在該方法執行完畢後,要把bean實例從對象池中移走。
@Stateful
public class CalculatorBean implements Calculator, Serializable {
// ... ...
@PostConstruct
public void initialize () {
// Initializes the history records and load
// necessary data from database etc.
// 初始化歷史記錄,並從數據庫中裝入必需的數據。
}
@PreDestroy
public void exit () {
// Save history records into database if necessary.
// 如有必要則將歷史記錄保存至數據庫中
}
@Remove
public void stopSession () {
// Call to this method signals the container
// to remove this bean instance and terminates
// the session. The method body can be empty.
// 調用這個方法來通知容器將bean實例移除並中止session.
// 這個方法可以爲空。
}
// ... ...
}
消息驅動bean
Session bean服務提供了同步調用的方法。另一個重要的藕合鬆散服務類型是一種通過進入的消息來觸發的異步服務(比如:email或Java消息服務產生的消息)。EJB 3.0的消息驅動bean(MDB)是設計用來專門處理基於消息請求的組件。
一個MDB類必須實現MessageListener接口。當容器檢測到bean守候的隊列一條消息時,就調用onMessage()方法,將消息作爲參數傳入。MDB在OnMessage()中決定如何處理該消息。你可以用注釋來配置MDB偵聽哪一條隊列。當MDB部署時,容器將會用到其中的注釋信息。在下面的例子中,CalculatorBean MDB會在JMS隊列queue/mdb有消息進入時調用。MDB解析消息,並根據消息內容計算投資。
@MessageDriven(activateConfig =
{
@ActivationConfigProperty(propertyName="destinationType",
propertyValue="javax.jms.Queue"),
@ActivationConfigProperty(propertyName="destination",
propertyValue="queue/mdb")
})
public class CalculatorBean implements MessageListener {
public void onMessage (Message msg) {
try {
TextMessage tmsg = (TextMessage) msg;
Timestamp sent =
new Timestamp(tmsg.getLongProperty("sent"));
StringTokenizer st =
new StringTokenizer(tmsg.getText(), ",");
int start = Integer.parseInt(st.nextToken());
int end = Integer.parseInt(st.nextToken());
double growthrate = Double.parseDouble(st.nextToken());
double saving = Double.parseDouble(st.nextToken());
double result =
calculate (start, end, growthrate, saving);
RecordManager.addRecord (sent, result);
} catch (Exception e) {
e.printStackTrace ();
}
}
// ... ...
}
依賴注入
在上一節中,你學到了如何開發藕合鬆散的服務組件。但是,爲了存取那些服務對象,你需要通過服務器的JNDI來查找存根對象(session bean)或消息隊列(MDB)。JNDI查找是把客戶端與實際的服務端實現解藕的關鍵步驟。但是,直接使用一個字符串來進行JNDI查找並不優雅。有這樣幾個原因:
客戶端與服務端必須有一致的基於字符串的名字。它沒有在編譯時得到認證或在佈署時得到檢查。
從JNDI返回的服務對象的類型沒有在編譯時進行檢查,有可能在運行時出現轉換(casting)錯誤。
冗長的查找代碼,有着自己的try-catch代碼塊,在應用之間是重復的和雜亂的
EJB 3.0,對任何POJO,提供了一個簡單的和優雅的方法來解藕服務對象和資源。使用@EJB注釋,你可以將EJB存根對象注入到任何EJB 3.0容器管理的POJO中。如果注釋用在一個屬性變量上,容器將會在它被第一次訪問之前賦值給它正確的值。下面的例了演示了怎樣把CalculatorBean無狀態session bean的存根注入到CalculatorMDB MDB類中。
public class CalculatorMDB implements MessageListener {
@EJB Calculator cal;
// Use the cal variable
// ... ...
}
注釋如果被用在JavaBean風格的setter方法上時,容器會在屬性第一次使用之前,自動地用正確的參數調用bean的setter方法。下面的片斷演示了這是如何做的:
public class CalculatorMDB implements MessageListener {
Calculator cal;
@EJB
public void setCal (Calculator cal) {
this.cal = cal;
}
// Use the cal variable
// 使用cal變量
// ... ...
}
除@EJB注釋之外,EJB 3.0也支持@Resource注釋來注入來自JNDI的任何資源。下面的例子中,我演示瞭如何注入服務器端默入的TimerService和SessionContext對象,也演示瞭如何注入來自JNDI的命名數據庫和JMS資源。
@Resource
TimerService tms;
@Resource
SessionContext ctx;
@Resource (name="DefaultDS")
DataSource myDb;
@Resource (name="ConnectionFactory")
QueueConnectionFactory factory;
@Resource (name="queue/A")
Queue queue;
此外,你也可以把一個容器管理的持久化管理器(也就是,EntityManager-類似於Hibernate session對象)注入到EJB 3.0 POJO中。
把容器服務交給POJO
除了管理生命週期和訪問藕合鬆散的服務對象外,EJB 3.0通過簡單的注釋也爲POJO提供了運行時刻服務。
事務
最有用的容器服務可能就是事務管理服務,當應用出現失敗或異常時,它保證了數據庫的完整性。你可以簡單地將爲一個POJO方法申明它的事務屬性。這樣容器就可以在合適的上下文中運行這個方法。舉例來說,下面的代碼申明瞭容器在運行updateExchangeRate()時必須創建一個新的事務。當這個方法退出時提交事務。實際上,所有在updateExchangeRate()中被調用的方法都在此事務中運行,除非有特別申明。在updateExchangeRate()中的數據庫操作要麼全部成功,要麼全部失敗。
@Stateless
public class CalculatorBean implements Calculator {
// ... ...
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void updateExchangeRate (double newrate) throws Exception {
// Update the database in a loop.
// 在循環中更新數據庫
// ... ...
// The operations in the loop must all be successful or
// the database is not updated at all.
// 循環中的操作必須全部成功或者根本不更新。
}
}
安全
容器也提供了安全服務來進行用戶認證和根據用戶規則來限制對POJO的訪問。對每一個POJO來說,你可以通過使用@SecurityDomain注釋爲它指定一個安全域, 安全域告訴容器到哪裏去找密碼和用戶角色列表。JBoss中的other域表明文件是classpath中的users.propertes和roles.properties。這樣,對每一個方法來說,我們可以使用一個安全限制注釋來指定誰可以運行這個方法。比如,下面的例子,容器對所有試圖調用addFund()的用戶進行認證,只允許擁有AdminUser角色的用戶實際運行它。如果你沒有登錄或者沒有以管理員的身份登錄,一個安全意外將會拋出。
@Stateless
@SecurityDomain("other")
public class CalculatorBean implements Calculator {
@RolesAllowed({"AdminUser"})
public void addFund (String name, double growthrate) {
// ... ...
}
@RolesAllowed({"AdminUser"})
public void addInvestor (String name, int start, int end) {
// ... ...
}
@PermitAll
public Collection <Fund> getFunds () {
// ... ...
}
// ... ...
@RolesAllowed({"RegularUser"})
public double calculate (int fundId, int investorId,
double saving) {
// ... ...
}
}
通用攔截器
事務和安全服務都可以被看作是容器管理的運行時刻攔截器。容器攔截了對EJB存根的調用,並在其上應用事務上下文或進行安全限制。
在EJB 3.0中,你可以自己寫攔截器來擴展容器服務。使用@AroundInvoke注釋,你可以將任意bean方法作爲攔截器方法在任意bean方法之前和之後運行。下面的例子中,log()方法是一個攔截器,它計算和記錄了其它bean方法的執行時間:
@Stateful
public class CalculatorBean implements Calculator {
// Bean methods that are to be intercepted by "log()"
// bean方法將被log()方法攔截
//
// ... ...
@AroundInvoke
public Object log (InvocationContext ctx)
throws Exception {
String className = ctx.getBean().getClass().getName();
String methodName = ctx.getMethod().getName();
String target = className + "." + methodName + "()";
long start = System.currentTimeMillis();
System.out.println ("Invoking " + target);
try {
return ctx.proceed();
} catch(Exception e) {
throw e;
} finally {
System.out.println("Exiting " + target);
cal.setTrace(cal.getTrace() + "
" +
"Exiting " + target);
long time = System.currentTimeMillis() - start;
System.out.println("This method takes " +
time + "ms to execute");
}
}
}
下一步?
在第一部分中,我大致地討論了EJB 3.0基於POJO的編程模型和如何在EJB 3.0中開發藕合鬆散的服務組件。在第二部分中,我會討論EJB 3.0的另一個主要的概念:可管理的POJO持久性。
使用EJB 3.0簡化Java開發(二)
在第一部分中,我討論了在企業級JavaBean 3.0(EJB)中注釋驅動的POJO編程模型。我闡述瞭如何開發POJO服務,如何讓容器服務使用POJO, 如何使用依賴注入來組合應用。這些POJO服務主要是用來封裝應用的商業邏輯。在商業邏輯的背後,現今的大多數應用都有由一個高性能的關系型數據庫作爲支撐的數據模型層。
在第二部分中,我將討論EJB 3.0實體bean如何利用POJO和注釋的優勢來極大地簡化你的數據模型以及它們與後臺關係數據庫的持久化。在我們進入EJB 3.0實體bean的細節之前,讓我們先來看一下爲什麼對於企業級Java應用,數據模型和持久化是如此巨大的一個挑戰。
對象-關系映射(ORM)
在Java虛擬機中,所有的數據都被模型化並且封裝在了類和對象的樹結構中。但是在後端的關系型數據庫中,數據被模型化爲關系型表,它們通過共享的鍵域(外鍵)相互關聯起來。相同的數據卻有兩個視圖,這對企業級Java的開發者來說是一個艱難的挑戰:當你想從持久化的數據存儲中存取數據時,你必須在對象與關系表達之間來回轉換,這一過程叫做對象-關系映射(ORM)。在Java EE(Java企業版,以前叫做J2EE),你可以通過兩個途徑來實現對象-關系映射。
手動的:你可以使用Java數據庫連接來直接操作持久化-對於簡單應用的直截了當的解決方案。JDBC API的類是緊貼在關系型數據庫表、行和列之後的數據模型。你必須手動地在應用的內部對象模型與JDBC對象模型之間進行轉換,如果你的應用的內部模型本身就類似於2維的關系表的話,那採用JDBC是最佳手段。
自動的:你可以把ORM交給框架。框架通常向你提供一個可以和任意數據對象進行交互的API。通過那個API,你可以存儲、獲取和查詢數據庫。框架在後臺完成了框架對象的轉換。因爲特定的關系型SQL查詢不適合對象接口,ORM框架通常定義它自己的查詢語言,並且自動爲當前關系型數據庫生成正確的SQL語句。對於擁有複雜的數據模型的應用來說,基於框架的手段能爲你節省很多時間並降低了出錯的可能。
對象數據庫
一個對象型數據庫直接在數據庫中存儲、獲取和查找對象。因爲不再需要ORM,所以它對於Java應用非常適合。不幸的是,現今的對象型數據庫相對於關系型數據庫來說還不成熟,速度也慢。你可以這樣說,一個好的ORM框架從根本上來說,就是爲關系型數據庫提供一個對象型數據庫的接口。兩者它都要做到最好。
這篇文章,我將重點放在專爲企業級Java ORM應用設計的自動框架上。下一節,我將提到幾個流行的ORM框架和EJB 3.0中幾個關鍵的革新。
ORM 框架
EJB 實體bean是Java EE中“官方”的ORM解決方案。但是,在EJB1.x和2.x中,實體bean的難以使用是出了名的,原因如下:
●EJB 1.x和2.x實體bean必須遵守一種嚴格的組件模型。每一個bean類必須實現一個home接口和一個商業接口。它們必須從某種抽象類中繼承,而且必須實現其所有方法,即使它們多數爲空。這樣的一種嚴格組件模型使得想從EJB 1.x和2.x的實體bean中構建面向對象的數據模型幾乎變得不可能了。
●EJB 1.x和2.x容器需要特別冗長的xml配置文件來建立實體bean與關系型數據庫中的表映射。那些文件是非常單調乏味和容易出錯的。
簡而言之,EJB 1.x和2.x實體bean是一個設計拙劣的ORM框架。它既沒有滿足Java數據對象模型的需求,也沒有滿足關系表數據模型的需求。出於對EJB 1.x和2.x實體bean的不滿,開發者開始尋找其它的ORM方案。實際使用中,開源的Hibernate(JBoss開發)和Oracle公司的TopLink是最成功的兩個POJO ORM框架。Hibernate和TopLink都是基於POJO的。它們不依賴於任何預定義的組件模型。作爲替代,它們使用POJO數據對象(簡單的JavaBean式的),自動地解讀出如何映射它們,以及它們之間的關系(關系型數據庫)。通常,JavaBean類映射到一張數據庫表,並根據數據庫表中的外鍵映射出類之間的關系。你可以在一個簡單直接的xml配置文件中指明ORM的配置信息,比如JavaBean類對應的表名和屬性對應的列名。你可以通過框架中的工具(如:Hibernate中的Session類)來對那些POJO進行操作(如:存儲、獲取和查找)。
EJB 3.0是建立在Hibernate和TopLink的思想和成功之上。它爲Java EE提供了一個標準的POJO ORM框架。另外,EJB 3.0有兩個超越現今所有持久化解決方案的關鍵革新:
●沒有使用XML文件來指明ORM配置信息, EJB 3.0允許開發者直接在POJO代碼中注釋出映射信息。舉例來說,你可以用注釋來指明每個JavaBean屬性對應的關系型表列。在這篇文章的後面,你將看到更多的例子。注釋使得映射更直接,更容易維護了。
●EJB 3.0爲實體bean定義了一個新的歸檔格式。每個檔案使用一組獨立的,爲後端數據庫和ORM行爲所專用的配置集來定義一個持久化上下文。在這篇文章的後面,我會討論持久化上下文。
現在,讓我們通過幾個簡單的例子來看一下EJB 3.0是如何完成POJO ORM的。
映射一個簡單的對象
在EJB 3.0中,每個實體bean都是一個簡單的JavaBean式的類。爲了告訴EJB 3.0容器這個類應該爲持久化進行映射,你應該用@Entity來注釋這個類。
每一個實體bean類映射到一個關系型數據庫表。默認地,表名對應類名。你可以用@Table來爲類指定另一個表。每一個JavaBean屬性映射到表的列上,同樣的,默認列名就是屬性名。你可以用@Column注釋在屬性的Setter方法上來改變這種默認關系。下面是一個EJB 3.0的簡單例子:
@Entity
// @Table (name="AlternativeTableName")
public class Person implements Serializable {
protected int id;
protected String name;
protected Date dateOfBirth;
public void setId (int id) {
this.id = id;
}
@Id(generate = GeneratorType.AUTO)
public int getId () {
return id;
}
public void setName (String name) {
this.name = name;
}
// @Column (name="AlternativeColumnName")
public String getName () {
return name;
}
public void setDateOfBirth (Date dateOfBirth) {
this.dateOfBirth = dateOfBirth;
}
public Date getDateOfBirth () {
return dateOfBirth;
}
}
當容器把Person類映射到Person SQL數據庫表以後,每一個Person實例就是表中的一條數據記錄。
映射一個簡單的JavaBean類是容易的。但自動ORM框架真正閃光之處在於映射互相關聯的對象。下一節中,我們看一下EJB 3.0如何操作對象間的關系。
關系
在一個數據模型裏面,一般來說類相互之間都會有某種聯系。比如,一個Person(個人)對象可以和一個Resume(確認)對象相關聯,反過來也一樣(一對一關系);一個Person對象可以和多個CreditCard(信用卡)對象相關,而一個CreditCard對象只能和一個Person對象相關(一對多關系)。多個Person對象可以和一個Address(地址)對象相關,而一個Person對象只能對應一個Address對象(多對一關系)。(譯者注:此處原著筆誤, Person與Address位置顛倒了;編者注:我看兩者是多對多的關系。一家人住在同一個地方,這個地址對於這一家人來說是一對多的關系;房主在別的地方又買了一套房,房主與地址的關系是一對多的關系。)
在一個數據模型中,對象指針用來操作那些關系。舉例來說,一個Person對象可以有一個屬性(也就是域)指向一個Resume對象。而另一個屬性是CreditCard對象的集合。爲了告知EJB 3.0容器對象間的關系,你只需簡單地在POJO中注釋JavaBean屬性。
@Entity
public class Person implements Serializable {
// ... ...
protected Resume resume;
protected CreditCard [] cards;
protected Address addr;
// ... ...
@OneToOne
public Resume getResume () {
return resume;
}
// ... ...
@ManyToOne
// @JoinColumn (name="MyCustomId")
public Address getAddr () {
return addr;
}
// ... ...
@OneToMany
public Collection <CreditCard> getCards () {
return cards;
}
}
在關系型數據庫中,那些關系自動地被EJB 3.0容器使用外鍵來重建了。舉例來說,Person表有一個外鍵包含了Resume表中相應的主鍵。運行時,EJB 3.0容器加強了一對一的關系:它保證了Resume鍵值對於Person表中的每一行是唯一的。爲了啓用Resume表到Person表的雙向查詢,你可以Resume表中定義一個Person屬性,並把它也加上@OneToOne注釋。
Person表中也有一個外鍵包含了Address表中相應行的主鍵。這種情況下,相同的Address主鍵可以出現在多個Person行中,這是多對一關系。對於一對多的關系,映射稍有一點複雜,因爲外鍵列是定義在多對一表中的。於是,在CreditCard類中,你必須用@ManyToOne來定義一個Person屬性。
改變外部鍵字段名
ORM中使用的外部鍵字段的名字是由容器自動決定的或者由@JoinColumn注釋來顯式的指定。
上面討論的關系只是實體bean之間關系的一種類型,實體類之間另外一種重要關系是繼承。
繼承
面向對象設計方法的一個關鍵概念是繼承。使用繼承,你可以創建一個複雜的對象樹而不需要重復的代碼。舉例來說,Consultant(顧問)是提供有償服務的一個人,那麼在我們的數據模型中,Consultant類就從Person(個人)類中繼承,並增加了一個價格屬性。不幸的是,當今的關系型數據庫並不存在繼承的概念。ORM框架主要通過以下兩個手段來模仿這種行爲:
●框架可以爲每一個類生成一個單獨的表。子類的表重復了那些從父類的字段。子類和父類都存儲爲各自對應的表。
●框架可以使用包含了所有子類屬性的表。兩種類(父類和子類)的實例都存儲於同一張表—父類中不存在的字段(也就是,子類的字段)取null值。爲了使繼承映射更爲強壯,表也可以有一個“區別”列,它存儲的標記表明該行數據映射到哪一個類。
EJB 3.0實體bean支持上述兩種映射策略,默認是單表映射策略。你可以簡單地用注釋指明子類的繼承策略和區別字段的名字。下面是Consultant類的例子,它從Person類中繼承:
@Entity
@Inheritance(discriminatorValue="C")
@DiscriminatorColumn(name="person_type")
public class Consultant extends Person {
protected double rate;
public void setRate (double rate) {
this.rate = rate;
}
public double getRate () {
return rate;
}
}
從上面的例子中,容器使用默認策略將Consultant類映射到Person類對應的同一張表中。如果表中的person_type字段的值爲C,那麼那一行數據就代表了一個顧問類。否則,當前行代表的是一個普通的Person類。
持久化檔案
現在你的數據模型有了一組使用了注釋EJB 3.0實體bean的類,你可以將它們捆綁在一起佈署到服務器環境中。EJB 3.0爲實體bean定義了一種特殊的歸檔格式,叫做持久化檔案(文件後綴名爲.par)。
一個.par文件是一組實體bean類文件加上一個簡單的配置文件META-INF/persistence.xml的jar打包文件。persistence.xml文件定義了持久化上下文,它告知EJB 3.0哪一個後端數據庫(數據源)與這一組實體bean相對應。persistence.xml也包含了配置屬性的細節。舉例來說,JBoss EJB 3.0是在Hibernate 3.0之上實現的,於是你可以通過persistence.xml傳遞任意的Hibernate配置選項。這有一個範例persistence.xml文件,它包含了JBoss和Hibernate專用的配置屬性,包括SQL 方言(dialect)和二級緩存。
<entity-manager>
<name>cal</name>
<jta-data-source>java:/DefaultDS</jta-data-source>
<properties>
<property name="hibernate.dialect"
value="org.hibernate.dialect.MySQLDialect" />
<property name="hibernate.cache.provider_class"
value="org.jboss.ejb3.entity.TreeCacheProviderHook"/>
<property name="hibernate.treecache.mbean.object_name"
value="jboss.cache:service=EJB3EntityTreeCache"/>
</properties>
</entity-manager>
實體管理器
一旦你部署了實體bean, 你必須通過EJB 3.0的實體管理器的API來訪問和操作它們。EJB 3.0容器爲每個部署的持久化上下文(也就是,.par文件)提供了一個實體管理器對象。從一個EJB 3.0 session bean POJO(參看第一部分)中 ,你可以通過@PersistenceContext注釋將實體管理器對象注入,並傳入上下文的名字。
@Stateless
public class ManagerBean implements Manager {
@PersistenceContext (unitName="cal")
protected EntityManager em;
// Use "em"
// 使用“em”
// ... ...
}
基本操作
要創建一個數據對象並把它存入數據庫中,你只需簡單地使用Java的new關鍵字來創建POJO,並把它傳給EntityManager.persist()方法。
Person p = new Person ();
p.setName ("A new baby");
p.setDateOfBirth (new Date ());
em.persist (p);
要從數據庫中取得對象,你可以使用EJB 3.0查詢語言來搜索數據庫。下面的例子演示瞭如何將Person表中的所有行作爲Person Java對象的集合來返回。
// 得到所有人的對象
Collection <Person> persons = (Collection <Person>)
em.createQuery("from Person p").getResultList();
可管理的POJO
通過實體管理器保存和獲取的對象是被管理在持久化上下文中的。這意味着如果對象後來被改變了,那這種改變將會被自動檢測並持久化到數據庫中。在下面的例子中,我們更新了一個可管理的POJO的一個屬性。這個改變會被EJB 3.0容 器自動檢測到並發送給了數據。
Person p = em.find(Person.class, personId);
p.setName ("Another Name");
//p會在當前事務結束時被自動地更新到數據庫中去。
// 並沒用更多的API調用
既然EJB 3.0實體僅只是POJO,那麼它們就可以能夠被序列化並通過網絡傳遞。如果一個對象不是被容器創建的(也就是說,它是從網絡連接中傳遞過來的或者是某一個遠程調用返回的結果),那麼持久化上下文並不會管理它。不過,你可以通過調用EntityManager.merge()方法將一個非管理的POJO合併到持久化上下文中。下面是將一個解序列化的POJO合併入當前持久化上下文中的例子。
InputStream in;
// 初始化輸入流
Person p = Util.deserialize (in);
// ... ...
em.merge (p);
// p現在是一個可管理的對象了。p的任何改變將會被自動檢測並持久化
p.setName ("Another Name");
數據庫同步
當實體管理器對象在一個session bean中使用時,它是和服務器的事務上下文綁定的。實體管理器在服務器的事務提交時提交併且同步它的內容。在一個session bean中,服務器的事務默認地會在調用堆棧的最後提交。當然,你也可以通過注釋來爲每個商務方法指定具體的事務屬性。下面的例子展示瞭如何爲一個session bean的方法聲明一個新的事務。
@TransactionAttribute(TransactionAttributeType.REQUIRESNEW)
public void update () {
// 這個方法更新Person對象
// 更新將會在這個方法的末尾被提交和刷新到數據庫中
批處理中刷新數據庫操作
爲了只在當事務提交時纔將改變更新到數據庫中,容器將所有數據庫操作集中到一個批處理中,這樣就減少了代價昂貴的與數據庫的交互。
如果你需要在事務提交之前將更新刷新到數據庫中,你可以直接地調用EntityManager.flush()方法。或者你可以將一個方法注釋爲@FlushMode(FlushModeType.NEVER),於是事務管理器將不會在方法的結尾(也就是事務的結尾)處刷新更新到數據庫中。這種情況下,你可以手工地來刷新數據庫以獲得對數據庫操作的最大控制。
總結
EJB 3.0 提供了一種簡單有效的框架將Java POJO映射到SQL數據庫中的關系型表中。它基於Java類中的名字和結構進行智能的默認映射策略。但你也可以用一組簡單的注釋重載所有的默認值,來處理複雜的對象關系。
EJB 3.0實體管理器提供了簡單的API來持久化和從數據庫中查找對象。每一個實體管理器對象與一組映射的POJO相關聯,並有着它自己的數據庫設置。它會自動地捆綁到服務器的事務管理器中。