一起学习
JDO应用介绍
by Teresa Lau
译自:
(译者注)本文以一个实际的具有一定相互关系的类结构实例和KodoJDO产品作基础,介绍JDO的原理、使用及特点。尤其是与传统JDBC技术的对比,比如一个类的代码从480行减少到140行的现实,说明JDO对代码的减少。另外,本文作者是一位具有五年以上Java顾问经验的女中豪杰,拥有硕士学位,目前工作于纽约。
Java Data Object(JDO) 是一个用于存取某种数据仓库中的对象的标准化API。
使JDO从各种数据对象存取技术中异军突起的是它的高度的易用性和灵活性。
JDO提供了透明的对象存储,因此对开发人员来说,存储数据对象完全不需要额外的代码(如JDBC API的使用)。这些繁琐的例行工作已经转移到JDO产品提供商身上,使开发人员解脱出来,从而集中时间和精力在业务逻辑上。另外,JDO很灵活,因为它可以在任何数据底层上运行。JDBC只是面向关系数据库(RDBMS),而JDO更通用,提供到任何数据底层的存储功能,比如关系数据库、文件、XML以及对象数据库(ODBMS)等等,使得应用可移植性更强。
概述
描述符(Metadata)和增强器(Enhancer)
在JDO中,任何需要存储的类必须是PersistenceCapable(可存储的),而任何用到这些类的其它类必须是PersistenceAware(存储可知的)。看上去有点复杂,不过好消息是采用JDO的透明存储技术,你不需要在代码中将你的类去实现javax.jdo.PersistenceCapable接口或PersistenceAware接口。你只需要象往常一样写一个类即可,JDO厂商会提供一个增强器,这个增强器会改造你的类代码(译者注:就是.class文件,改造class中的二进制代码并不是一件很高深的事,有很多工具可以提供给你对class文件进行改造,比如在某个方法开始和结束时输出日志等等),根据你的描述符使之实现PersistenceCapable接口。你需要做的唯一的额外工作就是为你需要存储的类写一个XML格式的描述符文件。附件1显示了一个我在后面的代码示范中会用到的描述符文件。(附件17和本文的代码可以在下载。)
描述符一般来说很短,在默认条件下写起来一点也不难,JDO从类本身已经获取了大量的信息。你仅在下列情况下在描述符中添加额外信息:
。你需要改变JDO的默认操作方式,比如,使一个属性不存入数据库,即使它没有标记为transient
。有些额外的信息是JDO无法从类本身获取的,比如:哪一个属性你想设为数据库中的主键(译者注:只有使application identity时才有这个必要),或者某个集合类型(Collection或子接口/类)中的元素是什么类型
存储管理器(PersistenceManager)
当你的类被增强后,你就可能使用PersistenceManager来存储你的对象了。要获取一个PersistenceManager,先要设置一些属性,通常包括下列信息:
。数据库连接信息
。JDO产品的类名
。默认的一些属性,包括连接池大小等等
附件3的第23行显示了如何设置一个Properties对象,从而获取一个JDO的PersistenceManagerFactory(相当于JDBC连接池中的DataSource),进而获得一个PersistenceManager对象(相当于JDBC中的Connection对象),之后,你可以用这个PersistenceManager对象来增加、更新、删除、查询对象(这也是我在后面将会讨论的)。当你的这些操作做完后,你需要关闭主这个PersistenceManager对象,以释放它使之能被再次使用(比如另一个线程)。
附件3的代码片断告诉你如果使用JDO来存储和查询对象。利用一个PersistenceManager类型的对象pm,你可以使用pm.makePersistent()方法来将一个新的对象保存到数据库(第68行)。一个对象仅仅在第一次出现的时候才需要保存到数据库(译者注:更严格一点,只在没有已存储的对象引用时才需要显式地调用makePersistent()方法),当它已经在数据库中存在以后,可以直接通过访问其属性来更新该对象的信息。所有的更新在当前的Transaction(数据库概念:事务)被提交时全部保存到数据库中。而如果你不希望保存主这些更改,只需要简单地rollback当前的Transaction即可(第15到17行)。类似地,你可以调用pm.deletePersistent()删除一个对象(第26行)。
要访问已经存储过的对象,可以简单地遍历其Extent(类的所有扩展),这是一个对该类对象的逻辑上的总称(第12到15行)。
如果你希望有选择地(而不是一古脑地)取出某个类的所有对象的一个子集,你可以创建一个Query(查询)。要做到这一点,调用pm.newQuery()来获得一个Query对象,并传入参数:候选对象集合和一个过滤器。候选对象集合可以是一个Collection或者一个类的Extent。过滤器是一个JDOQL(JDO Query Language)语句。当你创建了这个Query以后,就可以执行它来获得一个符合条件的集合(Collection,第22到26行)。JDOQL是JDO的查询语言;它有点象SQL,但却是依照Java的语法的。这里的例子只是一个简单的示范;使用JDOQL,你的过滤串可以写得很复杂。另外,如果你在过滤串中使用绑定的参数的话,你可以写一个简单的查询然后执行很多次,每次给出新的参数值。关于JDOQL有很多资料可以参考,参见本文的资源列表。
一个对象存储的例子
要找出JDO是否象Sun说的那么好,我会写一些代码,分别使用JDO和JDBC来存储我创建的Book对象(见附件4)。这个Book对象有一个name属性和一个Block对象。为使示例有趣一些,Book对象有一个限制:每本书由其名称唯一确定,也就是说,你不能加入两本同名的书。
一个Block(块)是Book的组成部分,它可以是Document、Chapter或Section。最顶层的Block是Document类型的,并且可以包括任意数量的Chaptor Blocks。每个Chaptor Block可以包括任意数量的Setion Blocks,因此这些Block中有一种嵌套的关系。在每一个Block中,我们用一个HashMap来存放任意数量的其它Block的键值对。
附件5列出了一个我的例子中做测试用的Book。这本书包括两章(Chaptor),第一章有一节(Section),还有第二章。特别地,第二章有一个属性:Color=Red。
用这个图书的例子, 我要实现以下一个存储功能:
。增加:看看我能否成功地向数据库中添加两本书,并且如果我用同样的名字加入第三本书,将会产生一个名称唯一性检查失败的异常
。更新:看看我是否能够更新一本书:增加一个属性“Comment”到它的根Block中。当我提交(commit)的时候,这些更新应该被保存下来,而如果我回滚(rollback),这些更新将被丢弃。
。删除:看看能否通过Query查询一本书然后从数据库中将它删除。
如果没有JDO, 我通常会这样设计:先设计几个相关的关系数据表来存储书中的数据,然后使用SQL和JDBC来存储/读取这些表。由于长度的关系,我不会在这里列出我的JDBC实现,不过如果感兴趣,你可以从JDJ 网站上下载。注意,为了通过JDBC/SQL实现以上这些功能,我必须写很长一段代码(480行!)。我现在要做的是让你看看用JDO来实现同样的功能会有多么的简单。
用JDO存储一个Book对象
用JDO来存储一个Book对象时,尽管我使用与JDBC方式中同样的Book主键类,但Block的ID属性已经完全没有必要。在JDBC方式中,必须用ID属性来在内部引用数据表中的不同的Block。但在JDO中,我根本不需要这个属性,因为JDO会自动地在底层处理它。
为了标明Book对象是需要存储的,我写了一个描述符来标记Book和Block类(见附件1)。在描述符中,我为Block类的children集合属性标明其元素类型是Block(第10到11行),而HashMap的键和值的类型都是String(第12到14行)。此外,由于ID属性并不是Block真正需要的,我在描述符中标明它不需要存储(第8行)。在Book的描述符片断中,我标明nm(名称)属性是它的主键(第5行),因此Book类需要使用自定义的标识类型(Application Identity),类名是BookKey(第4行)。BookKey类的代码见附件6。
在本例中,我使用的JDO产品是KodoJDO(采用关系数据库作底层)。市场上有很多JDO的产品(Implementation);你可以选择其中任意一种,而你的代码不需要作任何变化。底层的关系数据库我选择Enhyda InstantDB(一个Kodo产品附带的关系数据库)。JDO的精髓在于开发人员不需要知道某个JDO产品是如何将数据存入数据库的,所以我也不需要设计任何数据表,尽管我们底层使用的是关系数据库。Kodo提供了一个名为schematool的工具,根据我的描述符自动地创建需要的数据表结构。我所需要做的全部事情就是运行下面的命令来准备数据库底层(译者注:实际上,KodoJDO2.4.0以上版本就可以完成自动的数据库同步,这一步都可以省略。不过只建议在开发时使用):
schematool action refresh Book
schematool action refresh Block
下一步我正常地编译我的类,然后用Kodo的增强器(Enhancer),名为jdoc,来增强我的类代码(译者注:这些步骤可以采用Ant批处理工具来完成,大多数JDO厂商都提供Ant任务支持):
jdoc Book
jdoc Block
jdoc BookPersistJDO
这里,只要我将未增强的class文件(Book.class,BlockPersist.class,BookPersistJDO.class)和描述符放到jdoc能找到的位置,jdoc就会更改这些类代码,使之成为PersistenceCapable,而没有标记为可存储的类将会被增强为PersistenceAware的。在本例中,Book.class和Block.class会被改造为PersistenceCapable的,而BookPersistJDO.class会被改造为PersistenceAware的。(译者注:此处需要说明一下,多数意见提倡非存储的类只通过访问器(accessors,即get/set/is方法)去访问可存储对象的属性,这样,这些非存储的类根本不需要强化,以减少复杂性。)
增强了这些类之后,任何对我的对象进行操作的工作都可以通过PersistenceManager完成。用我前面提到过的那些代码,我可以很简单地获取一个PersistenceManager pm,然后用它来增加、删除、更新一本书。附件7显示了我的BookPersistJDO.java的代码片断。里面的方法addBook(第3行)示范了我如何通过JDO增加一本书,而deleteBook(第13行)示范了我如何删除一本书。
为通过名称来取一本书,我使用JDOQL过滤器创建了一个Query。执行这个Query后,我得到一个符合条件的对象集合(Collection)(第25到29行)。从中得到一个Book对象后,我直接更改其属性然后提交。
附件3显示了我的JDO方式的例子的测试结果。结果符合我的预期。首先,我成功地将一本书加入数据库,然后加了另一本同名的书,结果产生了一个JDOUserException,告诉我违反了名称唯一性的规则。接着,我可以更新一本书的信息,提交这些更新,或者通过回滚放弃这些更新。最后,我可以从数据库中通过名称找出一本书,然后将它删除。
两种版本的比较(JDBC vs JDO)
通过使用JDO和JDBC来解决同样的问题,我观察到以下内容:
1、采用JDO的话,我可以做到与JDBC同样的事。我可以通过JDOQL来查询对象;通过标记Book类的nm属性为主键来保证书的名称唯一性;另外可以增加、删除、更新这些对象。
2、JDO使我的Transaction处理更加容易。在JDBC方式中,因为一本书实际上被对应到四个不同的关系数据表中的许多条记录,我必须保证所有的插入和删除操作在同一个Transaction中完成。相反,JDO只需要一个操作就可以保存所有这些变化,而我不需要使用Transaction来维护这个操作的原子性。
3、BookPersistJDO.java只有140行,比BookPersistJDBC.java(480行)少很多,这些事实都说明JDO使我的代码少了很多。尤其是我的类之间关系有嵌套的特性使得在关系数据库中的表结构比较复杂。在我的JDBC实现中,我不得不花很多心思来设计我的数据库,从而存储和获取这些嵌套的数据。我不得不用一个ID字段来表示每一个Block,并使用一个外键来表达父/子关系。在JDO实现中,我根本不需要考虑这些问题,一切都被正确地存储下来。
4、我为了提高性能而在JDBC实现中加入的缓冲机制在JDO中并不需要,因为JDO产品一般都包含了性能优化和缓冲。这些也节约了我很多工作量,因为我不需要担心我的缓冲是否始终与数据库保持同步。
在这些区别之外,JDO产品和我的JDBC实现可能没有多大区别。比如,这个JDO产品,使用了一个关系数据库,可能也采用类似的表结构设计和ID产生机制,并且用JDBC来存储数据。然而,关键在于我不需要了解这些实现上的细节:这些负担被转移到具有专业水平的JDO厂商身上,也正是这些人更希望在这些细节上做得更好。另外,这些厂商也可以自由地选择在其它类型的存储技术中实现存储,比如对象数据库或文件等等,这些给我们在选择具体存储方式上在有很大的灵活性。
结论
JDO提供了很多好处:
。它具有所有必须的数据存储功能:增、删、改、事务、数据唯一性、缓冲
。它免除了开发人员的一大堆繁琐的工作,使代码更易读和易维护
。它独立于具体的厂商,防止了厂商依赖性
。尽管我的代码没有显示,它可以在任何数据仓库中工作,使开发灵活而可移植
JDO是一种值钻研的技术。本文只是你的一个起点;更多的信息请看下面的资源。
资源
。一站式的网站:
。规范:
。Roos, R. (2002). Java Data Objects. Addison Wesley出版社: (译者注,我有该书作者给我的pdf版本,可以与大家共享,但切勿作商业用途)
。本文例子用到的JDO产品:SolarMetric: (译者注:KodoJDO是目前做得最好的产品,性能方面优化特别多,尤其是最新的2.5.0beta版本。该产品的开发团队基本上都来自于麻省理工大学。KodoJDO目前的缺点是不象南非的HemiSphere公司的JDOGenie产品一样,有独立的描述符编辑工具,只能搭配JBuilder7以上使用。不过在我的建议下,他们准备在3.0版本中加入这样的工具。)
JDO的属性配置
附件2提供了我后面用到的属性文件。我用了一个关系数据库,第14行包含了数据库连接的信息。第6行包含我用到的JDO产品的类名(译者注:即厂商对PersistenceManagerFactory的实现类)。第8到10行是对PersistenceManager的默认设置,标明我使用Optimistic Transaction,以及数据在rollback时可从缓冲中恢复,在提交时不保留这些值(译者注:目的是保证有些数据库中的触发器产生的更新可以再从数据库中读回来)。还有很多的细节,可以设置进行Transaction时采用哪些选项。请阅读JDO的API来找到它们的含义,并在需要的时候使用。
JDBC实现
这里我简单地描述一下我如何通过JDBC存储Book类。知道这里面包括多大的工作量后,你会更加理解JDO为你所做的一切。
我创建了四个表来存储Book(见图1)。为保证数据唯一性,Book表的nm字段标记为单一索引(译者注:实际上就是标为主键)。为跟踪每个块(Block)的属性和其子集,我为每个Block增加了一个blockId字段。这个BlockId字段在每个Block加入数据库的时候产生(在已经存在的最大的Block表中的blockId值的基础上加1)。
增加一本书涉及到将一个Book对象的信息分解为四个表:Book,Block,BlockRelation,以及BlockAttr。特别地,每个Block需要生成一个blockId。进一步,最麻烦的是Blocks是嵌套/递归的,我必须写入递归的代码将这些数据插入到数据库。
在附件5创建Book的例子中,这些数据表显示在图2中。你可以看到,一本书被转换成四个表中的很多行,因此,所有的插入操作都必须包装在一个Transaction中,这样才能保证一本书被完整地加入或完整地取消,永不残缺。同样的,删除一本书涉及到从四个表中找出所有合适的需要删除的行,并在一个Transaction中删除以保证这本书被完整地删除或取消删除。
为了更好的性能,我并不想每次有人需要查询一本书的时候都递归地从四个表中取出数据。我创建了一个BookCache,在应用开始运行的时候一下子从数据库中取出所有的书。当我增加、删除、更新这些书的时候,我的代码保证BookCache与数据库是同步的。所有这些工作都在这个cache中完成,使得取一本书只需简单地在cache中按名字查找。
仍不完美的地方
撇开以上谈到的这些优点,还有一些地方是JDO做得还不够的:
。它对新开发的项目很好,但如果要将现存的关系数据库也使用JDO,就需要多做一些映射工作(编辑描述符)
。对开发人员来说,采用JDO后,我们不再需要处理底层的数据库访问,所以在性能优化上可能比原来难一点。因为JDO必须做很多额外的工作,比如跟踪对象属性的变化以同步内部的缓冲等等,JDO产品做得如何对性能至关重要
。JDOQL还不支持聚合操作,比如max, min, sum这些SQL能做到的事。(译者注:在JDO1.0规范的第24章详细描述了下一步2.0规范应该做到的内容,其中包含这些)
。如果能在编译时就能找了JDOQL的错误就好多了。比如在一个过滤串中,你通过一个字符串来指定一个属性名,你很容易写错名称,但编译不会出错,运行时才出错。(译者注:SQL也存在同样问题)
。一些人认为JDO的好处在于你不需要再写SQL,但真实的情况是你必须学习JDOQL。
作者介绍
Teresa Lau已经作为独立的Java顾问五年多了,重点在金融系统上。她拥有计算机硕士学位,目前工作在纽约。
译者注:
本文的版权属于笔者本人,但欢迎转载,前提是注明出处和原作者。另外,欢迎在我的专栏中查看我的另几篇文章,并提出宝贵意见!
下载本文示例代码
一个纽约女技术员的JDO经验一个纽约女技术员的JDO经验一个纽约女技术员的JDO经验一个纽约女技术员的JDO经验一个纽约女技术员的JDO经验一个纽约女技术员的JDO经验一个纽约女技术员的JDO经验一个纽约女技术员的JDO经验一个纽约女技术员的JDO经验一个纽约女技术员的JDO经验一个纽约女技术员的JDO经验一个纽约女技术员的JDO经验