《Java Data Objects》第一章 翻譯稿

Java數據對象技術

(孫賓 譯)

   (本譯文的版權屬作者本人,出於交流的目的,歡迎轉載,但必須註明出處和原作者!如果您對該書感興趣,並希望看到全書的中文譯本,請向O'Reilly中國出版社發郵件表達您的看法和意見:[email protected])

image001.jpg

前言

  Java Data Objects(JDO)是Java平臺的一項重要創新。當開發者們還在普遍使用JDBC訪問數據庫的時候,當來自一些大企業廠商的專家組還在設計並大肆鼓吹EJB與CMP的API的時候,Craig Russell和David Jordan鼓起勇氣去探索另一條路。與其它一些支持者一起,他們尋找到了提供Java平臺上的存儲機制的簡單辦法,這種辦法對程序員來說既自然又方便。本書描述了他們的研究成果:JDO。

  JDO中獨到的核心思想是在儘可能不增加程序員額外工作的情況下提供一個面向Java的數據庫存儲機制。程序員不需要學習SQL,也不需要很麻煩地通過JDBC調用將數據從Java對象中複製到數據庫或從中讀出,他們只需要按自然的想法使用Java類、屬性和對象之間的引用,而不用寫與之無關的大量代碼,那些代碼會使人頭暈腦脹。甚至對於查詢來說,程序員也可以用Java的布爾表達式(即判斷比較表達式)來代替SQL。換句話說,程序員只用寫Java代碼,而存儲都是自動實現的。

  除了透明存儲之外,按JDO方式寫的代碼具有二進制兼容性,可以跨平臺、跨數據庫。JDO可以用於對象/關係數據庫映射,這種方式中會自動生成JDBC調用來自動地將Java對象和關係數據庫中的數據對應起來。另外,JDO對象也可以直接保存到文件中,達到與對象數據庫一樣的功能和性能。

  針對JDO的辛勤勞動得到了回報:透明存儲的概念受到了廣泛的歡迎。JDO有了自己的社區網站:JDOCentral.com,以及企業級的Java討論區:TheServerSide.com,在這些媒體中,開發人員們都稱讚JDO的簡單和實用。很多開發人員用JDO代替了EntityBean,再在SessionBean中使用JDO數據對象。另一些開發人員將JDO作爲方便的高層面的JDBC代替品,用於JSP頁面和其它一些Java代碼中。在我和Graham Hamilton在1995年定義了JDBC接口之後,JDO也經歷了長時間的發展,它已經很值得合併到J2EE中去。

  說到編寫JDO書籍,我只能想到兩個最合適的人選。Craig是JDO專家組的規範領導者,而Dave是專家組中最活躍的成員之一。他們的資質遠遠超出了這個小組,在此基礎上,JDO被設計得很實用。他們兩人都在程序設計語言和數據存儲有超過十年的研究,包括嚴密的事務語義、多種形式的存儲模型、對象關係、緩衝效率、存儲和非存儲對象的交互、實際開發中的代碼簡便性等等。他們都在ODMG(對象數據管理小組,OMG的子組)作爲核心成員工作了多年。最重要的是,作爲開發人員,兩人都熱愛並迫切需要JDO所提供的功能。
Craig和Dave現在合作了一本精闢、易讀、實用的入門書,我希望大家都象我一樣喜愛它。

Rick Cattell, Deputy Software CTO
Sun Microsystems, February 16, 2003

目錄

(一些名詞可參見目錄後面的術語表)

1. 初步概覽
  定義數據對象模型
        需要存儲的類
    將類聲明爲可存儲的
  項目編譯環境
    JDO參考產品需要的jar文件
    項目目錄結構
    增強類代碼以便存儲
  創建數據庫連接和事務
    獲取一個PersistenceManager
    創建一個FOStore數據庫
  對實例的操作
    保存實例
    訪問實例
    更改實例
    刪除實例
  小結

2. JDO接口概述
  javax.jdo 包
    JDO 相關的異常
  javax.jdo.spi 包(面向JDO產品開發商)
  可選功能
    標識功能
    可選的集合類
    事務相關的可選特性

3. JDO 體系
  JDO應用程序所在Java虛擬機體系
    單PersistenceManager
    多PersistenceManager訪問同一數據庫
    多PersistenceManager訪問不同的數據庫
    共享的JDO產品緩衝
  數據庫訪問
    直接訪問文件系統或本地數據庫
    遠程訪問一個JDO服務器
    遠程訪問一個SQL數據庫
  JDO應用程序的系統體系
    用在本地數據庫上的JDO胖客戶端
    Web服務器中的JDO應用
    作爲Web Service的JDO應用
    連接到採用EJB組件的應用服務器的胖客戶端
    含EJB服務器的Web服務器
    採用SessionBean作爲對外接口的EJB SessionBean
    用JDO來提供"容控存儲-CMP"

4. 定義可存儲類
  類與實例的分類
    類的分類
    實例的分類
  Java類和元數據(Metadata)
    JDO 元數據
    繼承
    Media Mania公司的對象模型
  屬性
    支持的類型
    屬性的存儲
    Media Mania模型的完整的元數據

5. 數據庫映射
  映射方式
  構建基於SQL 99的關係模型
  Java和關係模型中的模型構建
  將類映射到表
  將單值屬性映射到字段
    名稱映射
    類型映射
    索引
  標識
  繼承
  引用
  集合與關係
    採用外鍵
    採用對應表(Join Table)
    一對一關係
    標記列表(List)和映射(Map)

6. 類增強
  增強方式
    參考增強器
    具體的JDO廠商相關的增強
  二進制兼容性
  增強對代碼的影響
  增強器所做的改造
    元數據
    實例級的數據
    屬性調整

7. 創建JDO運行環境
  配置PersistenceManagerFactory
    連接參數
    可選特性的參數
    標誌
    多個實例中的標誌設置
    定義可選標誌和默認設置
    與具體廠商相關的標誌
    不能配置的參數
  獲取PersistenceManager
    用戶對象
    關閉PersistenceManager
    關閉PersistenceManagerFactory
  事務
    事務參數
    事務與數據庫鎖定
    JDO中的事務類型
    獲取一個事務
    設置事務類型
    事務界限
    回滾時的數據恢復
    查看事務是否正在進行
  多PersistenceManager
  多線程

8. 實例管理
  實例存儲
    顯式保存
    可達性存儲
  類擴展的訪問
    訪問一個類擴展
    類擴展的遍歷
    忽略緩衝
  訪問和更新實例
    顯式標明更改過的實例
  刪除實例
    刪除的延伸

9. JDO查詢語言
  查詢組件
  創建和初始化查詢
  緩衝中的變化
  查詢中的命名空間
    類型名
    屬性、參數和變量名
    關鍵字
    標識符
  查詢的執行
    參數聲明
    執行查詢
    預編譯查詢
  查詢的過濾條件
    表達式的一般特點
    查詢中的操作符
    引用
    集合
  查詢結果的排序
  關閉查詢

10. 對象標識
  概述
    JDO中的標識類型
    元數據
    標識類
  數據庫標識
  應用標識
    主鍵屬性類型
    可存儲類的equals()和hashCode()方法
    應用標識類
    單屬性主鍵
    複合主鍵
    帶有外鍵的複合主鍵
    繼承體系中的應用標識
  非持續性標識
  標識相關方法
    獲取標識類
    取得實例的標識
    通過標識取得實例
    更改實例的標識
    取得實例的當前的應用標識
    標識的字符串形式
  高級話題
    選擇標識類型
    採用標識與採用查詢的比較
    跨PersistenceManager的標識

11. 數據對象生命週期中的狀態和轉換
  生命週期的狀態
    Transient (臨時)
    Persistent-New (新保存)
    Hollow (空心態,表示還未讀入數據的對象狀態)
    Persistent-Clean (淨值態,表示與數據庫一致)
    Persistent-Dirty (髒數據狀態,表示已被更改但還未同步到數據庫)
    Persistent-Deleted (已刪)
    Persistent-New-Deleted (新刪除)
  狀態審查
  狀態轉換
    數據庫事務過程中的狀態轉換
    事務完成時的狀態轉換
    事務之間的狀態

12. 屬性管理
  事務相關屬性
  空值
  屬性獲取
    默認存取組
    獲取所有屬性
    屬性的管理
  串行化
  生命週期事件中的屬性管理
  一類和二類對象
    標明一個二類對象
    嵌入集合元素
    作爲二類的可存儲類
    實例共享

13. 緩衝管理
  對緩衝中實例的顯式管理
    刷新實例
    廢除實例
  複製
  支持事務的臨時實例
    支持事務的臨時實例的生命週期狀態
    狀態審查
    狀態轉換
  將已存儲對象臨時化

14. 非事務訪問
  非事務方式的特性
  在事務外讀取
  Persistent-Nontransactional 狀態
  事務提交後保持數據
  事務回滾後恢復數據
    鏡像前
    恢復已存儲實例
    恢復Persistent-New 的實例
  事務外更改已存儲實例
    熱緩衝示例

15. 樂觀事務
  提交時覈實
    從失敗的事務中恢復
    設置樂觀事務方式
    樂觀方式示例
  樂觀方式的狀態轉換
  刪除實例
  使實例事務化
  更改實例
  提交
  回滾

16. Web服務器環境
  Web服務器
    訪問PersistenceManagerFactory
    爲請求服務
    每請求一個PersistenceManager
    每應用一個PersistenceManager
    每個事務請求一個PersistenceManager
    每個Session一個PersistenceManager
    事務處理
  J  SP頁面
  Struts與JDO

17. J2EE應用服務器
  企業級JavaBean(EJB)
  無狀態SessionBean
    配置PersistenceManagerFactory
    容器控制事務的無狀態SessionBean
    容器控制事務的有狀態SessionBean
  組件控制的事務
    javax.transaction.UserTransaction
    javax.jdo.Transaction
    組件控制事務的無狀態SessionBean
    組件控制事務的有狀態SessionBean
  消息組件(Message-Driven Beans)
  保存實體與JDO
    本地存儲
    遠程存儲

附錄

A. 生命週期狀態與轉換

B. JDO 元數據格式 DTD

C. JDO 接口和異常類

D. JDO 查詢語言的BNF語法

E. 示範程序的源代碼

索引

術語表

  1. 持續性(Persistence,也可稱數據存儲)
    表示對數據或對象的存儲。所謂持續性,即表示數據或對象可以獨立於運行環境而保持存在,比如系統崩潰或重啓後,原先保存的數據還在,不會隨程序的結束或系統的重新初始化而丟失。一般來說,數據庫就是典型的一種持續性處理機制。
  2. 容控存儲(Container Managed Persistence)與自控存儲(Bean Managed Persistence)
    是企業級Java組件(Enterprise JavaBean,即EJB)技術中的實體Bean(Entity Bean)的兩種種實現方式,由EJB容器(即J2EE服務器)來控制對數據源的訪問的方式稱作容控存儲(簡稱CMP),而由實體Bean自己的代碼來實現對數據源訪問的方式稱作自控存儲(簡稱BMP)。詳見EJB技術文檔。
  3. 受控環境(Managed Environment)
    表示按J2EE規範的標準,由不同的中間件產品搭建起來的環境,各個應用組件之間通過JNDI(Java命名樹接口,類似Windows的註冊表)進行互相調用。這樣的環境稱作受控環境
  4. JDO產品(JDO Implementation,或簡稱產品)
    是對JDO規範進行實現的一些中間件,由中間件廠商根據JDO規範進行設計並實現。
  5. PersistenceManager(存儲管理器)
    JDO技術中的最常用的API接口類,表示負責完成對象的存儲、獲取和創建查詢器的對象類。
    PersistenceManager的身份相當於JDBC中的Connection數據庫連接(實際上也是有聯繫的)
  6. PersistenceManagerFactory(存儲管理器工廠)
    這個是PersistenceManager的獲取途徑,一個PersistenceManager必須從一個PersistenceManagerFactory獲得。在身份和地位上類似於JDBC中的javax.sql.DataSource。
  7. 數據庫(Datastore)
    表示可以爲JDO提供底層存儲的系統或機制,本來譯爲"數據倉"會更爲準確,但考慮到讀者的接受程度,還是採用"數據庫"更容易接受,只是在閱讀本文的時候,請注意不要與關係數據庫混爲一談,本文中提到的數據庫(Datastore),包括文件存儲機制、關係數據庫、對象數據庫、網絡存儲、LDAP等等可以提供持續性存儲管理的系統
  8. 元數據(Metadata)
    也可以稱作描述符(尤其是在EJB體系中),用於描述Java類如何映射到具體的數據庫中的描述性的XML結構化數據,裏面標明哪些類需要存儲,以及如何存儲的細節。
    在Java以及相關的中間件中,類似的描述性質的數據都稱作元數據,比如在JDBC中描述查詢結果集的ResultSet元數據,就是描述返回的結果記錄集的字段結構、大小等信息的。
  9. 對可存儲類的增強(Enhance)
    爲了讓需要存儲的類與實際的底層數據源掛鉤,需要對編譯生成的.class文件進行二進制代碼的改造,加入與JDO產品的底層環境打交道的代碼。這個將.class文件改造的過程一般用一個工具完成,這個過程稱作增強,而這個工具一般由JDO產品廠商提供,稱作增強器。
  10. 迭代(iterate)
    也稱反覆或遍歷,表示對一個對象集中的元素按特定的順序依次進行訪問。這是java的Collection架構的核心概念之一。

第一章 初步概覽

  作爲異軍突起的新型語言,Java定義了一個標準的運行環境,用戶定義的類在其中得到執行。這些用戶自定義類的實例代表了真實環境中的數據,包括儲存在數據庫、文件或某些大型事務處理系統中的數據,而小型系統通常也需要一種在本地負責控制數據存儲的機制。

   由於數據訪問技術在不同的數據源類型中是不一樣的,因此對數據進行訪問成了給程序開發人員的一種挑戰,程序員需要對每一種類型的數據源使用特定的編程接口(API),即必須至少知道兩種語言來基於這些數據源開發業務應用:Java語言和由數據源所決定的數據訪問語言。這種數據訪問語言一般根據數據源的不同而不同,這使得學習使用某種數據源的開發成本相應提升。

  在Java數據對象技術(JDO)發佈之前,通常有三種方式用於存儲Java數據:串行化(即Serialization,也稱序列化)、JDBC和EJB中的CMP(容控存儲)方式。串行化用於將某個對象的狀態,以及它所指向的其它對象結構圖全部寫到一個輸出流中(比如文件、網絡等等),它保證了被寫入的對象之間的關係,這樣一來,在另一時刻,這個對象結構圖可以完整地重新構造出來。但串行化不支持事務處理、查詢或者向不同的用戶共享數據。它只允許在最初串行化時的粒度(指訪問對象的接口精細程度)基礎上進行訪問,並且當應用中需要處理多種或多次串行化時很難維護。串行化只適用於最簡單的應用,或者在某些無法有效地支持數據庫的嵌入式系統中。

  JDBC要求你明確地處理數據字段,並且將它們映射到關係數據庫的表中。開發人員被迫與兩種區別非常大的數據模型、語言和數據訪問手段打交道:Java,以及SQL中的關係數據模型。在開發中實現從關係數據模型到Java對象模型的映射是如此的複雜,以致於多數開發人員從不爲數據定義對象模型;他們只是簡單地編寫過程化的Java代碼來對底層的關係數據庫中的數據表進行操縱。最終結果是:他們根本不能從面向對象的開發中得到任何好處。

  EJB組件體系是被設計爲支持分佈式對象計算的。它也包括對容器管理持續性Container Managed Persistence(參見術語表)的支持來實現持續性。主要由於它們的分佈式特性,EJB應用比起JDO來複雜得多,對資源的消耗也大得多。不過,JDO被設計成具有一定的靈活性,這樣一來,JDO產品都可以用來在底層實現EJB的存儲處理,從而與EJB容器結合起來。如果你的應用需要對象存儲,但不需要分佈式的特性,你可以使用JDO來代替EJB組件。在EJB環境中最典型的JDO使用方案就是讓EJB中的對話組件(Session Bean)直接訪問JDO對象,避免使用實體組件(Entity Bean)。EJB組件必須運行在一個受控(Managed,參見術語表)的應用服務環境。但JDO應用可以運行在受控環境中,也可以運行在不受控的獨立環境中,這些使你可以靈活地選擇最合適的應用運行環境。

   如果你將精力集中在設計Java對象模型上,然後用JDO來進行存儲你的數據類的實例,你將大大提高生產力和開發效率。你只需要處理一種信息模型。而JDBC則要求你理解關係模型和SQL語言(譯者注:JDO並不是要取代JDBC,而是建立在JDBC基礎上的一個抽象的中間層,提供更簡單的數據存儲接口)。即使是在使用EJB CMP(即容控存儲,參見術語表)的時候,你也不得不學習與EJB體系相關的許多其它方面的內容,並且在建模方面還有一些JDO中不存在的侷限性。
JDO規範了JDO運行環境和你的可存儲對象類之間的約定。JDO被設計成支持多種數據源,包括一般情況下考慮不到的數據庫之類的數據源。從現在開始,我們使用數據庫(參見術語表)這一概念來表示任何你通過JDO來訪問的底層數據源。

   本章將會展開討論JDO的基本能力,這些基於對一個虛擬的Media Mania公司所開發的一個小型應用進行細緻的分析。這個公司在遍佈美國的很多商店中出租和出售多種形式的娛樂音像產品。他們的商店中有一些售貨亭,提供一些電影以及電影中的演員的信息。這些信息對客戶和商店的職員開放,以幫助選擇適合客戶口味的商品。

定義數據對象模型

  圖1-1是一個UML類圖,顯示了Media Mania公司的對象模型的相關類以及相互之間的關係。一個Movie(電影)對象表示一部特定的電影。每個至少在一部電影中出演角色的演員由一個Actor(演員)對象代表。而Role(角色)類表示某個演員在某部電影中扮演的特定角色,因此Role類也表示了電影和演員之間的一種關係,這種關係包含一個屬性(電影中的角色名)。每部電影包含一到多個角色。每個演員可以在不同的電影中扮演不同的角色,甚至在同一部電影中扮演多個角色。

image002.jpg
圖1-1 Media Mania公司的對象結構的UML類圖

  我們會將這些數據類以及操縱這些數據類實例的的程序放到com.mecdiamania.prototype包中。

需要存儲的類

  我們定義Movie、Actor和Role這幾個類爲可持續的,表示它們的實例是可以被儲存到數據庫中的。首先我們看看每個類的完整的源代碼。每個類中有一個package語句(譯者注:原書中錯寫成了"import"),因此可以很清楚地看到本例用到的每個類分別在哪個包中。

  例1-1顯示了Movie類的源代碼。JDO是定義在javax.jdo包中的,注意這個類並不一定要導入任何具體的JDO類。Java中的引用和java.util包中的Collection及相關子類(接口)被用來表示我們的類之間的關係,這是大多數Java應用中的標準方式。

  Movie類中的屬性使用Java中的標準類型,如String、Date、int等等。你可以將屬性聲明爲private的,不需要對每一屬性定義相應的get和set方法。Movie類中還有一些用於訪問這些私有屬性的方法,儘管這些方法在程序中的其它部分會用到,但它們並不是JDO所要求的。你可以使用屬性包裝來提供僅僅是抽象建模所需要的方法。這個類還有一些靜態屬性(static的),這些屬性並不存儲到數據庫。

   "genres"屬性是一個String型的,內容是該電影所屬的電影風格(動作、愛情、詭異等等)。一個Set接口用來表示該電影的演員表中的角色集合。"addRole()"方法將元素加入到演員表中,而"getCast()"方法返回一個不可以更改的集合,該集合中包含演員表。這些方法並不是JDO規定的,只是爲了方便應用編程而編寫的。"parseReleaseDate()"方法和"formatReleaseDate()"方法用於將電影的發行日期標準化(格式化)。爲了保持代碼的簡單,如果parseReleaseDate()的參數格式不對,將會返回null。

例1-1 Movie.java
package com.mediamania.prototype;
import java.util.Set;
import java.util.HashSet;
import java.util.Collections;
import java.util.Date;
import java.util.Calendar;
import java.text.SimpleDateFormat;
import java.text.ParsePosition;
public class Movie {
    private static SimpleDateFormat yearFmt = new SimpleDateFormat("yyyy");
    public static final String[] MPAAratings = {
        "G", "PG", "PG-13", "R", "NC-17", "NR"};
    private String title;
    private Date releaseDate;
    private int runningTime;
    private String rating;
    private String webSite;
    private String genres;
    private Set cast; // element type: Role
    private Movie() {}
    public Movie(String title, Date release, int duration, String rating,
                 String genres) {
        this.title = title;
        releaseDate = release;
        runningTime = duration;
        this.rating = rating;
        this.genres = genres;
        cast = new HashSet();
    }
    public String getTitle() {
        return title;
    }
    public Date getReleaseDate() {
        return releaseDate;
    }
    public String getRating() {
        return rating;
    }
    public int getRunningTime() {
        return runningTime;
    }
    public void setWebSite(String site) {
        webSite = site;
    }
    public String getWebSite() {
        return webSite;
    }
    public String getGenres() {
        return genres;
    }
    public void addRole(Role role) {
        cast.add(role);
    }
    public Set getCast() {
        return Collections.unmodifiableSet(cast);
    }
    public static Date parseReleaseDate(String val) {
        Date date = null;
        try {
            date = yearFmt.parse(val);
        } catch (java.text.ParseException exc) {}
        return date;
    }
    public String formatReleaseDate() {
        return yearFmt.format(releaseDate);
    }
}
  JDO對一個需要存儲的類強加了一個要求:一個無參數的構造器。如果你在類代碼中不定義任何構造器,編譯器會自動產生一個無參數的構造器;而如果你定義了帶參構造器,你就必須再定義一個無參構造器,可以將其聲明爲private以禁止外部訪問。如果你不定義這個無參構造器,一些JDO產品會自動爲你產生一個,但這只是具體的JDO產品提供的功能,是不可移植的。

  例1-2顯示了Actor類的源碼。在我們的目標中,所有的演員都有一個不會重複的名字來標識自己,可以是與出生時的姓名不同的化名。基於此,我們用一個String來表示演員的姓名。每個演員可能扮演一到多個角色,類中的"roles"成員表示Actor與Role關係中Actor的這一邊的屬性。第①行的註釋僅僅爲了文檔化,它並不爲JDO實現任何特殊的功能。第②行和第③行“addRole()”和“removeRole()”方法使程序可以維護某個Actor實例和它所關聯的Role實例集。

例1-2 Actor.java
package com.mediamania.prototype;
import java.util.Set;
import java.util.HashSet;
import java.util.Collections;
public class Actor {
    private String name;
①  private Set roles; // element type: Role
    private Actor() {}
    public Actor(String name) {
        this.name = name;
        roles = new HashSet();
    }
    public String getName() {
        return name;
    }
②  public void addRole(Role role) {
        roles.add(role);
    }
③  public void removeRole(Role role) {
        roles.remove(role);
    }
    public Set getRoles() {
        return Collections.unmodifiableSet(roles);
    }
}

  最後,例1-3給出了Role類的源碼。這個類代表了Movie類和Actor類之間的關係,並且包含某個演員在某部電影中扮演的具體角色的名字。其構造器初始化了對Movie和Actor對象的引用,並且通過調用處於關係的另一端的addRole()方法來保持邏輯一致性。

例1-3 Role.java
package com.mediamania.prototype;
public class Role {
    private String name;
    private Actor actor;
    private Movie movie;
    private Role() {}
    public Role(String name, Actor actor, Movie movie) {
        this.name = name;
        this.actor = actor;
        this.movie = movie;
        actor.addRole(this);
        movie.addRole(this);
    }
    public String getName() {
        return name;
    }
    public Actor getActor() {
        return actor;
    }
    public Movie getMovie() {
        return movie;
    }
}

  至此,我們已經瞭解了在數據庫中有實例存在的每個類的源碼。這些類並不需要導入或使用任何JDO相關的具體類。進一步,除了無參的構造器,無須任何數據或方法來標明這些類爲可存儲的。用於訪問或更新屬性數據並維護實例間的關係的代碼與大多數Java應用中的標準代碼是一模一樣的。

將類聲明爲可存儲的

  爲了讓類可以存儲,必須指明哪些類是需要存儲的,並且需要提供任何與具體存儲細節相關,而Java代碼中又無法體現的信息。JDO使用一個XML格式的元數據文件(metadata,參見術語表)來描述這些信息。
你可以基於類(多個文件)或包(一個文件)來定義XML格式的元數據文件。如果是基於類的,文件名與該類的名稱相同(譯者注:不包含包名),只是擴展名以".jdo"結尾。因此,描述Movie類的元數據文件需要命名爲"Movie.jdo"並且與編譯生成的Movie.class放置在同一個目錄中。如果選用基於包的元數據文件,則其中包含該包下的多個類以及多個下級包(sub-package)。例1-4給出了對Media Mania公司的對象模型進行描述的元數據。這個元數據基於這個對象模型所在的包,並且寫入文件"com/mediamania/prototype/package.jdo"中。

例1-4 …/prototype/package.jdo文件中的JDO元數據
    <?xml version="1.0" encoding="UTF-8" ?>
①  <!DOCTYPE jdo PUBLIC
"-//Sun Microsystems, Inc.//DTD Java Data Objects Metadata 1.0//EN"
"http://java.sun.com/dtd/jdo_1_0.dtd">
    <jdo>
②      <package name="com.mediamania.prototype" >
③          <class name="Movie" >
④              <field name="cast" >
⑤                  <collection element-type="Role"/>
                </field>
            </class>
⑥          <class name="Role" />
            <class name="Actor" >
                <field name="roles" >
                    <collection element-type="Role"/>
                </field>
            </class>
        </package>
    </jdo>
  

第①行中指明的"jdo_1_0.dtd"文件提供了對JDO元數據文件中用到的元素的定義。這個文檔類型定義(DTD)是由JDO規範所規定的,必須由一個JDO產品附帶提供。該文件也可以在http://java.sun.com/dtd下載。你也可以將"DOCTYPE"行中的內容改爲指向你本地文件系統中的一個副本文件。

  元數據文件可以包含與一個或多個含有可存儲類的包的關於一些存儲細節方面的信息。每個包由一個"package"元素進行定義,該元素具有一個"name"屬性來表示該包的名稱。第②行給出了我們的com.mediamania.prototype包的對應包元素。在該包元素內,是各個該包中的類元素。(如第③行就是Movie類的描述元素)。這個文件中可以順序寫入多個包元素,它們不能互相嵌套。

  如果某個屬性的存儲細節信息必須額外指出,那麼需要在"class"元素內部加入一個"field"元素,見第④行。比如,你可以通過這個字段元素標明一個集合類型的屬性中放置的是什麼樣的元素類型。這個元素不是必須的,但加入這個元素可以更有效地、更準確地完成映射。Movie類有一個集合(Set)類型的屬性:cast,而Actor類也有一個集合類型的屬性:roles;它們都包含對Role的引用。第⑤行標明瞭cast的元素類型。在多數情況下,在元數據中某屬性的默認值被假定爲最常用的值(比如Collection類型的屬性的元素類型會被默認爲Object)。

  所有的可以存儲的屬性在默認情況下會被視爲需存儲的(即具有持續性)。"static"和"final"的屬性則不能設置爲需存儲的。一個"transient"的屬性在默認情況下不被認爲是需存儲的,但可以顯式地在元數據中將其標明爲需存儲的。第四章將詳細闡述此問題。

  第四、十、十二和十三章會詳細描述你可以對類和類中的屬性進行哪些特性的描述。而對於一個非常簡單的象"Role"一樣的沒有什麼集合類型的屬性的類來說,你可以僅僅將這個類在元數據中列出來,如第⑥所示,只要這個類不需要什麼特別的與默認情況不同的說明。

項目編譯環境

  在本節中,我們將查看一下用於編譯和運行我們的JDO應用程序的開發環境。這包括項目的文件目錄結構,編譯所需要的相關的jar文件,以及對可存儲的類進行增強(Enhance,參見術語表)的語法(我們將在本節的後面部分詳細說明類的增強這個概念)。這個環境的建立一般與你所具體使用的JDO產品有關,所以你實際的項目開發環境及相關目錄結構可能會稍有不同。

  你可以使用Sun公司提供的JDO參考產品(Reference Implementation,是Sun在提出JDO規範的同時給出的一個實現規範的簡單產品,用於給其它JDO廠商提供參考,也可以直接作爲JDO產品使用,只是性能方便可能很差。這一方面有點類似於Sun的隨J2EE規範一同發佈的J2EE開發包中的樣本服務器),也可以根據自己的需要選擇其它的JDO產品。本書中的例子均基於JDO參考產品。你可以在http://www.jcp.org網站上選擇JSR-12,然後便可以下載到這個參考產品。當你安裝了一個JDO產品後,你需要搭建一個目錄結構,並設置相應的CLASSPATH以包含項目所需要的所有jar文件和相關的目錄,這樣才能編譯和運行你的應用程序。

  JDO在你的編譯過程中引入了一個額外的步驟,稱作類增強(Class Enhancement,參見術語表)。每個需要存儲的類必須經過增強才能在JDO的運行環境中使用。你的需存儲的類被javac編譯器編譯成一些.class文件,而一個增強器讀取這些生成的二進制代碼文件和對應的元數據文件,然後根據元數據文件標明的信息將一些額外代碼插入到二進制代碼中,從而生成一些新的可以在JDO環境中運行的.class文件。你的JDO應用程序只能調入這些增強過的類文件。JDO參考產品包含了一個增強器,名爲"參考增強器(Reference Enhancer)"。

JDO參考產品需要的jar文件

當你採用JDO參考產品後,你需要在開發過程中將下列jar文件放到你的CLASSPATH中。在運行時,所有這些jar文件也必須處於你的CLASSPATH中。
jdo.jar
  JDO規範定義的標準幾個的接口和類。

jdori.jar
  Sun公司的參考產品的jar文件

btree.jar
  JDO參考產品所用到的軟件,用於管理文件中存儲的數據。JDO參考產品採用一個文件來保存數據對象。

jta.jar
  Java的事務控制API。其中包含javax.transaction包中定義的Synchronization接口,在JDO接口中會使用到。這個jar文件中包含的其它一些工具類一般來說在一個JDO產品中會很有用。你可以在http://java.sun.com/products/jta/index.html上下載這個jar文件

antlr.jar
  JDO參考產品解析JDO的查詢語言(即JDOQL,參見術語表)中用到的語法分析技術相關文件。參考產品採用了Antlr 2.7.0。你可以在http://www.antlr.org上下載。

xerces.jar
  參考產品在解析XML文件(主要是元數據文件)所使用的Xerces-J 1.4.3版本。該文件可以在http://xml.apache.org/xerces-j/上下載。

 

  前三個文件是包含在JDO參考產品中的;後三個文件可以從各自的網站上下載。

  參考產品還包含一個jar文件:jdo-enhancer.jar,其中包括參考增強器。其中的所有類在jdori.jar文件中也有。多數情況下,你會在開發環境和運行環境都使用jdori.jar,不需要jdori-enhancer.jar文件。jdori-enhancer.jar文件被單獨打包原因是這樣一來你可以獨立於具體使用的JDO產品而對類代碼進行增強。除參考產品之外,一些其它的產品也會將這個jar文件與產品一起發佈。

  如果你使用了其它的JDO產品,它的文檔會告訴你所需要的jar文件的列表。一個產品通常將所有這些需要的jar文件都放到它安裝時生成的某個特別的目錄下。包含JDO的標準接口的jdo.jar文件應該被所有的JDO產品所包含,一般情況下,這個文件都會在某個具體廠商的JDO產品中存在。JDOCentral(http://www.jdocentral.com)提供大量的JDO資源,包括很多商用JDO產品的免費試用版下載。

項目目錄結構

  對於Media Mania應用開發環境來說,你需要採用下面的目錄結構,這個項目必須有一個根目錄,存在於系統的文件體系的某個地方。下面這些目錄都是以這個根目錄爲基準的:

src
  這個目錄包括應用的所有源碼。在src目錄下,有一個按照com/mediamania/prototype結構的子目錄體系(與Java中的com.mediamania.prototype包相對應)。這也是Movie.java、Actor.java和Role.java源文件所在的目錄。

classes
  當Java源碼被編譯時,生成的.class文件置於這個目錄中

enhanced
  這個目錄存放增強後的.class類代碼文件(由增強器所產生)

database
  這個目錄存放JDO參考產品用於存儲數據的文件。

  儘管這樣的目錄結構並不是JDO規範所要求的,但你得理解它,這樣才能跟隨我們對Media Mania應用的描述。

  當你執行你的JDO應用時,Java運行環境必須調入增強版本的類文件,也就是處於enhanced目錄中的類文件。因此,在你的CLASSPATH中這個目錄必須處於classes目錄之前。作爲一種可選方案,你也可以採用就地增強,用你的增強後的類文件直接替換未增強的文件。

增強類代碼以便存儲

  類在其實例被JDO環境處理之前必須先被增強。JDO增強器在你的類中加入額外的數據和方法,使其實例可以被JDO產品處理。增強器先從由javac編譯器所產生的類文件中讀取信息,再根據元數據來生成新的增強過的包含必要功能的類文件。JDO規範化了增強器所做的改變,使得增強後的類文件具有二進制兼容性,可以在其它的JDO產品中使用。這些增強後的文件也獨立於任何具體的數據庫。

  前面已經提到,Sun公司提供的JDO參考產品中的增強器稱作參考增強器。而JDO產品廠商一般可能會提供自己的增強器;在命令行調用增強器的語法可能會與這裏提到的有所不同。每個產品都會向你提供文檔以闡釋如果在該產品上對你的類進行增強。

  例1-5給出了使用參考增強器對我們的Media Mania應用的類進行增強的命令行。"-d"參數指明將要存放增強後的類文件的目錄,我們已經計劃放到enhanced目錄下。增強器接收一系列JDO元數據文件和一系列需要增強的類文件作參數。目錄之間的分隔符和續行符(line-continuation)可能會不一樣,這依賴於你進行編譯的操作系統。

例1-5 對類進行增強
java com.sun.jdori.enhancer.Main -d enhanced / 
    classes/com/mediamania/prototype/package.jdo / 
    classes/com/mediamania/prototype/Movie.class / 
    classes/com/mediamania/prototype/Actor.class / 
    classes/com/mediamania/prototype/Role.class
  儘管將元數據文件與源代碼放在一起會比較方便,JDO規範還是推薦元數據文件可以作爲與類文件一起作爲資源被類載入器(ClassLoader)調入。元數據在編譯時和運行時都需要,所以,我們將package.jdo元數據文件放在classes目錄體系中的prototype包的目錄中。

  在例1-5中,我們的對象模型中的所有.class類文件被一起列出,但你也可以將每個類文件單獨增強。當這個增強命令執行時,它將增強後的新文件放到enhanced目錄下。

創建數據庫連接和事務

  現在既然我們的類已經被增強了,它們的實例也就可以被儲存到數據庫中了。我們現在來看看應用中如果創建一個與數據庫的連接並在一個事務(Transaction)中執行一些操作。我們開始寫直接使用JDO接口的軟件代碼,所有的在應用中用到的JDO接口都定義在javax.jdo包中。

  JDO中有一個接口叫做PersistenceManager(存儲管理器,見術語表),它具有一個到數據庫的連接。一個PersistenceManager還有一個JDO中的Transaction(事務)接口的實例,用於控制一個事務的開始和結束。這個Transaction實例的獲取方式是調用PersistenceManager實例的currentTransaction()方法。

獲取一個PersistenceManager

  PersistenceManagerFactory(存儲管理器工廠,見術語表)用來配置和獲取PersistenceManager。PersistenceManagerFactory中的方法用來設置一些配置屬性,這些配置屬性控制了從中獲得的PersistenceManager實例的行爲。於是,一個JDO應用的第一步便是獲取一個PersistenceManagerFactory實例。要取得這個實例,需要調用下面的JDOHelper類的靜態方法:

  static PersistenceManagerFactory getPersistenceManagerFactory(Properties props);

  這個Properties實例可以通過程序設置,也可以從文件中讀取。例1-6列出了我們將在Media Mania應用中用到的配置文件的內容。其中,第①行中的PersistenceManagerFactoryClass屬性通過提供具體JDO產品的PersistenceManagerFactory接口實現類來指明採用哪個JDO產品。在本例中,我們指明Sun公司的JDO參考產品所定義的類。例1-6中列出的其它的屬性包括用於連接到特定的數據庫的連接URL和用戶名/密碼,這些一般都是連接到具體的數據庫所需要的。

例1-6 jdo.properties文件內容
①  javax.jdo.PersistenceManagerFactoryClass=com.sun.jdori.fostore.FOStorePMF
    javax.jdo.option.ConnectionURL=fostore:database/fostoredb
    javax.jdo.option.ConnectionUserName=dave
    javax.jdo.option.ConnectionPassword=jdo4me
    javax.jdo.option.Optimistic=false
  這個連接URL的格式依賴於採用的具體的數據庫。在JDO參考產品中包括它自己的存儲機制,稱作"文件對象數據庫File Object Store"(FOStore)。例1-6中的ConnectionURL屬性標明瞭實際使用的數據庫位於database目錄中,在我們的項目的根目錄下。在本例中,我們提供了一個相對路徑;但提供絕對路徑也是可以的。這個URL同時指明瞭FOStore數據庫文件名稱將以"fostoredb"開頭。

  如果你使用了別的JDO產品,你需要對以上這些屬性提供另外的值,可能你還得提供一些額外的屬性。請參閱該產品的文檔以獲取需要配置的必要的屬性的說明。

創建一個FOStore數據庫

  要使用FOStore我們必須先創建一個數據庫。例1-7中的程序利用jdo.properties文件創建一個數據庫;所有的應用都使用這個配置文件。第①行將這些配置屬性從jdo.properties文件中調入到一個Properties實例中。該程序的第②行加入了一個"com.sun.jdori.option.ConnectionCreate"屬性以指明數據庫需要創建。將其設爲true,就能引導參考產品創建該數據庫。我們在第③行調用getPersistenceManagerFactory()來獲取PersistenceManagerFactory。第④行生成一個PersistenceManager。

  爲完成數據庫的創建,我們還需要開始並結束一個事務。第⑤行中調用了PersistenceManager的currentTransaction()方法來訪問與該PersistenceManager相關聯的Transaction實例。第⑥行和第⑦行調用這個Transaction實例的begin()和commit()方法來開始和結束一個事務。當你執行這個程序時,在database目錄下就會生成一個FOStore數據庫,包括兩個文件:fostore.btd和fostore.btx.

例1-7 創建一個FOStore數據庫
package com.mediamania;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Properties;
import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManagerFactory;
import javax.jdo.PersistenceManager;
import javax.jdo.Transaction;
public class CreateDatabase {
    public static void main(String[] args)) {
        create();
    }
    public static void create() {
        try {
            InputStream propertyStream = new FileInputStream("jdo.properties");
            Properties jdoproperties = new Properties();
①          jdoproperties.load(propertyStream);
②          jdoproperties.put("com.sun.jdori.option.ConnectionCreate", "true");
            PersistenceManagerFactory pmf =
③              JDOHelper.getPersistenceManagerFactory(jdoproperties);
④          PersistenceManager pm = pmf.getPersistenceManager();
⑤          Transaction tx = pm.currentTransaction();
⑥          tx.begin();
⑦          tx.commit();
        } catch (Exception e) {
            System.err.println("Exception creating the database");
            e.printStackTrace();
            System.exit( -1);
        }
    }
}

  JDO參考產品提供了這種程序化創建FODatastore數據庫的方式,而大多數數據庫都提供一個獨立於JDO的工具來創建數據庫。JDO並不規定一個與廠商無關的接口來創建數據庫。數據庫的創建一般都與具體使用的數據庫相關。本程序中顯示了在FOStore數據庫中是如何完成這一步的。

  另外,如果你在關係數據庫上使用JDO,某些情況下可以有一個額外的步驟:根據對象模型創建或者將對象模型映射到一個現存的數據庫模式(shema,即某數據庫用戶及其所擁有的數據表體系的合稱)。創建一個數據庫模式的過程與你採用的具體JDO產品的有關,你需要查看該產品的文檔來決定採取必要的步驟。

對實例的操作

  至此我們已經有了一個可以存放數據類的實例的數據庫,每個程序需要獲得一個PersistenceManager來訪問或更新該數據庫。例1-8給出了MediaManiaApp類的源碼,這個類是本書中的每個應用程序的基礎類,每個程序是在execute()方法中實現了具體的業務邏輯的一個具體的子類(Concrete子類,相對於抽象Abstract而言)。

  MediaManiaApp有一個構造器用來從jdo.properties中讀取配置信息(行①)。從該文件調入配置信息後,它調用getPropertyOverrides()方法並且合併成最終的屬性集(properties)到jdoproperties對象中。一個程序子類可以重載getPropertyOverrides()來提供額外的配置信息或者更改jdo.properties文件中給出的配置。這個構造器獲取一個PersistenceManagerFactory(行②),然後獲取一個PersistenceManager(行③)。我們還提供一個getPersistenceManager()方法以便在MediaManiaApp類之外獲取PersistenceManager。與PersistenceManager關聯的Transaction在第④行獲取。

  各個程序子類調用一個在MediaManiaApp類中定義的executeTransaction()方法,這個方法在行⑤中開始一個事務,然後在行⑥中調用execute()方法,也即執行子類中的具體功能的方法。
我們選擇了一個特別的程序類的設計來簡化和減少創建一個可運行的環境的冗餘代碼。這些並不是JDO所要求的,你也可以根據自己的應用程序環境選擇最爲合適的方式。

  當(子類中實現的)execute()方法返回後,我們會嘗試提交這個事務(行⑦),而如果有任何異常發生的話,我們會回滾(rollback)這個事務並將異常信息打印到系統錯誤輸出流中(System.err)。

例1-8 MediaManiaApp基類
package com.mediamania;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Properties;
import java.util.Map;
import java.util.HashMap;
import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManagerFactory;
import javax.jdo.PersistenceManager;
import javax.jdo.Transaction;
public abstract class MediaManiaApp {
    protected PersistenceManagerFactory pmf;
    protected PersistenceManager pm;
    protected Transaction tx;
    public abstract void execute(); //defined in concrete application subclasses
    protected static Map getPropertyOverrides() {
        return new HashMap();
    }
    public MediaManiaApp() {
        try {
            InputStream propertyStream = new FileInputStream("jdo.properties");
            Properties jdoproperties = new Properties();
①          jdoproperties.load(propertyStream);
            jdoproperties.putAll(getPropertyOverrides());
②          pmf = JDOHelper.getPersistenceManagerFactory(jdoproperties);
③          pm = pmf.getPersistenceManager();
④          tx = pm.currentTransaction();
        } catch (Exception e) {
            e.printStackTrace(System.err);
            System.exit( -1);
        }
    }
    public PersistenceManager getPersistenceManager() {
        return pm;
    }
    public void executeTransaction() {
        try {
⑤          tx.begin();
⑥          execute();
⑦          tx.commit();
        } catch (Throwable exception) {
            exception.printStackTrace(System.err);
            if (tx.isActive())
                tx.rollback();
        }
    }
}

存儲實例

  我們來看看一個簡單的程序,名爲CreateMovie,用於存儲一個Movie實例,如例1-9所示。該的功能被放在execute()方法中。構造一個CreateMovie的實例後,我們調用MediaManiaApp基類中定義的executeTransaction()方法,它會調用本類中重載過的execute()方法。這個execute()方法中行⑤初始化一個單獨的Movie實例,然後在行⑥調用PersistenceManager的makePersistent()方法保存這個實例。如果這個事務成功提交(commit),這個Movie實例就會被存儲到數據庫中。

例1-9 創建一個Movie實例並保存它
package com.mediamania.prototype;
import java.util.Calendar;
import java.util.Date;
import com.mediamania.MediaManiaApp;
public class CreateMovie extends MediaManiaApp {
    public static void main(String[] args)) {
        CreateMovie createMovie = new CreateMovie();
        createMovie.executeTransaction();
    }
    public void execute() {
        Calendar cal = Calendar.getInstance();
        cal.clear();
        cal.set(Calendar.YEAR, 1997);
        Date date = cal.getTime();
⑤      Movie movie = new Movie("Titanic", date, 194, "PG-13",
                                 "historical,drama");
⑥      pm.makePersistent(movie);
    }
}

  現在我們來看一個更大的應用程序:LoadMovies,如例1-10中所示,它從一個包含電影信息的文件中讀取並創建多個Movie實例。這個信息文件名作爲參數傳遞到程序中,LoadMovies構造器初始化一個BufferedReader來讀取信息。execute()方法通過調用parseMovieDate()每次從這個文件讀取一行並分析之,從而在行①創建一個Movie實例,並在行②保存之。當這個事務在executeTransaction()中提交時,所有新創建的Movie實例都會被保存到數據庫中。

例1-10 LoadMovies
package com.mediamania.prototype;
import java.io.FileReader;
import java.io.BufferedReader;
import java.util.Calendar;
import java.util.Date;
import java.util.StringTokenizer;
import javax.jdo.PersistenceManager;
import com.mediamania.MediaManiaApp;
public class LoadMovies extends MediaManiaApp {
    private BufferedReader reader;
    public static void main(String[] args)) {
        LoadMovies loadMovies = new LoadMovies(args[0]);
        loadMovies.executeTransaction();
    }
    public LoadMovies(String filename) {
        try {
            FileReader fr = new FileReader(filename);
            reader = new BufferedReader(fr);
        } catch (Exception e) {
            System.err.print("Unable to open input file ");
            System.err.println(filename);
            e.printStackTrace();
            System.exit( -1);
        }
    }
    public void execute() {
        try {
            while (reader.ready()) {
                String line = reader.readLine();
                parseMovieData(line);
            }
        } catch (java.io.IOException e) {
            System.err.println("Exception reading input file");
            e.printStackTrace(System.err);
        }
    }
    public void parseMovieData(String line) {
        StringTokenizer tokenizer new StringTokenizer(line, ";");
        String title = tokenizer.nextToken();
        String dateStr = tokenizer.nextToken();
        Date releaseDate = Movie.parseReleaseDate(dateStr);
        int runningTime = 0;
        try {
            runningTime = Integer.parseInt(tokenizer.nextToken());
        } catch (java.lang.NumberFormatException e) {
            System.err.print("Exception parsing running time for ");
            System.err.println(title);
        }
        String rating = tokenizer.nextToken();
        String genres = tokenizer.nextToken();
①      Movie movie = new Movie(title, releaseDate, runningTime, rating,
                                 genres);
②      pm.makePersistent(movie);
    }
}

  電影信息文件中的數據格式是:

  movie title;release date;running time;movie rating;genre1,genre2,genre3

  其中用於表示發行日期的數據格式由Movie類來控制,因此parseReleaseDate()被調用以根據發行日期數據產生一個Date實例。一部電影可以屬於多種風格,在數據行的尾部列出。

訪問實例

  現在讓我們來訪問數據庫中的Movie實例以驗證我們已經成功地將它們保存。在JDO中有很多方式可以訪問實例:

  1. 從一個類的擴展(Extent,表示一個類及其所有子類)中迭代(iterate,參見術語表)
  2. 通過對象模型來瀏覽(navigate)
  3. 執行一個查詢

  extent(擴展)是用來訪問某個類及其所有子類的工具。而如果程序中只想訪問其中的部分實例,可以執行一個查詢,在查詢中通過過濾條件(filter)規定必須滿足的一個布爾型的斷言(即判斷語句)來限制返回的實例。當程序從數據庫中訪問到一個實例之後,便可以通過在對象模型該實例相關的對其它實例的引用或對其它實例集合的遍歷來瀏覽其它的實例。這些實例在被訪問到之前不會從數據庫調入到內存。以上這些訪問實例的方式常常被結合起來使用,JDO保證在一個PersistenceManager中每個實例在內存中只會有一個副本。每個PersistenceManager控制一個單獨的事務上下文(transaction context)。

遍歷一個類擴展

  JDO提供了Extent接口來訪問一個類的擴展。這個擴展允許對一個類的所有實例進行訪問,但並不表示所有的實例都在內存中。下面的例1-11給出的PrintMovies程序就採用了Movie類的擴展。

例1-11 遍歷Movie類的擴展
package com.mediamania.prototype;
import java.util.Iterator;
import java.util.Set;
import javax.jdo.PersistenceManager;
import javax.jdo.Extent;
import com.mediamania.MediaManiaApp;
public class PrintMovies extends MediaManiaApp {
    public static void main(String[] args)) {
        PrintMovies movies = new PrintMovies();
        movies.executeTransaction();
    }
    public void execute() {
①      Extent extent = pm.getExtent(Movie.class, true);
②      Iterator iter = extent.iterator();
        while (iter.hasNext()) {
③          Movie movie = (Movie) iter.next();
            System.out.print(movie.getTitle());
            System.out.print(";");
            System.out.print(movie.getRating());
            System.out.print(";");
            System.out.print(movie.formatReleaseDate());
            System.out.print(";");
            System.out.print(movie.getRunningTime());
            System.out.print(";");
④          System.out.println(movie.getGenres());
⑤          Set cast = movie.getCast();
            Iterator castIterator = cast.iterator();
            while (castIterator.hasNext()) {
⑥              Role role = (Role) castIterator.next();
                System.out.print("/t");
                System.out.print(role.getName());
                System.out.print(",");
⑦              System.out.println(role.getActor().getName());
            }
        }
⑧      extent.close(iter);
    }
}

  第①行中我們從PersistenceManager獲取一個Movie類的擴展,第二個參數表示是否希望包含Movie類的所有子類,false使得只有Movie類的實例被返回,即便是還有其它的Movie的子類的實例存在。儘管我們目前還沒有任何Movie的子類,以true作參數將保證我們將來可能加入的類似的Movie的子類的實例也被返回。Extent接口有一個iterator()方法,即我們在行②中調用以獲取一個Iterator遍歷器來逐個訪問這個類擴展中的每個實例。行③採用遍歷器來訪問Movie類的實例。程序在後面就可以針對Movie的實例進行操作來取得數據並打印出來。例如:行④中我們調用getGenres()來取得一部電影所屬的風格,行⑤中我們取得電影中的角色集合,在行⑥中取得每個角色並打印其名稱,行⑦中我們通過調用getActor()來瀏覽該角色的演員對象,這是我們在Role類中已經定義好的,我們打印了該演員的姓名。

   當這個程序結束對類擴展的遍歷之後,行⑧中關閉這個遍歷器來釋放執行這個遍歷時佔用的相關資源。對一個擴展可以同時進行多個遍歷,這個close()方法關閉一個特定的遍歷器,而closeAll()方法可以關閉與一個類擴展相關聯的所有遍歷器。

瀏覽對象模型

   例1-11演示了對Movie類擴展的遍歷。但在行⑥中我們也根據對象模型瀏覽了一部電影相關的角色集合。行⑦中我們也通過Role實例中的引用訪問了相關的演員實例。行⑤和行⑦分別顯示了對"對多(to-many)"和"對一(to-one)"的關係的訪問(traversal)。從一個類到另一個類的關係具有一個重數(cardinality,表示可能發生關聯的目標對象總數),表示與一個或多個實例進行關聯。一個引用表示在重數爲一的情況;而一個集合用於關聯多個對象(重數爲多)。

   訪問相關聯的實例所需要的語法與在內存中對關聯對象的標準瀏覽方式是完全一樣的。在行③和行⑦之間程序並不需要調用任何JDO的接口,它僅僅是在對象中通過關係來遍歷(traverse)。相關的實例直到被程序直接訪問到時纔會被從數據庫讀入並在內存中生成。對數據庫的訪問是透明的,實例即需即調。某些JDO產品還提供Java接口之外的機制讓你調節對該JDO產品的緩衝的訪問機制。你的Java程序獨立於這些優化之外,但可以從這些優化中獲得運行性能上的改善。

  在JDO環境中訪問相關的數據庫對象的方式與在非JDO的環境訪問臨時(transient)對象的方式是一樣的,因此你可以按照非JDO的方式編寫你的軟件。現有的沒有任何針對JDO或其它方面的存儲因素的考慮的軟件可以通過JDO來完成對數據庫中的實例對象的瀏覽。這個特點極大地推動了開發生產力,也允許現有的軟件可以快速、方便地集成到JDO環境中。

執行查詢

   在一個類擴展的基礎上也可以運行一個查詢。JDO中的Query接口用來選取符合某些條件的實例子集。本章中剩下的例子需要按照給定的唯一名稱訪問指定的Actor或Movie對象。這些方法(參見例1-12)大同小異;getActor()執行一個基於姓名的查詢,而getMovie()方法執行一個基於片名的查詢。

例1-12 PrototypeQueries類中的查詢方法
package com.mediamania.prototype;
import java.util.Collection;
import java.util.Iterator;
import javax.jdo.PersistenceManager;
import javax.jdo.Extent;
import javax.jdo.Query;
public class PrototypeQueries {
    public static Actor getActor(PersistenceManager pm, String actorName) {
①      Extent actorExtent = pm.getExtent(Actor.class, true);
②      Query query = pm.newQuery(actorExtent, "name == actorName");
③      query.declareParameters("String actorName");
④      Collection result = (Collection) query.execute(actorName);
        Iterator iter = result.iterator();
        Actor actor = null;
⑤      if (iter.hasNext())
            actor = (Actor) iter.next();
⑥      query.close(result);
        return actor;
    }
    public static Movie getMovie(PersistenceManager pm, String movieTitle) {
        Extent movieExtent = pm.getExtent(Movie.class, true);
        Query query = pm.newQuery(movieExtent, "title == movieTitle");
        query.declareParameters("String movieTitle");
        Collection result = (Collection) query.execute(movieTitle);
        Iterator iter = result.iterator();
        Movie movie = null;
        if (iter.hasNext())
            movie = (Movie) iter.next();
        query.close(result);
        return movie;
    }
}

  我們來看看getActor()方法。在行①中我們取到一個Actor類的擴展,行②中通過在PersistenceManager接口中定義的newQuery()方法創建了一個Query實例,這個查詢建立在這個類擴展和相應的過濾條件的基礎上。

  在過濾條件中的"name"標識符代表Actor類中的name屬性。用於決定如何解釋這個標識符的命名空間(namespace)取決於初始化這個Query實例的類擴展。過濾條件表達式指明演員的姓名等於actorName,在這個過濾器中我們可以用"=="號來直接比較兩個字符串,而不必使用Java的語法(name.equals(actorName))。
actorName標識符是一個查詢參數,在行③中進行聲明。一個查詢參數讓你在查詢執行時給出一個值來進行查詢。我們選擇同樣的標識符"actorName"來既作爲這個方法的參數名,又作爲查詢的參數名。這個查詢在第④行執行,以getActor()方法的actorName參數值作爲查詢參數actorName的值。

  Query.execute()的返回類型被定義爲Object,在JDO1.0.1中,返回的類型總是Collection類型,因此我們可以直接將這個返回對象強制制轉換爲一個Collection。在JDO1.0.1中定義返回Object是爲了讓將來可以擴展爲返回一個Collection以外的類型。之後,我們的方法在第⑤行試着訪問一個元素對象,我們假定對一個姓名來說,在數據庫中只有單獨的一個Actor實例與之對應。在返回這個結果之前,行⑥關閉這個查詢以釋放相關的資源。如果這個查詢找到了該姓名的演員實例,則返回之,否則如果查詢結果是空集,則返回null。

更改實例

  現在我們看看兩個更改數據庫中的實例的程序。當一個程序在一個事務中訪問一個數據庫中的實例時,它可以更改這個實例的一個或多個屬性值。而事務提交時,所有對這些實例的更改會被自動地全部同步到數據庫中去。

  例1-13中給出的UpdateWebSite程序用來設置與一個電影相關的網站。它有兩個參數:第一個是電影的片名,第二個是電影的網站URL。初始化這個程序實例後,executeTransaction()方法被調用,而該方法中會調用本程序的execute()方法。

  行①調用getMovie()(在例1-12中定義)來取得指定片名的Movie對象,如果getMovie()返回null,程序會報告找不到該片名的電影,然後退出。否則,在行②中我們調用setWebSite()(在例1-1中定義),以便設置該Movie對象的webSite屬性爲給出的參數值。當executeTransaction()提交這個事務的時候,對Movie實例的修改會自動被同步到數據庫中。

例1-13 更改一個屬性
package com.mediamania.prototype;
import com.mediamania.MediaManiaApp;
public class UpdateWebSite extends MediaManiaApp {
    private String movieTitle;
    private String newWebSite;
    public static void main(String[] args)) {
        String title = args[0];
        String website = args[1];
        UpdateWebSite update = new UpdateWebSite(title, website);
        update.executeTransaction();
    }
    public UpdateWebSite(String title, String site) {
        movieTitle = title;
        newWebSite = site;
    }
    public void execute() {
①      Movie movie = PrototypeQueries.getMovie(pm, movieTitle);
        if (movie == null) {
            System.err.print("Could not access movie with title of ");
            System.err.println(movieTitle);
            return;
        }
②      movie.setWebSite(newWebSite);
    }
}

  在例1-13中,你可以看到,程序並不需要調用任何JDO接口來更改Movie對象的屬性,這個程序訪問了一個實例然後調用一個方法更改它的網站屬性,這個方法採用Java的標準語法來更改對應的屬性。而在提交之前無需任何額外的編碼來將更新同步到數據庫,JDO環境會自動地同步變化。本程序執行了對已存儲的實例的操作,而不需要直接導入或者使用任何JDO接口。

  現在我們看看一個大一些的程序,名爲LoadRoles,來展示JDO的一些特性。LoadRoles,見例1-14,負責調入一部電影的角色以及扮演這些角色的演員的信息。LoadRoles被傳入一個單獨的參數,用於指明一個文件名,然後程序的構造器中初始化一個BufferedReader來讀取這個文件。它讀取文件的文本,每行一個角色,按以下的格式:

  movie title;actor's name;role name

  通常某部電影的所有角色被組合放到本文件中的相鄰的位置;LoadRoles採用一些小的優化來決定當前正處理的角色是否與前一個角色同屬一部電影。

例1-14 實例更改和按可達性存儲(persistence-by-reachability)
package com.mediamania.prototype;
import java.io.FileReader;
import java.io.BufferedReader;
import java.util.StringTokenizer;
import com.mediamania.MediaManiaApp;
public class LoadRoles extends MediaManiaApp {
    private BufferedReader reader;
    public static void main(String[] args)) {
        LoadRoles loadRoles = new LoadRoles(args[0]);
        loadRoles.executeTransaction();
    }
    public LoadRoles(String filename) {
        try {
            FileReader fr = new FileReader(filename);
            reader = new BufferedReader(fr);
        } catch (java.io.IOException e) {
            System.err.print("Unable to open input file ");
            System.err.println(filename);
            System.exit( -1);
        }
    }
    public void execute() {
        String lastTitle = "";
        Movie movie = null;
        try {
            while (reader.ready()) {
                String line = reader.readLine();
                StringTokenizer tokenizer = new StringTokenizer(line, ";");
                String title = tokenizer.nextToken();
                String actorName = tokenizer.nextToken();
                String roleName = tokenizer.nextToken();
                if (!title.equals(lastTitle)) {
①                  movie = PrototypeQueries.getMovie(pm, title);
                    if (movie == null) {
                        System.err.print("Movie title not found:");
                        System.err.println(title);
                        continue;
                    }
                    lastTitle = title;
                }
②              Actor actor = PrototypeQueries.getActor(pm, actorName);
                if (actor == null) {
③                  actor = new Actor(actorName);
④                  pm.makePersistent(actor);
                }
⑤              Role role = new Role(roleName, actor, movie);
            }
        } catch (java.io.IOException e) {
            System.err.println("Exception reading input file");
            System.err.println(e);
            return;
        }
    }
}

  其中的execute()方法讀取文件中的每一行信息。首先,它檢查該行的電影片名是否與前一行一樣,如果不是,行①調用getMovie()來根據片名獲取該電影,如果該片名的電影在數據庫中不存在,則程序輸出一個錯誤信息,並跳過這行信息。行②試着訪問指定姓名的演員,如果數據庫中找不到該姓名的演員,則一個新的演員會被創建,在行③中設置其姓名,然後在行④中保存之。

  程序中至此我們已經讀取了文件信息並在數據庫中按文件中給出的名稱查找了相關的實例。而真正完成任務的行是行⑤,該行創建一個新的角色實例,這個角色的構造器在例1-3中已經定義;在此我們重複一下以便更詳細地看看:

public Role(String name, Actor actor, Movie movie) {
①  this.name = name;
②  this.actor = actor;
③  this.movie = movie;
④  actor.addRole(this);
⑤  movie.addRole(this);
}

  行①初始化本角色的名稱,行②建立一個到相關的演員對象的引用,行③建立一個到相應的電影實例的引用。Actor與Role之間的關係和Movie與Role之間的關係都是雙向的,因此關係的另一端也需要作相應更新,行④中我們調用演員的addRole()方法,它將本角色加入到該演員對象的roles集合中;類似地,行⑤中我們調用電影對象的addRole()方法將本角色加入到電影對象的cast(角色表)集合中。在Actor.roles中和Movie.cast中加入當前角色作爲一個元素將引起被actor和movie引用到的對象發生變化。

  Role構造器展示了你可以通過簡單地建立一個引用來建立到另一個實例的關係,也可以將一個或多個實例加入到引用的集合中來建立到另一實例的關係。這個過程是Java中的對象關係的體現,在JDO中也得到直接支持。當事務提交後,內存中建立的關係將被同步到數據庫中。

  Role構造器返回後,load()方法處理文件中的下一行。這個while循環在讀完文件中的所有行後結束。
你可能已經注意到我們從沒有對Role實例調用makePersistent()方法,而在提交時,Role實例也將被保存到數據庫,因爲JDO支持"可達性存儲(persistence-by-reachability)"。可達性存儲使得一個可存儲類的任何未存儲的實例在提交時被保存起來,只要從一個已經被保存的實例可以直接或間接地到達這個實例。實例的可達性基於直接的引用或者集合型的引用。一個實例的所有可達實例集合所形成的對象樹稱作該實例的"相關實例完全閉包(complete closure)"。可達性規則被傳遞性地應用在所有可存儲實例的在內存中的所有引用中,從而使得整個完全閉包成爲可存儲的。

  從其它存儲實例中去掉所有的對某個存儲實例的引用並不會自動地將被去掉的實例刪除,你需要顯式地刪除這個實例,這將是我們下一小節將要涉及的。如果你在一個事務中建立了一個存儲實例到非存儲實例的引用,但接着又修改了引用關係使非存儲實例不被引用,那麼在提交的時候,這個非存儲實例仍保持非存儲狀態,不會被保存到數據庫。

  存儲可達性讓你可以寫一大堆代碼而不需要調用任何JDO的接口來保存實例,因此你的代碼可以集中在如何在內存中建立實例間的關係,而JDO產品會將你在內存中通過關係建立的非存儲實例保存到數據庫。你的程序可以在內存中建立相當複雜的對象體系圖然後從一個已存儲實例建立一個到這個圖的引用來完成這些新對象的保存。

刪除實例

  現在我們來看看一個從數據庫中刪除一些實例的程序。在例1-15中,DeleteMovie程序用來刪除一個Movie實例。要刪除的電影的片名作爲參數給出。行①試着訪問這個電影實例,如果該片名的電影不存在,程序報告錯誤並退出。行⑥中我們調用deletePersistent()方法來刪除該Movie實例自身。

例1-15 從數據庫刪除一個Movie實例
package com.mediamania.prototype;
import java.util.Collection;
import java.util.Set;
import java.util.Iterator;
import javax.jdo.PersistenceManager;
import com.mediamania.MediaManiaApp;
public class DeleteMovie extends MediaManiaApp {
    private String movieTitle;
    public static void main(String[] args)) {
        String title = args[0];
        DeleteMovie deleteMovie = new DeleteMovie(title);
        deleteMovie.executeTransaction();
    }
    public DeleteMovie(String title) {
        movieTitle = title;
    }
    public void execute() {
①      Movie movie = PrototypeQueries.getMovie(pm, movieTitle);
        if (movie == null) {
            System.err.print("Could not access movie with title of ");
            System.err.println(movieTitle);
            return;
        }
②      Set cast = movie.getCast();
        Iterator iter = cast.iterator();
        while (iter.hasNext()) {
            Role role = (Role) iter.next();
③          Actor actor = role.getActor();
④          actor.removeRole(role);
        }
⑤      pm.deletePersistentAll(cast);
⑥      pm.deletePersistent(movie);
    }
}

  然後,我們也需要刪除該電影的所有角色實例,此外,由於演員實例中含有到角色實例的引用,我們也需要刪除這些引用。行②中我們取得與Movie實例相關的Role實例集合,然後遍歷每個角色,在行③中取得它關聯的演員,因爲我們要刪除這個角色,所以在行④中我們去掉演員對該角色的引用。行⑤中我們調用了deletePersistentAll()來刪除該電影的角色表中的所有角色實例。當事務提交時,電影實例和相關的角色實例被從數據庫中刪除,而與該電影相關的所有演員也得到相應更新,從而不再含有對這些已刪除的角色的引用。

  你必須調用這些deletePersistent()方法來顯式地刪除數據庫中的實例,它們並不是makePersistent()的反向方法因爲makePersistent()採用了可達性存儲的規則。進一步,JDO的數據庫沒有Java中的垃圾回收機制可以讓一個實例在不被數據庫中其它實例引用的時候被自動刪除。實現等價的垃圾回收機制是一個非常複雜的手續,而且這樣的系統常常變得性能低下。

小結

  你已經看到,一個具有一定規模的應用程序可以完成獨立於JDO來編寫,採用傳統的Java建模、語法和編程技巧。你可以基於Java的對象模型來定義應用程序中要保存的信息。當你採用類擴展或查詢到訪問數據庫中的實例的時候,你的代碼看上去與其它的訪問內存中的實例的Java程序沒什麼區別。你不需要學習其它的數據模型或類似SQL的訪問語言,你不需要給出從內存中的對象到數據庫中的鏡像數據之間的映射方法。你可以充分地利用Java中的面向對象的特性而不受任何限制(譯者注:限制實際上還是有的,只不過影響不大),這包括使用繼承和多態(polymorphism),而這些在JDBC或EJB體系中是不可能的;比起這些其它的競爭技術來,你能用對象模型和很少量的代碼開發應用程序。簡單、平常的Java對象可以用一種透明的方式保存到數據庫或在數據庫中訪問。JDO提供了一個非常容易上手而高效的環境來編寫需要保存數據的Java應用程序。

譯者注

  本書《Java Data Objects》是JDO規範專家組的兩名成員:專家組領導人Craig Russell和資深對象技術專家David Jordan共同編寫的JDO入門教材,權威實用,在2003年4月中旬,也就是JDO1.0規範出臺一年之際,由O'Reilly出版社正式出版。關於本書的相關信息,請查看:http://www.oreilly.com/catalog/jvadtaobj/chapter/index.html

  (如果您對該書感興趣,並希望看到全書的中文譯本,請向O'Reilly中國出版社發郵件表達您的看法和意見:[email protected])

  本文的版權屬於筆者本人,但歡迎轉載,前提是註明出處和原作者。另外,歡迎在我的專欄中查看我的另幾篇文章,並提出寶貴意見!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章