(孙宾 译)
(本译文的版权属作者本人,出于交流的目的,欢迎转载,但必须注明出处和原作者!如果您对该书感兴趣,并希望看到全书的中文译本,请向O'Reilly中国出版社发邮件表达您的看法和意见:[email protected])
前言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. 示范程序的源代码
索引
作为异军突起的新型语言,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类也表示了电影和演员之间的一种关系,这种关系包含一个属性(电影中的角色名)。每部电影包含一到多个角色。每个演员可以在不同的电影中扮演不同的角色,甚至在同一部电影中扮演多个角色。
图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()方法。
获取一个PersistenceManagerPersistenceManagerFactory(存储管理器工厂,见术语表)用来配置和获取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中有很多方式可以访问实例:
从一个类的扩展(Extent,表示一个类及其所有子类)中迭代(iterate,参见术语表) 通过对象模型来浏览(navigate) 执行一个查询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])
本文的版权属于笔者本人,但欢迎转载,前提是注明出处和原作者。另外,欢迎在我的专栏中查看我的另几篇文章,并提出宝贵意见!
本文地址:http://com.8s8s.com/it/it17855.htm