如何用JDO开发数据库应用
(本文的版权属作者本人,欢迎转载,但必须注明出处和原作者)
本文将介绍如何使用Sun公司的最新《Java Data Objects 》规范来进行基于数据库的简单应用程序的开发,从而使读者对JDO有一个直接的感性的认识,为更深入的开发作铺垫,同时也希望抛砖引玉,让更多的富有经验的高手也参与到推广JDO的进程中来,为读者提供更多更精彩的文章!
1. JDO是何方神圣,难道是ADO的翻版?
本节对稍熟悉JDO一点的读者来说,可能算是老生常谈,一堆垃圾,不如回收掉算了。不过我却认为这些是实话实说,有感而发,不吐不快,对新手可能也有一定的帮助,至少应该有一点共鸣吧。所以,老手请直接跳过本节。
1.1. Java的优点
自从Java语言面世以来,它那几乎完全面向对象的特性和解放我们程序员的自动垃圾回收机制给我们展现了一个全新的开发天地:原来程序还可以这样写!我用过几年C++,里面的指针简直折寿!我还记得有些功能里面不得不使用类似“***lpszInfoMapOfMap”之类的变量,它是指针的指针的指针,要在编码过程中准确地把握这一点已属不易,何况还要记得释放每一处占用的内存,并且还不能释放多次(严格地说,应该是将自己申请的内存进行且只进行一次释放)!我至今都还很佩服当年清晰的头脑,然而在调试过程层出不穷的“AccessViolation”和“NullPointer”错误竟使我一夜白头!(有一次熬夜调试一个问题,第二天憔悴了很多。)C++之后,我也用过三年以上的Delphi,程序代码好理解、易维护了很多,不过指针仍是胸中永远的痛!直到Java,才使我脱离苦海,进入“按思维的速度进行开发”的时代……
当Java的速度得到很大改善后,我们开始用它来写数据库应用,但说实话,Java的数据库方面还很原始,图形界面编程中的数据库组件很不好用,再加上主要写的是Web应用,只有JDBC接口可供选择。提起JDBC,我相信很多读者都会有这样的印象:概念太多,严密但麻烦,尤其是资源的释放也是一大问题。比起微软的ADO来,简直是一团乱麻,容错性尤其差劲。
1.2. 对象包装技术,百家争鸣?群魔乱舞?
于是,从规范化开发的原则出发,我们开始写自己的JavaBean来包装数据对象,使数据对象化,避免太多的地方涉及JDBC操作。但一些问题也随之而来:灵活性不够,接口死板,性能低下,这使我一阵苦恼。于是,“君子性非异也,善假于物也”,咱也上网去找点“技术支持”!很快,竟然被我发现了“Castor JDO”,一个专用于数据包装的撞阕榧峁┝薕DMG标准的OQL作为查询语言,方便且容易理解,比SQL好多了。这让我享受了一段时间的“面向对象的数据库开发”的好处,一句话,“效果不错,还实惠!”。
然而,好景不长,Castor一些内在的BUG影响了稳定性,而这个免费产品的更新又太慢,一直未能解决。只好放弃。“执手相看泪眼,竟无语凝噎”!怎么办?要知道,由俭入奢易,由奢入俭难,吃过肉的人,怎能忍受只能吃菜的生活!象《甲方乙方》里面那个一心想吃素的大款还是不多见的。对我们来说,再使用JDBC原始调用似乎难以下咽,再用JavaBean包装又有点返古,于是我又开始了网上的搜寻历程。余秋雨先生有《文化苦旅》,咱这也算是《编程苦旅》了,呵呵,苦笑。
从网上的资料来看,我的这些经历也是很多Java开发同仁的共同经历,无论是国内还是国外,不过从实际情况来看,国外的研究更深入更广泛一些,至少从网上所能找到的资料来说是这样。美国从八十年代起就开始研究面向对象的数据库ODBMS,目前已有一些成形的产品,比如Versant公司的Versant数据库,FastObjects公司的FastObjects t7数据库,以及其它一些相对市场份额小一些的诸如ObjectStore等公司的产品,当然,也不乏一些免费的产品,如Orient等等。总的来说,ODBMS尽管拥有面向对象的优点,但由于历史原因,在与关系数据库RDBMS的竞争中始终处于下风,基于RDBMS的应用还是占绝大多数,因此,出现了Object-Relational映射的一些工具,前面提到的Castor就是近年来出现的一个工具,实际上更早的时候,已经有一些成熟、稳定的商业化产品出现,比如前一阵被Oracle收购的TopLink,被BEA收购的WebGain等等,比较有名气的CocoBase等等。
象TopLink这样的产品我也了解了一下,功能确实强大,性能、稳定性都有优势,然而,其同样强大的价格和古怪的API令我却步。我很担心被锁定在某个产品上面,无法脱身,众所周知,Java给我们的就是一种自由的感觉,自由,永远是那么地吸引人。
出路在哪里?JDO浮现在我眼前。
1.3. JDO浮出水面,可别以为是ADO
JDO自1999年起就由一些经常写数据库对象映射层的富有经验的开发人员提出大纲,他们在长期的面向对象开发中进行了大量的数据库方面的处理和对象化包装,终于,多种多样的包装方式引起很多兼容性方面的问题。于是,一些主要的开发团队就联合起来,以SUN为领头羊,制定了JDO规范。它的目标不是取代JDBC或EJB,而是在JDBC的基础上进行包装,同时又可以做EJB的底层(CMP),简化J2EE服务器提供商的工作。JDO主要面向中小型规模的项目,不过随着产品提供商(Vendors)给出越来越多的功能(Feature),比如分布式的同步控制等等,JDO的作用也越来越大。
JDO规范在Sun的富有经验的Craig Russel的带领下,经过三年的讨论,终于在2002年四月形成了第一版。目前最新版是1.0.1版,在 http://access1.sun.com/jdo/ 可以看到。
(大家应该知道,Java的规范形成时间一般都比较长,因为它太开放了,任何人都可以发表意见,参与制定规范的人都要考虑这些意见)
ADO是微软的数据访问组件集合,相信很多写过基于ASP页面的数据库应用的朋友都印象深刻,快速、容错性强是它的特点,不过扩展性就不敢恭维,写多少年都是那几个东东,无法超越。一些刚接触JDO的读者可能会将JDO与ADO搞混起来,以为是Java版的ADO,那就大错特错了,两者风马牛不相及,可以说不是一个档次上的东西。庆幸的是,微软没有将“xDO”这类缩写注册成商标或专利,否则两个大块头又要打官司了,嘿嘿,我倒是喜欢看热闹,打起来同样精彩!
1.4. JDO产品介绍
JDO规范自从2002年4月推出以来,出现了很多种各有特色的产品,当然,这些产品都遵循JDO规范,不会影响你写的JDO应用的可移植性。下面列举一下我对各个产品的印象:(参见第6节的参考文章:《JDO资源介绍》)
- 教父:LiDO(法国LibeLis公司)
我对JDO的认识主要是通过LiDO这个产品,它在2002年3月的一份图文并茂的教程中简要解说了JDO的使用和优点。这个教程可以在这里下载:http://www.objectweb.org/conference/JDO.pdf。LiDO的特色是大而全,支持文件型数据库、RDBMS、ODBMS,甚至是XML数据库。不过配置较麻烦。最新版本是2.0RC。 - 霸主:KodoJDO(美国SolarMetrics公司)
Kodo是JDO的中流砥柱之一,在JDO1.0还未最后通过的时候,它就是一个比较成熟的产品了,其特点是注重性能和稳定性,目前最新版本是2.5.0,是客户最多的产品。 - 最佳传教士:JDOGenie(南非HemSphere公司)
这是目前我最推荐的产品,最新版本是1.4.7,性能也不错,稳定性还有待验证,但它有一个最大的特点:集成性好,最易学,其公司的CTO David Tinker也是一个善解人意的年轻人,采纳了很多网友的意见对产品进行改进,主要是在配置上非常方便,有一个专门的图形界面工具,可以进行配置、数据库生成、对象查询等等很实用的功能。强烈推荐! - 大家闰秀:JRelay(德国ObjectIndustries公司)
这也是一个出现得比较早的产品,也有一个GUI工具用于配置,曾几何时,这个工具还是相对很方便的,但一年多过去了,好象没什么进展,最新版本是2.0,我试过一段时间,后来就没有再跟进了。 - 两面派:FrontierSuite for JDO (美国ObjectFrontier)
这个产品与JRelay、Kodo一起,可算是早期的JDO三剑客,称它为两面派是因为它正向开发和反向开发都还可以。它的特色是反向工程(从表结构生成数据类)比较方便,与UML的结合也很强,不过真正运行起来的时候,配置复杂。当初该公司曾许诺我以10%的价格买一份,可惜我当时没在意。如果上天再给我一次机会…… - 免费午餐:TJDO(一群跨国界的有志之士)
这是一个在Sun提供的参考产品(Reference Implementation)的基础上加入一些扩展功能而形成的一个免费产品,目前最新版本是2.0beta3,不过进展也缓慢,这个版本已经出现好几个月了没有进一步的更新。
- 教父:LiDO(法国LibeLis公司)
以上这些是我用过的比较有代表性的产品,还有很多商业产品,以及其它一些或规范或不完全规范的免费JDO产品(如XORM、OJB等),这里不再一一列举,有兴趣的读者可以到 http://www.jdocentral.com/ 去进一步了解。
2. JDOGenie是哪路英雄
前面已经提过,JDOGenie是南非的一个商业化产品,可别小看南非人,他们的收入可不少!当然技术也不错。如果读者做过UML的建模和设计,可能会知道Together Control Center这个产品,也就是最近被Borland收购的一个UML设计工具。JDOGenie的出品公司HemSphere就是Together的南非总代理和合作伙伴。
说到这里,插句题外话,IBM收购了Rational,Borland收购了Together,Sun收购了DescribeUML,一场IDE+UML的大战又将上演。
好,书归正传,JDOGenie是我目前最推荐的产品,原因是易学易用,简单上手,对于想学习JDO的朋友是最适合不过的了!它有一个图形界面的配置工具,可在里面进行数据表映射、SQL操作、JDOQL查询等等功能,非常方便。对采用JDOGenie的Web服务器也可以通过这个图形工具进行监控,可以了解哪些查询费时,哪些查询执行次数多等等,有助于数据库优化调节。
俗话说,百闻不如一见,下面先给几张图片,过把瘾先:
控制台界面(WorkBench):
以下的内容是以JDOGenie为底层来介绍JDO的开发流程的,所以需要先下载JDOGenie:
先到http://www.hemtech.co.za/jdo/download.html下载最新版本(本文使用的是1.4.7),然后点击该页面上的“Obtain Evaluation License”链接获取一个月的试用License(一个月会不会太少了?放心,该公司在快到期时会发布一个新的License的)。在获取试用License的时候需要填写一些资料。
3. 我们要做什么--需求描述
本节主要描述本文中将要做的应用程序的功能。这个应用程序非常简单,是基于一位网友提出的一个《银行信用卡交易系统》中提取出来的一个功能子集,并作了一定的简化和功能改动,以便能体现JDO的特点,主要完成以下功能:
- 录入信用卡资料。
信用卡资料包括以下信息:- 卡号(自动生成)
- 持卡人姓名
- 身份证号
- 地址
- 联系电话
- 开户日期(自动生成)
- 开户金额
- 目前余额(自动计算)
- 允许透支额
- 浏览信用卡信息
浏览当前所有的信用卡资料 - 交易刷卡
针对信用卡产生一次交易,需要录入以下信息:
卡号、交易额
如果超出透支额,则提示无法完成交易。
如果交易成功,系统自动记录交易时间 - 存款
录入卡号和存款额 - 查询透支报表
查看当前所有透支的信用卡 - 查询交易明细
可根据持卡人身份证号查询所有的交易信息
以上就是整个应用的功能,非常简单,如果采用JDBC,我们立即会想到先建两个表:信用卡表和交易表,二者通过卡号关联,然后这些功能就是一堆SQL和这两个表组合而成的大杂烩。然而,我们现在要采用JDO来做,怎么做呢?请继续往下看……
- 录入信用卡资料。
4. 开发过程
刚才看上面这一段功能需求的功夫,JDOGenie也应该down下来了,如果还没有的话,感紧安装一个ADSL吧!(别乱猜,我可不是电信的职工!咱只有羡慕的份……)。
采用JDO进行开发的过程大致如下:
- 先编写原始的数据对象模型代码并编译成为.class文件,这些数据对象称作原始对象(POJO,Plain Old Java Objects)
- 然后编写存储描述符Metadata,也称元数据,表明一些与存储相关的设置,比如哪些类需要保存到数据库,每个类中的哪些字段需要优先读入,哪些字段延迟读入(LazyLoad)等等,这个metadata必须放到CLASSPATH中,扩展名是“.jdo”。
metadata的编写一般可以通过工具来完成,比如JDOGenie的工作台(WorkBench)就可以轻松地完成。 - 写完描述符后,就采用某个JDO产品的增强器(Enhancer)来根据元数据的描述改造编译生成的.class文件,使这些类代码JDO化,然后就可以在其它代码中调用了。应用代码中主要通过JDO存储管理器(javax.jdo.PersistenceManager,以后简称PM)来完成对数据对象的增加、删除、修改、查询等操作。
示意图如下(摘自Versant公司的JDO教程):其中的XML Config即是指*.jdo
4.1. 如何建模
当我们将自己的头脑OO化之后,以上的需求在我们头脑里变成了几个基本的对象:信用卡和交易记录,以及对这些对象进行的一些操作。(其实我们的头脑本来就是基本客观世界的对象的,应该说本来就是OO的,要不然大脑怎么会是一些环成“O”状的肠子呢?:D)
为简单明了,我们这里的建模也不采用类似ROSE或Together这样的大型的UML工具了。在Duke或Quake中,有时候步枪,甚至是电锯,会成为最有效最直接的杀人工具,而CS里面手枪也往往出奇制胜。现在,在程序开发的战场上,我们祭出最原始的利器:记事本!相信没有一个人不会使用它。简单点说,我们下面的内容都直接以源代码作为建模的说明。
首先,我们分析信用卡这个类,很简单,将前面列出的字段作为属性加到类中即可,这和建表的过程其实差不了多少(只是碰到对象之间的关系时,思路会有不同)。
package credit.system; import java.util.Date; public class CreditCard { String name; //姓名 String address; //地址 String idcard; //身份证号 String phone; //电话 Date createTime; //开户日期 Date lastTransactionTime; //最近一次交易的日期 float initialBalance; //开户金额 float balance; //目前余额 float allowOverDraft; //允许透支额 }
咦,好象有什么地方不对劲?不错,你的眼光真犀利!“我搞了这么多年数据库应用开发,从没见过没有关键字的表,也没见过这样一个没有标识字段的类!卡号哪儿去了??!!”
是啊,卡号哪儿去了?没有卡号的信用卡谁敢用?趁早卷铺盖回家吧!
别急,这里先给大家介绍一下JDO的一个关于对象标识的概念:标识实际上只是一个对象的唯一标记,有点象一个对象的内存地址,对传统的数据库来说,就是一条记录的主键。JDO认为,如果关键字只是用于标记一个对象的唯一性,而不参与业务逻辑(比如计算),则不必将它放到类代码中,这种唯一性的维护只需要由JDO中间件(Implementation)去完成,这种对象标识叫做Datastore Identity,一般实现上是使用递增整数;如果标识也参与业务逻辑(如主键是创建时间,会用于排序或范围查找),则可以在类代码中出现,这种对象标识叫做Application Identity。关于这些概念,请参考本文尾部参考文章中的《JDO对开发的帮助有哪些》一文。
在上面的信用卡类中,我们认为信用卡号只是对信用卡的一个标识,不参与业务逻辑,所以我们采用Datastore Identity的方式,让标识的唯一性由JDO产品去维护,就象对象在内存中的地址不需要我们在程序代码中指定,而是由JVM去维护一样。
咦,好象又有什么地方不对劲?不错,你的眼光还是这么犀利!“你的信用卡没有标识,那我的交易记录怎么去关联它??!!”
对啊,以前我写的JavaBean包装的数据对象,也需要有一个主键属性,另一个对象通过一个同样类型的属性来与这个对象关联,现在你这个主键属性都没了,我怎么去关联呢?无的放矢?
这个问题问得很好,也非常典型(注意,是非常典型,不是“非典型”)。
不过问这个问题的人,应该都是写过多年数据库应用的富有经验的开发人员,数据表、主键、外键关联的意识已经深入头脑,就算变成Java类,主键外键还是阴魂不散。这种方式可谓“换汤不换药”,没什么实质的变化,这样的对象模型也不能体现出对象之间的关系,只能通过程序员自己去把握。说实话,我最初也是这样去做对象包装的,惭愧惭愧,现在让我们步子再大一点,观念再开放一点,看看JDO中的概念吧:对象之间如果有关系的话,只需要直接将关系到的对象声明为一个该类型的属性(或属性集合)即可。
这样,我们的交易记录类就写成了下面的样子:
package credit.system; import java.util.Date; public class TransactionRecord { Date createTime; //交易发生时间 float amount; //交易金额 String note; //备注 CreditCard card; //信用卡 }
在这个类中,我们看到里面没有一个“信用卡号”的属性,取而代之的是一个信用卡对象“card”,这样,我们就不会需要在通过交易记录取得相关信用卡的时候去调用一条查询语句来取得信用卡对象了,只需要简单地读取这个交易记录对象的card属性即可得到。这也是面向对象的便捷性之一。
有了这两个类,我们的《银行信用卡交易系统》的基础也就搭起来了。
4.2. 如果怕出乱子……建议的代码规范
记得以前听过一句话,“世间永恒不变的真理就是不存在永恒不变的真理”,这里,我也想说一句:编程世界里最完美的解决方案就是不要认为有最完美的解决方案。(说什么啊,简单听不懂。呵呵,我自己也有点听不懂)。
我想说的是:JDO也是有一定的限制的,不能让你完全地展开双翅(注意,是“鱼翅”),在面向对象的大海中遨游。为什么JDO会有限制呢?因为它的原理是将你的类代码进行一定的改造,将JDO涉及的一些管理和维护代码插入到类代码中,这样,你的调用代码可能需要进行一些改变。这些就是JDO的限制。简单地说,如果你得到了一个TransactionRecord类型的对象tr,想通过它取得涉及的信用卡对象,不建议通过下面的代码:tr.card得到,而是建议将这个属性声明为private的,然后给出一个getter来获取(getCard()),也就是进行JavaBean式的属性包装。这样,我们的两个数据类就会变成下面的样子:
CreditCard.java: package credit.system; import java.util.Date; public class CreditCard { String name; //姓名 String address; //地址 String idcard; //身份证号 String phone; //电话 Date createTime; //开户日期 Date lastTransactionTime; //最近一次交易的时间 float initialBalance; //开户金额 float balance; //目前余额 float allowOverDraft; //允许透支额 public String toString() {
return "信用卡:余额="+balance+",持卡人="+name+",身份证号="+idcard+",电话="+phone;
}
public void setName(String value) { name = value; } public String getName() { return name; } public void setAddress(String value) { address = value; } public String getAddress() { return address; } public void setIdcard(String value) { idcard = value; } public String getIdcard() { return idcard; } public void setPhone(String value) { phone = value; } public String getPhone() { return phone; } public void setCreateTime(Date value) { createTime = value; } public Date getCreateTime() { return createTime; } public void setLastTransactionTime(Date value) { lastTransactionTime = value; } public Date getLastTransactionTime() { return lastTransactionTime; } public void setInitialBalance(float value) { initialBalance = value; } public float getInitialBalance() { return initialBalance; } public void setBalance(float value) { balance = value; } public float getBalance() { return balance; } public void setAllowOverDraft(float value) { allowOverDraft = value; } public float getAllowOverDraft() { return allowOverDraft; } } TransactionRecord.java: package credit.system; import java.util.Date; public class TransactionRecord { Date createTime; //交易发生时间 float amount; //交易金额 String note; //备注 CreditCard card; //信用卡 public String toString() {
return "交易记录:持卡人="+card.name+",身份证号="+card.idcard +",交易额="+amount+",时间="+createTime;
}
public void setCreateTime(Date value) { createTime = value; } public Date getCreateTime() { return createTime; } public void setAmount(float value) { amount = value; } public float getAmount() { return amount; } public void setNote(String value) { note = value; } public String getNote() { return note; } public void setCard(CreditCard value) { card = value; } public CreditCard getCard() { return card; } }
实际上,这些增加getter和setter的过程有很多工具可以帮忙,比如JBuilder,或者是Together,甚至是最小而精的免费工具Gel!
这样的采用访问器包装私有属性的建议,一般来说绝大多数Java开发人员都还是可以接受的。
4.3. 编辑metadata: system.jdo
这个过程,我们完全可以通过JDOGenie带的图形工具来完成。
我们将上面两个类编译以后,打开JDOGenie的workBench,即运行JDOGenie1.4.7解包后的/workbench.bat,如果是在Unix或Linux下就运行workbench.sh。注意要求你预先设置一个环境变量:JAVA_HOME,指向系统安装的JDK的根目录(不是bin目录)。
我们在其中新建一个project(File-->New Project),选择前面编译生成的类代码所在的根目录中(以下简称CLASSPATH,两个.class文件应该在该目录的credit/system/子目录中)存放这个project,因为这个project实际上是一个配置文件(注意不是.jdo文件),一般包含一些JDO产品相关的信息,比如是否打开某些扩展功能,License号是多少等等,这个文件在运行时是需要的。我们选择一个project名:creditSys,JDOGenie会在CLASSPATH中将这个project保存为一个名为“creditSys.jdogenie”的文件。
我们先设置数据库,为保证实用性,我们选择MySQL作为底层数据库,安装MySQL的过程很简单,从MySQL网站下载4.0.13版本,安装到系统中,然后启动mysql服务即可。我们会采用其安装后自动生成的“test”数据库作为本文的数据库。
我们还需要在MySQL网站上下载jdbc驱动:版本号是3.0.7,下载后,将其ZIP包中的mysql.jar文件解出来放到某个目录中备用。
设置数据库的界面如下:
配置好数据库后,可以点击下面的“Test”按钮测试一下连接是否正常。可能你会看到一个找不到MySQL的JDBC驱动的错误提示,没关系,我们直接点击OK,进入project属性配置界面,在其中将JDBC驱动加入,然后再回来测试连接即可。
project属性配置界面基本上不需要怎么设置,只要做两个必须的配置:
- 将MySQL的JDBC驱动加入到CLASSPATH中
- 将我们的类代码的根目录加入到CLASSPATH中
界面如下:
点击“OK”后,我们就到了主窗口,在主窗口中,我们可以进行metadata的编辑工作。我们通过菜单Meta-->Add Classes将前面的两个数据类加入到元数据中,表示这两个类是需要存储的。其余的数据表结构等等繁琐的事务,我们全部留给JDOGenie自动完成。添加数据类的过程中需要创建一个元数据文件,根据JDO的标准,一般情况下我们只需要在CLASSPATH根目录下创建一个“system.jdo”即可。加入数据类后的界面如下:
而自动生成的元数据文件system.jdo内容非常简单,实际上直接用手工写也不难:
<?xml version="1.0" encoding="UTF-8"?> <jdo> <package name="credit.system"> <class name="CreditCard" /> <class name="TransactionRecord" /> </package> </jdo>
不过如果类比较多,之间关系也比较复杂的时候,就最好通过工具完成,以免出现语法和语义错误,除非你已经很有经验。
之后,我们选择菜单的Build-->Recreate Schema来创建相应的数据库。数据库的表结构是自动生成的,如果你对其中一些表名或字段名或字段长度有异议,可以在主窗口中自定义。这里为简明扼要,全部采用自动生成。如果你想在建表之前看看数据结构,可以选择菜单“Build-->View Schema”先预览一下生成的表结构SQL代码:
-- credit.system.CreditCard create table credit_card ( credit_card_id INTEGER not null, -- address VARCHAR(255), -- address allow_over_draft FLOAT, -- allowOverDraft balance FLOAT, -- balance create_time DATETIME, -- createTime idcard VARCHAR(255), -- idcard initial_balance FLOAT, -- initialBalance last_transaction_time DATETIME, -- lastTransactionTime nme VARCHAR(255), -- name phone VARCHAR(255), -- phone jdo_version SMALLINT not null, -- constraint pk_credit_card primary key (credit_card_id) ) TYPE = InnoDB; -- za.co.hemtech.jdo.server.jdbc.sql.HighLowJdbcKeyGenerator create table jdo_keygen ( table_name VARCHAR(64) not null, last_used_id INTEGER not null, constraint pk_jdo_keygen primary key (table_name) ) TYPE = InnoDB; -- credit.system.TransactionRecord create table transaction_record ( transaction_record_id INTEGER not null, -- amount FLOAT, -- amount credit_card_id INTEGER, -- card create_time DATETIME, -- createTime note VARCHAR(255), -- note jdo_version SMALLINT not null, -- constraint pk_transaction_record primary key (transaction_record_id) ) TYPE = InnoDB;
接下来,我们保存这个Project,也就是将配置信息写入“creditSys.jdogenie”。下面我们就可以继续开发了,也就是说,我们所有的数据类包装工作就全部完成了,已经可以享受JDO的自动维护的对象存储和灵活的面向对象的JDOQL查询语言的好处了。
JDOGenie1.4.7还有一个好处,是它新增的UML类图功能,简洁明了地给出数据类之间的关系,对于理解别人的数据模型非常有用。选择菜单“Meta-->Diagrams”,将所有的类都加到图中,即可看到本文中的数据模型:
如果类比较多的话,可能这些关系线段会有交叉,那就需要我们手动地调整一下各个类的位置,做到尽量减少交叉。一个复杂一点的类图示范如下(未调整位置):
4.4. 增强我们的数据对象类代码
配置信息写完了,metadata也生成好了,下面就是用JDOGenie的类代码增强器增强我们编译生成的类代码。在这里,由于需要配置一些CLASSPATH、JDO文件路径等,我们写了一段Ant脚本来完成这些麻烦的工作。
这个Ant脚本保存在我们的项目根目录下,名为“build.xml”:
<?xml version="1.0" encoding="GB2312" ?> <project name="creditSys" default="enhance"> <property name="src" location="src" /> <property name="build" location="classes" /> <property name="jdbc.jar" location="d:/bak/lib/mysql.jar" /> <path id="cp"> <pathelement path="${build}"/> <pathelement path="${jdbc.jar}"/> <fileset dir="." includes="*.jar" /> </path> <target name="cleanup" description="清除所有编译生成的class文件"> <delete> <fileset dir="${build}" includes="**/*.class"/> </delete> </target> <target name="compile"> <javac debug="true" destdir="${build}" srcdir="${src}" classpathref="cp"/> </target> <target name="enhance" depends="cleanup, compile"> <taskdef classpathref="cp" resource="jdogenie.tasks"/> <jdo-enhance project="${ant.project.name}.jdogenie" outputdir="${build}" /> </target> <target name="run" description="运行credit.system.Main类,测试功能">
<java classname="credit.system.Main" classpathref="cp" fork="true" />
</target>
</project>
这样,我们需要增强类代码的时候,在DOS框中,在build.xml所在的目录下运行“ant”,即可完成增强过程。另外,要运行测试程序类credit.system.Main的话(后面会讲到),只需要运行“ant run”即可,这样可以解决没有类似JBuilder的IDE的问题。
4.5. 编写业务逻辑方法:永远不能回避的任务
业务逻辑,好象在面向对象编程中常常提到,但到底什么是业务逻辑呢?难道仅仅是为了显示自己很高深,所以张口闭口就业务逻辑?确实有这样一些人,谈论开发的分析与设计时,脱口而出就是“业务逻辑”,实际上可能他们自己也不知道到底什么才是业务逻辑。
我个人的理解可能也不是很充分。我是这样理解的:在应用中与存储无关的对象之间的协作(即属性的关联性的变化)就是业务逻辑,一般体现为一些规则,比如:当新生成一个交易时,就将信用卡的总交易次数加1,将系统的总交易次数也加1。类似这样的规则就是业务逻辑,这些是每个应用特定的,是工具无法自动为你实现的,必须在你自己的应用代码中体现,不论是作为Java代码,还是作为数据库存储过程,总之,必须有一段代码来体现这些业务逻辑。
采用JDO解决了存储的问题之后,我们就可以编写工具类(独立于数据类的包含业务逻辑方法的类)来完成需求中描述的功能了。注意我们在下面的代码中用到的JDO API。具体API的使用参见JDO文档以及本文末的参考文章中的《Java Data Objects第一章翻译》。
在完成第一个功能需求之前,我们先建立一些基础代码以将JDO集成起来。我们写一个名为“credit.system.Main”的工具类(也可称为控制类),来提供这些基础的方法:
package credit.system; import javax.jdo.*; import java.util.*; public class Main { public static void main(String[] args) throws Exception { System.out.println("开始测试功能……"); } /** * 本方法专用于获取JDO API的核心对象:存储管理器PersistenceManager */ public static PersistenceManager getPersistenceManager() { if(pmf == null) { java.util.Properties p = new java.util.Properties(); try { //从配置文件读入配置信息 p.load(Main.class.getClassLoader().getResourceAsStream("/creditSys.jdogenie")); } catch(IOException ex) { throw new RuntimeException(ex); } pmf = JDOHelper.getPersistenceManagerFactory(p); } return pmf.getPersistenceManager(); } private static PersistenceManagerFactory pmf; }为能让我们的代码能够编译并运行,我们还需要JDO的API包和JDOGenie的支持包,这些支持包都被合成到JDOGenie的lib目录下的“jdogenie.jar”文件中,还有一个JTA包也是JDOGenie运行时必须的,即其lib目录下的jta.jar。我们将这两个jar文件拷贝到我们的项目目录下,以便与JDOGenie的目录划清界限,另外,还需要将JDOGenie的license目录下的“jdogenie.license”文件(也就是通过邮件获取的license文件),放到我们的CLASSPATH中。当然,也可以将该license文件和JTA包一并压缩到jdogenie.jar中,我就喜欢这样做,这样可以使对JDOGenie的依赖浓缩到一个jar文件中,方便在不同JDO产品间切换(因为我一般会让一个应用在不同的JDO产品下工作,以保证规范性和兼容性)。
4.5.1. 录入信用卡资料
为了放置业务逻辑方法,为简便起见,我们将在Main类中用静态方法来完成这些业务功能。首先,我们写一个方法来完成添加信用卡资料的功能:
/** * 录入新信用卡,只需要录入必须的资料,其它的信息自动产生。 * @return 生成的新信用卡对象 * @throws IdCardDuplicatedException 身份证号重复,不允许创建新卡 */ public static CreditCard inputCard( String name, String address, String idcard, String phone, float initialBalance, float allowOverDraft ) throws IdCardDuplicatedException { CreditCard cc = new CreditCard(); cc.setName(name); cc.setAddress(address); cc.setIdcard(idcard); cc.setPhone(phone); cc.setInitialBalance(initialBalance); cc.setAllowOverDraft(allowOverDraft); //以下是自动产生的信息: cc.setCreateTime(new Date()); cc.setBalance(initialBalance); //使刚创建后的余额等于初始余额,这是典型的业务逻辑 //下面将新信用卡保存到数据库,注意其中的JDO API。 PersistenceManager pm = getPersistenceManager(); //先检测是否已经有该身份证注册的信用卡存在: Query q = pm.newQuery(CreditCard.class,"idcard==_newIdcard"); q.declareParameters("String _newIdcard"); Collection existCards = (Collection)q.execute(idcard); if(existCards.iterator().hasNext()) { throw new IdCardDuplicatedException(); //已经该身份证号存在 } //身份证号没重复,以下保存该信用卡对象: pm.currentTransaction().begin(); //每次对数据库的更新必须放到事务中 pm.makePersistent(cc); pm.currentTransaction().commit(); //提交新对象 pm.close(); //释放JDO资源 return cc; } public static class IdCardDuplicatedException extends RuntimeException {}
下面我们运行一下这个程序,将Main.main()改写如下:
public static void main(String[] args) throws Exception { System.out.println("开始测试功能……"); inputCard("张三","东风东路311号","223003433995431237","020-38864157",500.00f,5000.0f); System.out.println("信用卡已创建!"); }
编译,并运行(也可以在build.xml所在目录下运行“ant run”),系统显示:
开始测试功能……
jdbc.con.connect jdbc:mysql://localhost/test?useUnicode=true&characterEncoding=GB2312
jdbc.stat.exec set session transaction isolation level read committed
jdbc.stat.execQuery select version()
jdbc.con.rollback
jdbc.con.close
jdbc.con.connect jdbc:mysql://localhost/test?useUnicode=true&characterEncoding=GB2312
jdbc.stat.exec set session transaction isolation level read committed
jdbc.stat.execUpdate update jdo_keygen set last_used_id = last_used_id + ? where table_name = 'transaction_record'
jdbc.stat.execQuery select max(transaction_record_id) from transaction_record
jdbc.stat.exec insert into jdo_keygen (table_name, last_used_id) values ('transaction_record', 0)
jdbc.con.commit
jdbc.stat.execUpdate update jdo_keygen set last_used_id = last_used_id + ? where table_name = 'credit_card'
jdbc.stat.execQuery select max(credit_card_id) from credit_card
jdbc.stat.exec insert into jdo_keygen (table_name, last_used_id) values ('credit_card', 0)
jdbc.con.commit
jdbc.con.commit
jdbc.con.rollback
JDO Genie: Created RMI registry on port 2388
JDO Genie: Bound to jdogenie.jdogenie1
pm.created
jdoql.compile credit.system.CreditCard
idcard==_newIdcard
jdbc.stat.execQuery select credit_card_id, address, allow_over_draft, balance, create_time, idcard, initial_balance, last_transaction_time, nme, phone, jdo_version from credit_card where idcard = ?
jdbc.con.commit
tx.begin
tx.commit
jdbc.con.connect jdbc:mysql://localhost/test?useUnicode=true&characterEncoding=GB2312
jdbc.stat.exec set session transaction isolation level read committed
jdbc.stat.execUpdate update jdo_keygen set last_used_id = last_used_id + ? where table_name = 'credit_card'
jdbc.stat.execQuery select last_used_id from jdo_keygen where table_name = 'credit_card'
jdbc.con.commit
jdbc.stat.exec insert into credit_card (credit_card_id, address, allow_over_draft, balance, create_time, idcard, initial_balance, last_transaction_time, nme, phone, jdo_version) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
jdbc.con.commit
jdbc.con.commit
pm.closed
信用卡已创建!
以上信息说明我们的第一个功能已经正常完成,一条信用卡记录已经生成到数据库中。你可以通过mysql的数据库查询工具验证。我们发现Main函数返回后,系统并未退出运行,为什么呢?原来JDOGenie在默认的状态下打开了远程控制的监听服务,这对以后跟踪WebApp的服务器状态并进行调节时是很有用的。不过在这里只会干扰我们的视线,姑且将它关掉:在JDOGenie的工作台中,选择“File-->Project Properties”菜单,然后在弹出的项目属性配置对话框中将“Remote Access”检查框清掉,表示禁止远程访问,然后点击“OK”,并保存该Project。
然后,我们再次运行credit.system.Main,这次我们看到两个现象:一是程序扔出了异常,表示身份证号重复,这符合我们的预期;二是系统退出了,没有产生监听的线程,这即是刚才更改配置的结果。
到此为止,我们的创建信用卡的功能便完成了,现在我们来看一看JDOGenie提供的工作台集成的JDOQL查询功能:在工作台(WorkBench)中选中CreditCard类,然后选择菜单“Run-->View Class Extent”可以查看CreditCard类的所有对象,这里我们看到下面的结果:
如果我们修改程序并创建了多张信用卡的话,在这个对话框中会看到更多的CreditCard类的实例对象(上图实际上已经显示了两张信用卡信息。
下面我们接着实现需求描述中的其它功能,并且在输出信息中不再显示JDOGenie的调试跟踪信息(实际上在项目配置中可以将这些信息屏蔽掉),如图(将日志级别改为“errors”):
4.5.2. 浏览信用卡信息
这个功能仅仅是一个对所有信用卡的查看。实际上前面已经看到,在工作台中已经可以很轻松地完成这个功能,不过我们这里需要在程序中完成。
我们在Main类中加入一个方法:listCards()
/** * 列出系统中所有的信用卡资料 */ public static void listCards() { PersistenceManager pm = getPersistenceManager(); for(Iterator itr = pm.getExtent(CreditCard.class,false).iterator(); itr.hasNext(); ) { Object o = itr.next();
System.out.println("DataStoreIdentity="+JDOHelper.getObjectId(o)+", "+o);
} pm.close();
}
该方法列出当前系统中的所有信用卡,包括自动生成的卡号(对象标识)。可以看到,方法的内容非常简单。你可以不屑一顾地说:哦,原来JDOGenie的工作台中的查看类对象的功能也不过如此而已!不错,确实如此而已,原因很简单:科技以人为本,JDO的目标就是简化操作!
另外,再将main()方法改一下:
public static void main(String[] args) throws Exception { System.out.println("开始测试功能……"); listCards(); }
然后编译,运行,结果如下(如果用Ant来运行,可能显示结果会有一些Ant工具的信息,以后不再解释):
开始测试功能……
DataStoreIdentity=1979371451-1, 信用卡:余额=500.0,持卡人=张三,身份证号=223003433995431237,电话=020-38864157
DataStoreIdentity=1979371451-11, 信用卡:余额=500.0,持卡人=李四,身份证号=320012194501032339,电话=020-38864157
这里,我们可以看到几点:一是JDOGenie的日志功能已经关闭(前面的设置生效了),二是显示信用卡时利用到了CreditCard中的toString()方法。俗话说,机遇只照顾有准备的头脑,前面我们似乎是画蛇添足地给两个数据类加入了toString()方法,现在我们看到,这两个方法发挥了作用!
4.5.3. 交易刷卡
这个功能应该算是本系统的基本功能,信用卡拿来干什么的?就是拿来刷的。当然,拿来摆阔也有一定的可能,我就听说过有的人钱包一拿出来,N张信用卡排成一排,够豪气!不过可能一张都刷不了!
好了,废话少说,我们在Main类中加入一个新的刷卡方法buySomething():
/** * 用信用卡购买商品时的刷卡功能。 * @param cardId 信用卡的标识,一般可通过刷卡机识别得到 * @param amount 交易金额 * @param idcard 用于验证的身份证号,可通过收银员输入 * @return 交易后的卡上余额 * @throws IdcardMismatchException 身份证号不符,拒绝交易 * @throws TooMuchOverDraftException 该卡已经透支太多,不能完成本交易 */ public static float buySomething(String cardId, float amount, String idcard) throws IdcardMismatchException, TooMuchOverDraftException { PersistenceManager pm = getPersistenceManager(); pm.currentTransaction().begin(); CreditCard cc = (CreditCard)
pm.getObjectById(pm.newObjectIdInstance(CreditCard.class,cardId),false);
if(idcard == null || !idcard.equals(cc.getIdcard())) throw new IdcardMismatchException(); if(cc.getBalance()-amount < -cc.getAllowOverDraft()) throw new TooMuchOverDraftException(); TransactionRecord tr = new TransactionRecord(); tr.setCard(cc); tr.setAmount(amount); tr.setCreateTime(new Date()); tr.setNote("在天贸南大购买了一件商品"); pm.makePersistent(tr); cc.setBalance(cc.getBalance()-amount); pm.currentTransaction().commit();
float balance = cc.getBalance(); pm.close(); return balance; } public static class IdcardMismatchException extends RuntimeException {} public static class TooMuchOverDraftException extends RuntimeException {}
然后,修改Main.main():
public static void main(String[] args) throws Exception { System.out.println("开始测试功能……"); float balance = buySomething("1979371451-1",250.0f,"223003433995431237"); System.out.println("刷卡成功!余额:"+balance); }
运行结果:
开始测试功能……
刷卡成功!余额:250.0
嗯,好东西,再买一个:
开始测试功能……
刷卡成功!余额:0.0
啊,卡上没钱了?! 不过这是信用卡嘛,我再买!运行:
开始测试功能……
刷卡成功!余额:-250.0
哈哈,已经透支了,不过透支上限有5000大洋呢!我再买……
算了,还是打住吧,免得银行查下来,说我教唆用户恶意透支,有损我个人声誉。
4.5.4. 存款
存款,顾名思义,就是将现金存入信用卡,这样可以刷卡买更多的商品(这也要解释???)。
如果已经透支了,那这个时候进行的存款操作可以称作是“补仓”,哈哈,股票的术语我们借用一下。
方法Main.deposit():
/** * 存款。 * 对于存款,我们不设最多可存多少钱的限制,因此,这个方法不扔出异常。 * @param cardId 信用卡标识。这是通过PersistenceManager.getObjectId()得来的。 * @param amount 存款额 * @return 存款后的余额 */ public static float deposit(String cardId, float amount) { //assert amount > 0; //保证存款额是正数。(可以说是废话) PersistenceManager pm = getPersistenceManager(); pm.currentTransaction().begin(); CreditCard cc = (CreditCard) pm.getObjectById(pm.newObjectIdInstance(CreditCard.class,cardId),false); cc.setBalance(cc.getBalance()+amount); pm.currentTransaction().commit();
float balance = cc.getBalance(); pm.close(); return balance; }
Main.main()改成:
public static void main(String[] args) throws Exception { System.out.println("开始测试功能……"); float balance = deposit("1979371451-1",168.0f); System.out.println("存款168元后,余额是:"+balance); }
运行之,结果如下:
开始测试功能……
存款168元后,余额是:-82.0
为什么存168块钱呢?一来这个月迟到太多,工资被狂扣,二来,这个数字比较吉利,还可以剩点钱买碗面吃:(。(有没有这么可怜啊???)
总算又过了一个月,又发工资了,再存:
开始测试功能……
存款168元后,余额是:86.0
嘿,总算摆脱了透支的阴影,咱大老爷们的,总算能挺直腰板儿做人了!
唉,这祖宗传下来的观念就象三座大山,压死人了!瞧别人老美,没有一个不错钱(透支)的,别人活着多精神!咱太老实,借点钱就象是前世欠别人一样,心理负担太沉重,搞不好性格分裂,精神崩溃啊……
4.5.5. 查询透支报表
这个功能是查询当前系统中已经处于透支状态的那些信用卡,这里我们将会用到条件过滤,也就是使用JDO的查询语言JDOQL。在Main中加一个方法 listOverDrafts():
/** * 列出已经透支的信用卡。 */ public static void listOverDrafts() { PersistenceManager pm = getPersistenceManager(); Query q = pm.newQuery(CreditCard.class,"balance < 0"); //过滤条件 Collection col = (Collection)q.execute(); for(Iterator itr = col.iterator(); itr.hasNext(); ) { System.out.println(itr.next()); } pm.close(); }
我们注意到,经过刚才的一系列操作后,目前还没人透支(张三吓出一身冷汗,幸好刚才补了仓!)。再执行这个方法之前,我们再让张三买点东西,改一下main()方法,还是调用:buySomething("1979371451-1",250.0f,"223003433995431237"); 结果如下:
开始测试功能……
刷卡成功!余额:-164.0
可怜的张三又背上了沉重的心理负担。
下面我们改一下Main.main(),调用列出透支信用卡方法:
public static void main(String[] args) throws Exception { System.out.println("开始测试功能……"); System.out.println("以下是透支的信用卡列表:"); listOverDrafts(); }
运行,结果如下:
开始测试功能……
以下是透支的信用卡列表:
信用卡:余额=-164.0,持卡人=张三,身份证号=223003433995431237,电话=020-38864157
我们快马加鞭,继续挺进下一需求功能。
4.5.6. 查询交易明细
这个功能相对复杂一点,不过也只是理解上复杂,代码还是很简单的。我们在Main中增加一个方法 listTransactions():
/** * 列出某信用卡的交易明细。 * @param idcard 身份证号 */ public static void listTransactions(String idcard) { PersistenceManager pm = getPersistenceManager(); Query q = pm.newQuery(TransactionRecord.class, "card.idcard==_p0"); //过滤条件 q.declareParameters("String _p0"); //声明查询参数表 q.setOrdering("createTime ascending"); //按时间顺序列出 Collection col = (Collection)q.execute(idcard); //按指定身份证号查询 for(Iterator itr = col.iterator(); itr.hasNext(); ) { System.out.println(itr.next()); }
pm.close(); }
在这个方法中,我们用到了JDOQL最吸引人的特性之一:对象引用,即“card.idcard==_p0”,这样实际上相当于两个数据表联表查询,显然,这样的语句更易理解,更简洁!此外,我们用到了查询参数“_p0”,最后还用到了排序。实际上,还可以按此类所引用的其它类的属性进行排序,比如按持卡人姓名顺序列出所有交易记录,排序语句将是:q.setOrdering("card.name ascending"); 又省下表连接的冗长SQL语句。
最后,我们再修改Main.main()方法:
public static void main(String[] args) throws Exception { System.out.println("开始测试功能……"); System.out.println("以下是张三的交易记录:"); listTransactions("223003433995431237"); }
运行之,结果如下:
开始测试功能……
以下是张三的交易记录:
交易记录:持卡人=张三,身份证号=223003433995431237,交易额=250.0,时间=Mon Jun 30 09:24:51 GMT 2003
交易记录:持卡人=张三,身份证号=223003433995431237,交易额=250.0,时间=Mon Jun 30 09:25:34 GMT 2003
交易记录:持卡人=张三,身份证号=223003433995431237,交易额=250.0,时间=Mon Jun 30 09:27:24 GMT 2003
交易记录:持卡人=张三,身份证号=223003433995431237,交易额=250.0,时间=Mon Jun 30 09:29:15 GMT 2003
交易记录:持卡人=张三,身份证号=223003433995431237,交易额=250.0,时间=Mon Jun 30 09:29:53 GMT 2003
交易记录:持卡人=张三,身份证号=223003433995431237,交易额=250.0,时间=Mon Jun 30 10:35:57 GMT 2003
这些就是我们前面的几次调用产生的交易记录。实际上,存款记录也应该算是交易记录,只不过本系统中暂时没有这种需求。(注意了,以后提需求的时候要多加考虑才行,否则又会让开发商钻空子了!)
4.6. 让程序更实用些
上面我们已经完成了所有的需求功能,大家也看到了基于JDO的应用是如何开发的,需要哪些配置。我再次说明一下,JDOGenie是最方便学习JDO的工具,尽管每隔一个月你得去重新索取一个试用license。
话又说回来,上面这些完成业务逻辑的方法太简单了,不被开发主管打才怪!(参见:程序写得烂被主管狂扁!)
为了让程序更实用,我们需要将其改造成一个真正的应用,这就涉及到安全性、性能优化与索引创建、容易操作的界面、后台操作日志、备份、图文并茂的报表,等等等等。尽管这些已经超出本文的范围,我们下面还是进行一点简单的讨论。
一般有两种最常见的方式做这样的系统:独立的GUI程序或者基于浏览器的Web应用。下面分别给出两种方式下的建议:
4.6.1. 图形界面的独立程序
首先,我们要从java.awt.Frame扩展出一个子类,在其窗口中放入几个Panel,分别对应需求中的几项功能,如果该功能是需要输入参数的,则在相应的Panel中放几个TextField,然后给一个提交按钮;如果该功能不需要输入参数,则直接给一个功能按钮即可。每个按钮的事件响应中,将相关的参数从界面元素中读出,传入到相应的方法中,然后接收方法的返回,将结果打印在界面的输出区(对了,还需要在界面上加一个输出区域,比如TextArea)。
前面我们的Main类的业务逻辑方法中,很多地方都是直接打印了查询取得的数据,在独立GUI程序中不能这样做,只能将查询取得的数据(如Collection)返回到调用的按钮事件中,再由按钮事件处理的代码打印到输出区域。这也算是一种MVC的方式了,呵呵。
还有一点建议,由于pm.close()后,对象的属性不能保证准确读出,所以,在独立的GUI程序中,最好系统一直保留一个打开的pm,各个方法共享。
4.6.2. 采用JSP的Web应用
基于JSP的Web应用相对复杂一些,我们需要将jdogenie.jar(合并了jta.jar和其license文件的包)和mysql.jar放到应用的/WEB-INF/lib中,然后将自己的enhance过的类代码按目录结构放到/WEB-INF/classes/中,同时配置文件creditSys.jdogenie和system.jdo一并放到/WEB-INF/classes/目录中。
这些只是目录的配置,我们还需要对前面的代码进行改造。
首先,与独立的GUI程序一样,需要将直接通过System.out.println()输出的业务逻辑方法改为将查询所得的Collection或具体对象返回到调用者。这里的调用者就是JSP。我们可以对每个功能写一到两个jsp,这些JSP只完成数据输入和显示的作用,这一点与独立的程序差不多。
不过,由于Web应用是多线程的,就需要考虑到并发连接的问题,因此系统中不能只设置一个始终打开的PM,而是每个页面请求需要使用一个PM。为了能在一个页面请求的处理过程中在不同的代码片断里共享一个PM,你可以参考我在JDOCentral的论坛上发表的这篇贴子。
关于Web应用中PM连接池和资源释放的细节已经超出本文的范围,请参考我在CSDN上发表的其它关于JDO的文章(见本文尾部的“参考文章”)。
5. 总结
下面我们对以上的基于JDO的开发作一点总结。
5.1. 开发流程的变化
基于JDO开发的流程在本文开始处通过一幅图进行了介绍,主要增加了两个步骤:编写metadata,和根据metadata对类代码进行增强。而这两步通过JDOGenie的工作台很容易搞定,都是自动化的。
剩下的开发过程就比原始的JDBC包装简单多了,我们不再有SQL代码,不再有复杂的“增、删、改、查”通用接口,不再有不支持事务的担心,不再有……我们只需要在一个完全面向对象的数据对象模型上通过JDO的API进行操作即可。
另外一点,我们可以看到,基于JDO的开发中,各种开发角色(JavbaBean编写、JSP编写、数据库维护、配置)都是基于数据对象模型进行开发,比如说,用JDOGenie就可以生成数据模型的UML类图后,打印下来,交给各个开发参与人员,无论是写控制类JavaBean,还是JSP,还是数据库结构维护,都可以以这个图作基准,大家统一进行开发,而不是象基于JDBC的应用,大家都基于数据库结构进行开发。
JDO给我们带来的太多了,需要在开发过程中慢慢体会。
5.2. 速度会不会慢?--性能测试数据参考
我使用过一个有一定规模的应用来测试过JDO的性能,这个应用具有十多个类,每个类有5~20个属性,类之间有错综复杂的关系,功能很多,是一个完整的基于浏览器的Web应用。使用原始的自己的JDBC包装时,数据库相关的处理代码非常多,有10多K,而改造JDO后,这些代码都没了,实际上,相当于这些代码变成了JDO厂商的支持包。
接着是性能测试,原来的基于JDBC的应用每秒可处理8个请求,而采用JDOGenie作底层的JDO应用每秒可处理35个请求,采用KodoJDO作底层的JDO应用(手写的Java代码与JDOGenie一样)时,每秒可处理25个请求。这一切都归功于JDO规范所规定的延迟读取机制(Lazy Loading Mechanism)。
可能有眼尖的读者会问:为什么文章开头说Kodo是性能王者,而这里却不如JDOGenie呢?不错,眼睛实在是雪亮!这样的材质,怎么不去做私家侦探啊!搞个什么X美凤的片子,肯定一炮走红!
哦,对了,我还没给出解释。我前面说Kodo性能最好,是包括对稳定性的评价在内的。我这里对两者的测试,都是采用了JDO规范定义的乐观事务方式(javax.jdo.option.OptimisticTransaction),这种方式将事务的锁定限制在应用端,不对数据库造成压力,只是在对同一对象的并发改动很多的时候会导致很多冲突。一般来说,你的Web应用不会一天访问量上百万吧?如果不到,就完全可以采用这种事务处理方式。Kodo的事务处理在测试中成功机会比JDOGenie大很多,同样的测试时间,Kodo的类似访问数的统计数据比JDOGenie要多,也就是说Kodo的成功事务比JDOGenie多,尽管总访问数少于JDOGenie。综上所述,Kodo的性能还是要比JDOGenie好。不过JDOGenie也在奋起直追,我对它比较看好!
5.3. 还有些不爽--JDO1.0的局限性
好了,兴奋过后,应该是冷静的反思。
有一句话说得好,最值得相信的人,只有你自己!
我是一名JDO的狂热追随者,为推广JDO做着不懈的努力,当然免不了某些时候言语过激或片面化,所谓“一叶障目,不见泰山”。就象Java追随者认为.NET一无是处一样,实际上.NET有很多优点是Java无法相比的,比如快速,灵活。
我也冷静下来,仔细地思考了JDO1.0的不足之处,竟发现有很多:
- 增加额外步骤,配置复杂(相对于直接的JDBC)
- 对数据模型有一定限制(必须有一个无参构造器,属性访问需要getter和setter)
- 双向对象关系的处理太欠缺(JDO2.0计划中的自动维护的对象关系将解决这些)
- JDOQL的API稍显累赘(declare一大堆东西,比ODMG的OQL标准还是不如)
- 没有数据库统计功能(count(),max(),avg()等等,不过已经在JDO2.0计划中)
5.4. JDO2.0展望
Sun的动作就是慢,象一个迟暮的巨人。JDO1.0讨论了四年才半推半就地现身,JDO2.0又还未正式立项,真不知何年何月才能真正解决数据库应用中的所有问题!不过群众的参与就是推动力,让我们一起提出我们的意见,一起推动JDO2.0的车轮吧!(我们可以对JDO规范制定人的邮箱狂轰滥炸,直到他们说“马上开始制定”或者“算了,给你十万$,饶了我吧”为止)。
6. 参考文章(中文)
实际上下面列出的文章基本上都可以直接在www.CSDN.net上通过搜索关键字“JDO”的技术文档来找到。
- 《JDO对开发的帮助有哪些》
- 《JDO能给我们带来什么》
- 《JDO资源介绍》
- 《一个JDO的成功案例分析》
- 《一个纽约女技术员的JDO经验》
- 《最权威的JDO新书》
- 《Java Data Objects第一章翻译》
- 《选择CMP还是JDO》
还可以在CSDN上找到一些其它作者写的文章。
7. 参考资料
- JDO规范的主页--JSR12,http://jcp.org/en/jsr/detail?id=012
- Sun网站中作为J2SE optional pack的网页--http://java.sun.com/products/jdo/
- 核心推广网站--http://www.jdocentral.com
- 最集中的讨论区--http://www.jdocentral.com/forums/index.php
- 相关中文资源网之一--http://www.CSDN.net
- 相关中文资源网之二--http://www.JavaResearch.org
- Versant的一份介绍材料:http://www.jdocentral.com/pdf/JavaDataObjects_McCammon.pdf
- 综合资料:http://www.jdocentral.com/JDO_Resources_Body.html
8. 参考书籍:
以下信息来自:http://www.jdocentral.com/JDO_Resources_Body.html
- 《Java Data Objects》,作者:Robin Roos(英国)
- 《Java Data Objects》,作者:Craig Russell(美国) 和 David Jordan(美国)
- 《Core Java Data Objects》,作者:很多人,由Sun组织
- 《Using and Understanding Java Data Objects》 作者:David Ezzio(美国)
- 其它一些PDF资料:http://www.javaresearch.org/dn/JDO_introduce.zip
- 《Java Data Objects》,作者:Robin Roos(英国)
本文的版权属于笔者本人,但欢迎转载,前提是注明出处和原作者。另外,欢迎在我的专栏中查看我的另几篇文章,并提出宝贵意见!