Java開發2.0: 使用Amazon SimpleDB實現雲存儲,第2部分:使用SimpleJPA實現簡單對象持久化
使用諸如 Grails 的關系框架對幾乎所有類型的應用進行域對象建模是很容易的,但是使用 SimpleDB 又怎麼樣呢?在 Andrew Glover 的介紹 SimpleDB 的系列文章的第 2 部分,他向您介紹了如何使用 SimpleJPA,而非 Amazon SDK,在 SimpleDB 的雲存儲中實現對象持久化。除了使您能夠使用簡單 Java™ 對象進行域建模(通過 JPA)之外,SimpleJPA 還能夠自動地將基本數據類型轉換成兼容 Amazon 的字符串。您確實找不到比這更簡單的雲存儲方法了。
在介紹 SimpleDB 文章的 第一部分 中,我向您介紹了如何使用 Amazon 本身的 API 進行一個 CRUD 網絡的賽跑應用的建模。除了對大多數 Java 開發人員而言,Amazon 只使用字符串來描述數據類型的方法的明顯獨特性之外,您可能發現自己對於 Amazon API 還有一些疑慮。畢竟,現在使用關系數據庫的 API 已經非常標准且成熟了 — 而且更重要的是,他們已經很熟悉這些技術了。
除此之外,現在有許多關系框架實現了 Java Persistence API。因此為各種 RDBMS 進行各種類型的 Java 應用進行域對象建模都是非常容易和常見的。當您已經掌握了一種方法之後,很自然您會對於學習新的域對象建模方法會有一些抵觸 — 而好消息是使用 SimpleDB 時,您不需要學習新東西。
在 SimpleDB 文章的第 2 部分中,我將向您介紹如何重構第 1 部分的賽跑應用,使之符合 JPA 規范。然後我們將把應用移植到 SimpleJPA,並且探討一些能夠使這個創新的開放源碼平台經過調整而支持 NoSQL 域建模和基於雲的存儲的方法,這一樣很簡單。
為什麼使用 SimpleDB?
Amazon 的 SimpleDB 是一個簡單且極具可擴展性和可靠性的基於雲的數據存儲方法。由於它本質上是非關系/NoSQL,SimpleDB 既靈活又快速。作為 Amazon Web Service 家族的一部分,SimpleDB 使用 HTTP 作為底層通信機制,所以它能夠支持多種語言,包括 Java 語言、Ruby、C# 和 Perl。SimpleDB 價格也很便宜:根據 SimpleDB 的授權方式,您只需要為您使用的資源支付費用,這跟根據預計使用和空間預先購買授權的傳統方法很不一樣。作為新興的 NoSQL,或非關系數據存儲的一部分,SimpleDB 是與 Google 的 Bigtable 或 CouchDB 相對應的,它們在 這些系列文章中 有相應的介紹。
Hibernate 和 JPA:背景概況
現在有許許多多的 Java 開發人員都使用 Hibernate(和 Spring)實現數據持久化。除了是最先成功的開放源碼項目,Hibernate 也徹底改變了 ORM 領域。在出現 Hibernate 之前,Java 開發人員必須處理復雜的 EJB 實體 Bean;而在這之前,我們只能自己實現 ORM 或者使用來自諸如 IBM® 等供應商的產品。Hibernate 去掉了 EJB 的所有復雜性和開銷,轉而使用我們現在許多人都使用的基於 POJO 的建模平台。
Java Persistence API (JPA) 是由於 Hibernate 創新地使用 POJO 進行數據建模方法的流行而出現的。現在,EJB 3.0 實現了 JPA,Google App Engine 也一樣實現了 JPA。甚至如果您使用 Hibernate EntityManager,那麼 Hibernate 本身也是一個 JPA 實現,
既然 Java 開發人員已經越來越熟悉使用 POJO 對以數據為中心的應用進行建模,那麼可以說,SimpleDB 這樣一個數據存儲應該能夠給我們提供一個類似的選項。畢竟,它與數據庫有些相似,不是嗎?
用對象進行數據建模
要使用 SimpleJPA,我們需要修改一下我們的 Racer 和 Runner 對象,使它們符合 JPA 規范。幸好,JPA 基本要素是很簡單的:給平常的 POJO 加上注釋,而 EntityManager 實現會負責完成其他處理 — 不需要 XML。
JPA 所使用的兩個主要的注釋是 @Entity 和 @Id,這兩個注釋分別將一個 POJO 指定為持久化類,同時確定它的標識鍵。為了將我們的賽跑應用轉換為 JPA,我們也將使用另外兩個管理關系的注釋:@OneToMany 和 @ManyToOne。
在本文的第 1 部分中,我已經向您介紹了如何持久化選手和比賽對象了。然而,我沒有使用對象來表示這些實體 — 我只是使用了 Amazon 的原始 API 來存儲這兩個對象的屬性。如果我希望對一個比賽和比賽選手的關系進行建模,那麼我可以編寫如清單 1 所示的代碼:
清單 1. 一個簡單的 Race 對象
public class Race {
private String name;
private String location;
private double distance;
private List<Runner> runners;
//setters and getters left out...
}
在 清單 1 中,我給 Race 對象設置了 4 個屬性,最後一個是一個選手 Collection。接下來,我可以創建一個簡單的 Runner 對象(如清單 2 所示),它包含每位選手的姓名(現在我將盡量保持簡單),與他/她所參加的 Race 實例相關的 SSN。
清單 2. 與 Race 相關的一個簡單的 Runner
public class Runner {
private String name;
private String ssn;
private Race race;
//setters and getters left out...
}
您可以從 清單 1 和 2 看到,我在選手和比賽之間邏輯上建立了一個多對一的關系。在實際情況中,可能多對多關系更准確些(選手一般會參加多個比賽),但是這裡這樣做是為了簡單起見。另外,現在我也忽略構造函數、setter 和 getter。我將在後面向您介紹。
JPA 中的注釋
要使這兩個對象能夠使用 SimpleJPA 並不是很難。首先,我需要通過為每個對象添加 @Entity 注釋來將它們變成可持久化的。我也需要在 Race 對象中使用 @OneToMany,在 Runner 對象中使用 @ManyToOne 來正確定義它們的關系。
@Entity 注釋是在類上標注的,而關系注釋是在 getter 函數上標注的。這些注釋見清單 3 和 4:
清單 3. JPA 注釋的 Race
@Entity
public class Race {
private String name;
private String location;
private double distance;
private List<Runner> runners;
@OneToMany(mappedBy = "race")
public List<Runner> getRunners() {
return runners;
}
//other setters and getters left out...
}
在 清單 3 中,我使用 @OneToMany 注釋來標注 getRunners 函數。我也在實例 Runner 的 race 屬性上定義了一個關系。
在清單 4 中,我用類似的方法注釋了 Runner 對象的 getRace 函數。
清單 4. JPA 注釋的 Runner
@Entity
public class Runner {
private String name;
private String ssn;
private Race race;
@ManyToOne
public Race getRace() {
return race;
}
//other setters and getters left out...
}
大多數數據存儲(關系型或非關系型)都需要某種描述數據唯一性的方法。所以如果我將這兩個對象存儲到數據存儲中,我至少需要給它們添加 ID。在清單 5 中,我添加一個類型為 BigInteger 的 id 屬性到 Race 域對象。在 Runner 對象中我會使用相同的做法。
清單 5. 給 Race 添加一個 ID
@Entity
public class Race {
private String name;
private String location;
private double distance;
private List<Runner> runners;
private BigInteger id;
@Id
public BigInteger getId() {
return id;
}
@OneToMany(mappedBy = "race")
public List<Runner> getRunners() {
return runners;
}
//other setters and getters left out...
}
清單 5 中的 @Id 注釋並沒有關於 ID 是如何管理的信息。這樣程序就會假設由我手動管理這個 ID,而不是使用 EntityManager 來管理。
進入 SimpleJPA
到現在為止,我還沒有進行任何 SimpleDB 的配置。Race 和 Runner 對象都只是使用 JPA 注釋進行標注,從而可以存儲在任何由 JPA 實現所支持的數據存儲中。可選的存儲方式包括 Oracle、DB2、MySQL 和(您可能已經猜到的)SimpleDB。
SimpleJPA 是 Amazon 的 SimpleDB 的開源實現。雖然它並不支持完整的 JPA 規范(例如,您不能聯合 JPA 查詢),但是它支持大量很有用的一部分 JPA 規范。
使用 SimpleJPA 的一個很大的優點是它能夠無縫地處理我在 本文的第 1 部分 中所討論的按字母的問題。SimpleJPA 會對依賴於數字類型的對象進行字符串轉換和後續的填充(如果需要)。在大多數情況中,這意味著您不需要修改您的域模型來使用 String 類型。(其中只有一個例外情況,我將在後面進行討論。)
因為 SimpleJPA 是一個 JPA 實現,您可以很容易在其中使用符合 JPA 的域對象。SimpleJPA 只要求您使用 String ID,這意味著您的 id 屬性必須是 java.lang.String。為了簡化,SimpleJPA 提供了基本的類 IdedTimestampedBase,它負責管理域對象的 ID 屬性,以及日期屬性 created 和 updated。(在底層, SimpleDB 會生成一個唯一的 Id。)
將應用移植到 SimpleJPA
為了使 Race 和 Runner 類兼容 SimpleJPA,我可以擴展 SimpleJPA 便利基礎類,或者將每一個類的 id 屬性從 BigInteger 修改為 String。我選擇了第一種方法,如清單 6 所示:
清單 6. 修改 Race 類為使用 SimpleJPA 的 IdedTimestampedBase 基礎類
@Entity
public class Race extends IdedTimestampedBase{
private String name;
private String location;
private double distance;
private List<Runner> runners;
@OneToMany(mappedBy = "race")
public List<Runner> getRunners() {
return runners;
}
//other setters and getters left out...
}
雖然我不會向您顯示 Runner 中相同的代碼,但是您可以隨時查看這些代碼:擴展 IdedTimestampedBase,並刪除 Runner 的 id 屬性。
修改 Race 和 Runner 的 ID 是使賽跑應用符合 SimpleJPA 規范的第一步。接下來,我需要將基本數據類型(如,double、int 和 float)轉換為諸如 Integer 和 BigDecimal 的對象。
我將從修改 Race 的 distance 屬性開始。我發現(在當前版本的 SimpleJPA 中)使用 BigDecimal 比 Double 更可靠,所以我將 Race 的 distance 修改為 BigDecimal,如清單 7 所示:
清單 7. 將 distance 修改為 BigDecimal
@Entity
public class Race extends IdedTimestampedBase{
private String name;
private String location;
private BigDecimal distance;
private List<Runner> runners;
@OneToMany(mappedBy = "race")
public List<Runner> getRunners() {
return runners;
}
//other setters and getters left out...
}
現在 Runner 和 Race 都已經可以通過 SimpleJPA 實現進行持久化了。
使用 SimpleJPA 操作 SimpleDB
使用 SimpleJPA 來處理您的域對象在 SimpleDB 中的存儲與使用 JPA 實現進行普通的關系數據庫存儲差別不大。即使您從未使用過 JPA 開發應用,那麼對於您來說它也不會有太大的困難。唯一的新東西是要配置 SimpleJPA 的 EntityManagerFactoryImpl,這要求使用您的 Amazon Web Services 證書和您的 SimpleDB 域的前綴名。(另一個方法是在編譯路徑上增加一個包含您的證書的屬性文件。)
在創建一個 SimpleJPA EntityManagerFactoryImpl 實例時使用您指定的前綴名,這樣產生的 SimpleDB 域會由您的前綴,加一根橫槓,再加域對象名稱組成。所以,如果我指定 “b50” 為前綴,而我在 SimpleDB 中創建一個 Race,那麼這個域將會是 “b50-Race”。
一旦您創建一個 SimpleDB EntityManagerFactoryImpl 實例,其他方面就由這個接口完成了。您需要使用一個 EntityManager 實例,這個實例是從 EntityManagerFactoryImpl 獲取的,如清單 8 所示:
清單 8. 獲得一個 EntityManager
Map<String,String> props = new HashMap<String,String>();
props.put("accessKey","...");
props.put("secretKey","..");
EntityManagerFactoryImpl factory =
new EntityManagerFactoryImpl("b50", props);
EntityManager em = factory.createEntityManager();
處理域對象
一旦您擁有了一個 EntityManager 對象,您就可根據需要處理域對象了。例如,我可以像下面一樣創建一個 Race 實例:
清單 9. 創建一個 Race
Race race = new Race();
race.setName("Charlottesville Marathon");
race.setLocation("Charlottesville, VA");
race.setDistance(new BigDecimal(26.2));
em.persist(race);
在 清單 9 中,SimpleJPA 處理了所有 HTTP 請求來在雲中創建 Race。使用 SimpleJPA 意味著我也能夠使用一個 JPA 查詢來查詢比賽,如清單 10 所示。(記住您不能夠聯合這些查詢,但是我仍然可以使用數字進行搜索。)
清單 10. 根據實例查找一個比賽
Query query = em.createQuery("select o from Race o where o.distance = :dist");
query.setParameter("dist", new BigDecimal(26.2));
List<Race> races = query.getResultList();
for(Race race : races){
System.out.println(race);
}
從數字到字符串
例如,SimpleJPA 內部的數字到字符串轉換的方法是非常有用的,如果您在 SimpleJPA 中啟用查詢輸出,那麼您可以看到有哪些查詢發送到 SimpleDB。所提交的查詢如清單 11 所示。注意 distance 是如何編碼的。
清單 11. SimpleJPA 很好地處理數字!
amazonQuery: Domain=b50-Race, query=select * from `b50-Race`
where `distance` = '0922337203685477583419999999999999928946'
自動填充和編碼使開發更加簡單,您不覺得嗎?
SimpleJPA 中的關系
即使 SimpleDB 不允許查詢中進行域聯合,您也仍然可以在域中使用關聯項。正如我在 第 1 部分 中介紹的,您在一個對象中存儲另一個相關對象的鍵,然後在您需要時查詢這個對象。這也正是 SimpleJPA 所做的。例如,之前我向您介紹了如何使用 JPA 注釋將 Runner 鏈接到一個 Race。因此,我可以創建一個 Runner 實例,將現有的 race 添加到這個實例,然後再存儲 Runner 實例,如清單 12 所示:
清單 12. 使用 SimpleJPA 處理關系
Runner runner = new Runner();
runner.setName("Mark Smith");
runner.setSsn("555-55-5555");
runner.setRace(race);
race.addRunner(runner);
em.persist(runner);
em.persist(race); //update the race now that it has a runner
從 清單 12 我們也可以看到,我需要更新 Race 實例,這樣我所添加的 Runner 才會被存儲起來(同時,我給 Race 函數添加了一個 addRunner 函數,它只是直接將一個 Runner 添加到 Runner 內部的 Collection。)
再一次,如果我通過它的實例搜索一個比賽,我也可以得到它的一組選手,如清單 13 所示:
清單 13. 更多有趣的關系操作!
Query query = em.createQuery("select o from Race o where o.distance = :dist");
query.setParameter("dist", new BigDecimal(26.2));
List<Race> races = query.getResultList();
for(Race races : race){
System.out.println(race);
List<Runner> runners = race.getRunners();
for(Runner rnr : runners){
System.out.println(rnr);
}
}
使用 EntityManager 實例使我能夠通過 remove 函數刪除一些實體,如清單 14 所示:
清單 14. 刪除一個類的實例
Query query = em.createQuery("select o from Race o where o.distance = :dist");
query.setParameter("dist", new BigDecimal(26.2));
List<Race> races = query.getResultList();
for(Race races : race){
em.remove(race);
}
當我在 清單 14 中刪除了一個 Race 實例,所關聯的 Runners 並沒有刪除。(當然,我可以使用 JPA 的 EntityListeners 注釋來處理這個問題,這意味著我可以監控刪除事件,在事件發生時刪除 Runner 實例。)
結束語
這篇 SimpleDB 的快速教程向您介紹了如何使用 Amazon Web Services API 和 SimpleJPA 來處理非關系數據存儲的對象。SimpleJPA 實現了一部分的 Java Persistence API 來簡化 SimpleDB 的對象持久化。您已經看到,使用 SimpleJPA 的其中一個好處是它能夠自動地將基本數據類型轉換為 SimpleDB 能識別的字符串對象。SimpleJPA 也能夠自動地處理 SimpleDB 中的非聯合規則,從而簡化它的關系建模。SimpleJPA 擴展的監聽接口也使它能夠實現邏輯數據統一規則,這是您在關系數據庫所希望使用的。
SimpleJPA 的關鍵是它能夠幫助您快速簡單且廉價地實現重要的可擴展性。通過 SimpleJPA,您可以在非關系的、基於雲的存儲環境中利用您在多年的工作中所接觸到的諸如 Hibernate 等框架的知識。