Google App Engine的Java持久性與數(shù)據(jù)存儲(chǔ)
本文繼續(xù)介紹Google App Engine for Java。這篇講述持久性和關(guān)系。
Google App Engine for Java 力求為可伸縮的 Web 應(yīng)用程序成功地編寫一個(gè)持久層,可這個(gè)目標(biāo)的達(dá)成情況又如何呢?在本文中,我將概述 App Engine for Java 的持久性框架,從而結(jié)束本系列文章。該框架以 Java Data Objects(JDO)和 Java Persistence API(JPA)為基礎(chǔ)。盡管在剛剛出現(xiàn)時(shí)前景良好,但是 App Engine 的基于 Java 的持久性目前存在一些嚴(yán)重的缺陷,我將對(duì)此進(jìn)行解釋和演示。您將學(xué)習(xí) App Engine for Java 持久性是如何運(yùn)作以及有著哪些挑戰(zhàn),還將學(xué)習(xí)在使用面向 Java 開發(fā)人員的 Google 云平臺(tái)時(shí),您具有哪些持久性選擇。
在閱讀本文并遍覽這些示例時(shí),您要牢記這樣的事實(shí):現(xiàn)在的 App Engine for Java 是一個(gè)預(yù)覽 版。基于 Java 的持久性目前也許并不是您所希望或者需要的全部,可能并且應(yīng)該會(huì)在未來發(fā)生變化?,F(xiàn)如今,使用 App Engine for Java 進(jìn)行可伸縮的、數(shù)據(jù)密集型的 Java 應(yīng)用程序開發(fā)不合適膽小者或者保守派,這就是我在撰寫本文時(shí)所學(xué)到的。這更像跳入了游泳池的最深處:看不到任何救生員,項(xiàng)目要沉下去還是往前游,取決于您自己。
注意,本文中的示例應(yīng)用程序以 第 2 部分 中開發(fā)的聯(lián)系人管理應(yīng)用程序?yàn)榛A(chǔ)。您需要構(gòu)建該應(yīng)用程序,確保它是可運(yùn)行的,這樣才能繼續(xù)學(xué)習(xí)本文的示例。
基礎(chǔ)知識(shí)和抽象泄漏(leaky abstraction)
與原始的 Google App Engine 一樣,App Engine for Java 依靠 Google 的內(nèi)部基礎(chǔ)設(shè)施,實(shí)現(xiàn)可伸縮的應(yīng)用程序開發(fā)的 Big Three:分布、復(fù)制和負(fù)載均衡。由于使用的是 Google 基礎(chǔ)設(shè)施,因此所有神奇的地方大都發(fā)生在后臺(tái),可以通過 App Engine for Java 的基于標(biāo)準(zhǔn)的 API 獲得。數(shù)據(jù)存儲(chǔ)接口是以 JDO 和 JPA 為基礎(chǔ)的,而它們自身又是以開源的 DataNucleus 項(xiàng)目為基礎(chǔ)。AppEngine for Java 還提供了一個(gè)低級(jí)別的適配器 API,用來直接處理基于 Google 的 BigTable 實(shí)現(xiàn)的 App Engine for Java 數(shù)據(jù)存儲(chǔ)(要了解更多有關(guān) BigTable 的信息,請(qǐng)參見 第 1 部分)。
然而,App Engine for Java 數(shù)據(jù)持久性并不像純 Google App Engine 中的持久性那樣簡(jiǎn)單。由于 BigTable 不是一個(gè)關(guān)系數(shù)據(jù)庫,JDO 和 JPA 的接口出現(xiàn)了一些抽象泄漏。例如,在 App Engine for Java 中,您無法進(jìn)行那些執(zhí)行連接的查詢。您可以在 JPA 和 JDO 間設(shè)置關(guān)系,但它們只能用來持久化關(guān)系。并且在持久化對(duì)象時(shí),如果它們?cè)谙嗤膶?shí)體群中,那么它們只能被持久化到相同的原子事務(wù)中。根據(jù)慣例,具有所有權(quán)的關(guān)系位于與父類相同的實(shí)體群中。相反,不具有所有權(quán)的關(guān)系可以在不同的實(shí)體群中。
重新考慮數(shù)據(jù)規(guī)范化
要使用 App Engine 的可伸縮的數(shù)據(jù)存儲(chǔ),需要重新考慮有關(guān)規(guī)范化數(shù)據(jù)的優(yōu)點(diǎn)的教導(dǎo)。當(dāng)然,如果您在真實(shí)的環(huán)境中工作了足夠長(zhǎng)的時(shí)間,那么,您可能已經(jīng)為了追求性能而犧牲過規(guī)范化了。區(qū)別在于,在處理 App Engine 數(shù)據(jù)存儲(chǔ)時(shí),您必須盡早且經(jīng)常進(jìn)行反規(guī)范化。反規(guī)范化 不再是一個(gè)忌諱的字眼,相反,它是一個(gè)設(shè)計(jì)工具,您可以把它應(yīng)用在 App Engine for Java 應(yīng)用程序的許多方面。
當(dāng)您嘗試把為 RDBMS 編寫的應(yīng)用程序移植到 App Engine for Java 時(shí),App Engine for Java 的持久性泄漏的主要缺陷就會(huì)顯露出來。App Engine for Java 數(shù)據(jù)存儲(chǔ)并不是關(guān)系數(shù)據(jù)庫的臨時(shí)替代物,因此,要把您對(duì) App Engine for Java 所做的工作移植到 RDBMS 端口并不容易。采用現(xiàn)有的模式并把它移植到數(shù)據(jù)存儲(chǔ)中,這種場(chǎng)景則更為少見。如果您決定把一個(gè)遺留的 Java 企業(yè)應(yīng)用程序移植到 App 引擎中,建議您要小心謹(jǐn)慎,并進(jìn)行備份分析。Google App Engine 是一個(gè)針對(duì)專門為它設(shè)計(jì)的應(yīng)用程序的平臺(tái)。Google App Engine for Java 支持 JDO 和 JPA,這使得這些應(yīng)用程序能夠被移植回更傳統(tǒng)的、未進(jìn)行規(guī)范化的企業(yè)應(yīng)用程序。
關(guān)系的問題
App Engine for Java 目前的預(yù)覽版的另外一個(gè)缺點(diǎn)是它對(duì)關(guān)系的處理。為了創(chuàng)建關(guān)系,現(xiàn)在您必須對(duì) JDO 使用 App Engine for Java 特有的擴(kuò)展。假設(shè)鍵是在 BigTable 的工件的基礎(chǔ)上生成 — 也就是說,“主鍵” 將父對(duì)象鍵編碼到其所有子鍵中 — 您將不得不在一個(gè)非關(guān)系數(shù)據(jù)庫中管理數(shù)據(jù)。另外一個(gè)限制是持久化數(shù)據(jù)。如果您使用非標(biāo)準(zhǔn)的 AppEngine for Java Key
類,事情將會(huì)變得復(fù)雜。首先,把模型移植到 RDBMS 時(shí),如何使用非標(biāo)準(zhǔn) Key
? 其次,由于無法使用 GWT 引擎轉(zhuǎn)換 Key
類,因此,任何使用這個(gè)類的模型對(duì)象都無法被作為 GWT 應(yīng)用程序的一部分進(jìn)行使用。
當(dāng)然,撰寫這篇文章時(shí),Google App Engine for Java 還是純粹的預(yù)覽模式,沒有到發(fā)布的最佳時(shí)間。學(xué)習(xí) JDO 中的關(guān)系文檔(很少,而且包含一些不完整的示例)時(shí),這點(diǎn)就變得顯而易見了。
App Engine for Java 開發(fā)包提供了一系列的示例程序。許多示例都使用 JDO,沒有一個(gè)使用 JPA。這些示例中沒有一個(gè)示例(包括一個(gè)名為 jdoexamples 的示例)演示了關(guān)系,即使是簡(jiǎn)單的關(guān)系。相反,所有的示例都只使用一個(gè)對(duì)象把數(shù)據(jù)保存到數(shù)據(jù)存儲(chǔ)中。Google App Engine for Java 討論組 充斥著有關(guān)如何使簡(jiǎn)單關(guān)系起作用的問題,但卻鮮有答案。很顯然,有些開發(fā)人員有辦法使其起作用,但是實(shí)現(xiàn)起來都很困難,而且遇到了一些復(fù)雜情況。
App Engine for Java 中的關(guān)系的底線是,無需從 JDO 或 JPA 獲得大量支持就能夠管理它們。 Google 的 BigTable 是一種已經(jīng)經(jīng)過檢驗(yàn)的技術(shù),可用來生成可伸縮的應(yīng)用程序,然而,您還可以在此基礎(chǔ)上進(jìn)行構(gòu)建。在 BigTable 上進(jìn)行構(gòu)建,您就不必處理還不完善的 API 層面。另一方面,您只要處理一個(gè)較低級(jí)別的 API。
#p#
App Engine for Java 中的 Java Data Objects
把傳統(tǒng)的 Java 應(yīng)用程序移植到 App Engine for Java 中,甚至是給出關(guān)系挑戰(zhàn),這些可能都沒有什么意義,然而,持久性場(chǎng)景還是存在的,這時(shí)使用這個(gè)平臺(tái)就有意義了。我將使用一個(gè)可行的示例來結(jié)束本文,您將體驗(yàn) App Engine for Java 持久性是如何工作的。我們將以 第 2 部分 中建立的聯(lián)系人管理應(yīng)用程序?yàn)榛A(chǔ),介紹如何添加支持,以使用 App Engine for Java 數(shù)據(jù)存儲(chǔ)工具持久化 Contact
對(duì)象。
在前面的文章中,您創(chuàng)建了一個(gè)簡(jiǎn)單的 GWT GUI,對(duì) Contact
對(duì)象進(jìn)行 CRUD 操作。您定義了簡(jiǎn)單的接口,如清單 1 所示:
package gaej.example.contact.server; import java.util.List; import gaej.example.contact.client.Contact; public interface ContactDAO { void addContact(Contact contact); void removeContact(Contact contact); void updateContact(Contact contact); List<Contact> listContacts(); } |
接下來,創(chuàng)建一個(gè)模擬版本,與內(nèi)存集合中的數(shù)據(jù)進(jìn)行交互,如清單 2 所示:
清單 2. 模擬 DAO 的 ContactDAOMockpackage gaej.example.contact.server; import gaej.example.contact.client.Contact; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; public class ContactDAOMock implements ContactDAO { Map<String, Contact> map = new LinkedHashMap<String, Contact>(); { map.put("rhightower@mammatus.com", new Contact("Rick Hightower", "rhightower@mammatus.com", "520-555-1212")); map.put("scott@mammatus.com", new Contact("Scott Fauerbach", "scott@mammatus.com", "520-555-1213")); map.put("bob@mammatus.com", new Contact("Bob Dean", "bob@mammatus.com", "520-555-1214")); } public void addContact(Contact contact) { String email = contact.getEmail(); map.put(email, contact); } public List<Contact> listContacts() { return Collections.unmodifiableList(new ArrayList<Contact>(map.values())); } public void removeContact(Contact contact) { map.remove(contact.getEmail()); } public void updateContact(Contact contact) { map.put(contact.getEmail(), contact); } } |
現(xiàn)在,使用與 Google App Engine 數(shù)據(jù)存儲(chǔ)交互的應(yīng)用程序替換模擬實(shí)現(xiàn),看看會(huì)發(fā)生什么。在這個(gè)示例中,您將使用 JDO 持久化 Contact
類。使用 Google Eclipse Plugin 編寫的應(yīng)用程序已經(jīng)擁有了使用 JDO 所需的所有庫。它還包含了一個(gè) jdoconfig.xml 文件,因此,一旦對(duì) Contact
類進(jìn)行了注釋,您就已經(jīng)準(zhǔn)備好開始使用 JDO。
清單 3 顯示擴(kuò)展后的 ContactDAO
接口,可使用 JDO API 進(jìn)行持久化、查詢、更新和刪除對(duì)象:
package gaej.example.contact.server; import gaej.example.contact.client.Contact; import java.util.List; import javax.jdo.JDOHelper; import javax.jdo.PersistenceManager; import javax.jdo.PersistenceManagerFactory; public class ContactJdoDAO implements ContactDAO { private static final PersistenceManagerFactory pmfInstance = JDOHelper .getPersistenceManagerFactory("transactions-optional"); public static PersistenceManagerFactory getPersistenceManagerFactory() { return pmfInstance; } public void addContact(Contact contact) { PersistenceManager pm = getPersistenceManagerFactory() .getPersistenceManager(); try { pm.makePersistent(contact); } finally { pm.close(); } } @SuppressWarnings("unchecked") public List<Contact> listContacts() { PersistenceManager pm = getPersistenceManagerFactory() .getPersistenceManager(); String query = "select from " + Contact.class.getName(); return (List<Contact>) pm.newQuery(query).execute(); } public void removeContact(Contact contact) { PersistenceManager pm = getPersistenceManagerFactory() .getPersistenceManager(); try { pm.currentTransaction().begin(); // We don't have a reference to the selected Product. // So we have to look it up first, contact = pm.getObjectById(Contact.class, contact.getId()); pm.deletePersistent(contact); pm.currentTransaction().commit(); } catch (Exception ex) { pm.currentTransaction().rollback(); throw new RuntimeException(ex); } finally { pm.close(); } } public void updateContact(Contact contact) { PersistenceManager pm = getPersistenceManagerFactory() .getPersistenceManager(); String name = contact.getName(); String phone = contact.getPhone(); String email = contact.getEmail(); try { pm.currentTransaction().begin(); // We don't have a reference to the selected Product. // So we have to look it up first, contact = pm.getObjectById(Contact.class, contact.getId()); contact.setName(name); contact.setPhone(phone); contact.setEmail(email); pm.makePersistent(contact); pm.currentTransaction().commit(); } catch (Exception ex) { pm.currentTransaction().rollback(); throw new RuntimeException(ex); } finally { pm.close(); } } } |
#p#
逐一比對(duì)方法
現(xiàn)在,考慮一下使用清單 3 中的每個(gè)方法時(shí)發(fā)生的情況。您將會(huì)發(fā)現(xiàn),方法的名字可能是新的,但它們的動(dòng)作大部分情況下都應(yīng)該感到熟悉。
首先,為了獲取 PersistenceManager
,創(chuàng)建了一個(gè)靜態(tài)的 PersistenceManagerFactory
。如果您以前使用過 JPA,PersistenceManager
與 JPA 中的 EntityManager
很相似。如果您使用過 Hibernate,PersistenceManager
與 Hibernate Session 很相似?;旧希?CODE>PersistenceManager 是 JDO 持久性系統(tǒng)的主接口。它代表了與數(shù)據(jù)庫的會(huì)話。getPersistenceManagerFactory()
方法返回靜態(tài)初始化的 PersistenceManagerFactory
,如清單 4 所示:
private static final PersistenceManagerFactory pmfInstance = JDOHelper .getPersistenceManagerFactory("transactions-optional"); public static PersistenceManagerFactory getPersistenceManagerFactory() { return pmfInstance; } |
addContact()
方法把新的聯(lián)系人添加到數(shù)據(jù)存儲(chǔ)中。為了做到這點(diǎn),需要?jiǎng)?chuàng)建一個(gè) PersistenceManager
實(shí)例,然后,調(diào)用 PersistenceManager
的 makePersistence()
方法。makePersistence()
方法采用臨時(shí)的 Contact
對(duì)象(用戶將在 GWT GUI 中填充),并且使其成為一個(gè)持久的對(duì)象。所有這些如清單 5 所示:
清單 5. addContact()
public void addContact(Contact contact) { PersistenceManager pm = getPersistenceManagerFactory() .getPersistenceManager(); try { pm.makePersistent(contact); } finally { pm.close(); } } |
注意在清單 5 中,persistenceManager
是如何被封入在 finally
塊中。這確保能夠把與 persistenceManager
關(guān)聯(lián)的資源清除干凈。
如清單 6 所示,listContact()
方法從它所查找的 persistenceManager
中創(chuàng)建一個(gè)查詢對(duì)象。它調(diào)用了 execute()
方法,從數(shù)據(jù)存儲(chǔ)中返回 Contact
列表。
清單 6. listContact()
@SuppressWarnings("unchecked") public List<Contact> listContacts() { PersistenceManager pm = getPersistenceManagerFactory() .getPersistenceManager(); String query = "select from " + Contact.class.getName(); return (List<Contact>) pm.newQuery(query).execute(); } |
在從數(shù)據(jù)存儲(chǔ)中刪除聯(lián)系人之前,removeContact()
通過 ID 查找聯(lián)系人,如清單 7 所示。它必須這么做,而不僅僅是把聯(lián)系人直接刪除,這是因?yàn)閬碜?GWT GUI 的 Contact
對(duì) JDO 一無所知。在刪除前,您必須獲得與 PersistenceManager
緩存關(guān)聯(lián)的 Contact
。
清單 7. removeContact()
public void removeContact(Contact contact) { PersistenceManager pm = getPersistenceManagerFactory() .getPersistenceManager(); try { pm.currentTransaction().begin(); // We don't have a reference to the selected Product. // So we have to look it up first, contact = pm.getObjectById(Contact.class, contact.getId()); pm.deletePersistent(contact); pm.currentTransaction().commit(); } catch (Exception ex) { pm.currentTransaction().rollback(); throw new RuntimeException(ex); } finally { pm.close(); } } |
清單 8 中的 updateContact()
方法與 removeContact()
方法類似,用來查找 Contact
。然后,updateContact()
方法從 Contact
中復(fù)制屬性。這些屬性被當(dāng)作實(shí)參(Argument)傳送到 Contact
,后者由持久性管理器查找。使用 PersistenceManager
檢查所查找的對(duì)象發(fā)生的變化。如果對(duì)象發(fā)生了變化,當(dāng)事務(wù)進(jìn)行提交時(shí),這些變化會(huì)被 PersistenceManager
刷新到數(shù)據(jù)庫。
清單 8. updateContact()
public void updateContact(Contact contact) { PersistenceManager pm = getPersistenceManagerFactory() .getPersistenceManager(); String name = contact.getName(); String phone = contact.getPhone(); String email = contact.getEmail(); try { pm.currentTransaction().begin(); // We don't have a reference to the selected Product. // So we have to look it up first, contact = pm.getObjectById(Contact.class, contact.getId()); contact.setName(name); contact.setPhone(phone); contact.setEmail(email); pm.makePersistent(contact); pm.currentTransaction().commit(); } catch (Exception ex) { pm.currentTransaction().rollback(); throw new RuntimeException(ex); } finally { pm.close(); } } |
#p#
對(duì)象持久性注釋
為了使 Contact
能夠具有持久性,必須把它識(shí)別為一個(gè)具有 @PersistenceCapable
注釋的可持久性對(duì)象。然后,需要對(duì)它所有的可持久性字段進(jìn)行注釋,如清單 9 所示:
清單 9. Contact 具有可持久性
package gaej.example.contact.client; import java.io.Serializable; import javax.jdo.annotations.IdGeneratorStrategy; import javax.jdo.annotations.IdentityType; import javax.jdo.annotations.PersistenceCapable; import javax.jdo.annotations.Persistent; import javax.jdo.annotations.PrimaryKey; @PersistenceCapable(identityType = IdentityType.APPLICATION) public class Contact implements Serializable { private static final long serialVersionUID = 1L; @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Long id; @Persistent private String name; @Persistent private String email; @Persistent private String phone; public Contact() { } public Contact(String name, String email, String phone) { super(); this.name = name; this.email = email; this.phone = phone; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } } |
通過面向?qū)ο蟮木幊毯徒涌谠O(shè)計(jì)原則,您只需使用新的 ContactJdoDAO
替代原始的 ContactDAOMock
。然后 GWT GUI 無需任何修改就可處理 JDO。
最后,在這種替換中,真正改變 的是 DAO 在服務(wù)中被實(shí)例化的方式。如清單 10 所示:
清單 10. RemoteServiceServlet
public class ContactServiceImpl extends RemoteServiceServlet implements ContactService { private static final long serialVersionUID = 1L; //private ContactDAO contactDAO = new ContactDAOMock(); private ContactDAO contactDAO = new ContactJdoDAO(); ... |
結(jié)束語
在這篇由三部分組成的文章中,介紹了 Google App Engine for Java 目前為持久性提供的支持,這是交付可伸縮應(yīng)用程序的基礎(chǔ)??偟慕Y(jié)論令人失望,但是要注意這是一個(gè)正在發(fā)展中的平臺(tái)。為 App Engine for Java 預(yù)覽版編寫的應(yīng)用程序被連接到 App Engine 的持久性基礎(chǔ)設(shè)施,即使是用 JDO 或 JPA 編寫。App Engine for Java 預(yù)覽版幾乎沒有為它的持久性框架提供任何文檔,而且 App Engine for Java 提供的示例幾乎無法演示即使是最簡(jiǎn)單的關(guān)系。
即使 JDO 和 JPA 實(shí)現(xiàn)已經(jīng)完全成熟,目前您仍然不可能編寫一個(gè) App Engine for Java 應(yīng)用程序并輕松地把它移植到一個(gè)基于 RDBMS 的企業(yè)應(yīng)用程序。要使移植能夠起作用,至少要編寫大量的代碼。
我希望持久性能隨著時(shí)間的推移而成熟起來。如果現(xiàn)在必須使用 App Engine for Java,您可能需要繞過 Java API,直接編寫低級(jí)別的 Datastore API。使用 App Engine for Java 平臺(tái)是可能的,但是,如果習(xí)慣了使用 JPA 和/或 JDO,那么將出現(xiàn)一條學(xué)習(xí)曲線,因?yàn)榇嬖诒疚那懊婷枋龅某橄笮孤?,并且目前的功能要么還無法正常運(yùn)行,要么還沒有進(jìn)行很好的文檔記錄。