- 浏览: 19398 次
- 性别:
文章分类
最新评论
在前面读者看到的本文的《配置篇》中,我们介绍了用于本文中的基于JDO的WebApp开发的各种环境配置,尤其是本文选用的JDO产品:JDOGenie。JDOGenie的特点就是图形配置工具功能强大,方便使用,学习JDO非常容易。
我们前面已经完成了一个基本的数据对象模型,并且,每个数据类的属性都用getter/setter进行了封装。这里重温一下这个数据模型:
这个类图是非常重要的,相当于传统JDBC开发的数据库结构图,但又比数据库结构图更高级一层。实际上,这个类图很容易使用UML工具生成,包括所有数据类的Java源代码。后面的真正的业务逻辑开发过程才是我们的重点,但这个类图却是贯穿整个开发过程的核心,数据库管理员可以根据这个类图来配置并生成数据库结构;开发人员按照这个类图进行对象获取和访问;美工人员可以根据这个类图来使用流行的可视化页面设计工具美化JSP页面(比如显示某个回复的主题标题,就按图中的结构使用${a_reply.topic.title})。
现在我们已经看到这个类图的重要性,并且也知道这个类图可以通过UML工具快速生成。因此,我们在开发之前几乎不需要任何手工编码,工作的重点就是与根据需求与相关开发人员讨论并确定这个类图结构。这里也体现了JDO开发的一个优点,就是开发之前不需要太多地在意具体的代码,而只需要在面向对象的层面上讨论确立类图的结构即可。讨论类图比讨论数据库结构要容易得多,一方面容易记忆,另一方面一般来说,一个类图会比相应的数据库结构图简单得多(比如一些双向的关系直接在类图上体现,不用象数据库一样需要一个额外的关联表来对应)。
接下来,我们在《配置篇》的基础上继续进行JDO版论坛的开发。请先快速地在脑海中回顾一下我们使用JDOGenie的工作台生成本应用的项目配置文件,并将数据类导入的过程,下面介绍与前面那些图形界面的操作等价的配置文件。对图形界面不感兴趣的读者也可以直接从这里开始阅读。
1 等价的配置文件代码
实际上,在《配置篇》中的一切配置步骤,就是为了生成两个文件:一个是描述所有数据类的元数据文件:WEB-INF/classes/system.jdo,其源码如下:
<?xml version="1.0" encoding="UTF-8"?>
<jdo>
<package name="jdobbs">
<class name="Post" />
<class name="Reply" persistence-capable-superclass="Post" />
<class name="Topic" persistence-capable-superclass="Post">
<field name="replies">
<collection element-type="Reply">
<extension vendor-name="jdogenie" key="inverse" value="topic" />
</collection>
</field>
</class>
</package>
</jdo>
另一个生成的文件是JDOGenie专用的配置文件,包含数据库连接、JDOGenie缓冲配置等等信息。源码如下:
# Server name (must be unique on a machine)
server=jdogenie1
remote.access=true
remote.pm=false
hyperdrive=true
# JDO Genie version
version=2.2.0beta7 (28 Nov 2003)
# Data stores
storeCount=1
store0.name=main
store0.type=jdbc
store0.driver=com.mysql.jdbc.Driver
store0.url=jdbc\:mysql\://localhost/test?useUnicode\=true&characterEncoding\=GBK
store0.db=mysql
store0.user=
store0.password=
store0.properties=
store0.maxActive=5
store0.maxIdle=3
store0.minIdle=2
store0.init.sql=
store0.validate.sql=
store0.retry.interval.ms=1000
store0.retry.count=10
store0.pscache.max=
store0.ext.jdbc-key-generator=-
store0.jdbc.namegen=-
# .jdo resources
jdoFileCount=1
jdo0=system.jdo
# Properties for JDOHelper.getPersistenceManagerFactory(...)
javax.jdo.PersistenceManagerFactoryClass=za.co.hemtech.jdo.client.BootstrapPMF
javax.jdo.option.Optimistic=true
javax.jdo.option.RetainValues=false
javax.jdo.option.RestoreValues=false
javax.jdo.option.IgnoreCache=false
javax.jdo.option.NontransactionalRead=true
javax.jdo.option.NontransactionalWrite=false
javax.jdo.option.Multithreaded=false
# Event logging
event.logging=-
log.downloader=-
# Cache settings
cache.enabled=true
cache.maxobjects=10000
cache.listener=-
query.cache.enabled=true
query.cache.max.queries=10000
# Data store 0 mappings
store0.jdbc.type.count=0
store0.jdbc.javatype.count=0
# Workbench properties (not used at runtime)
mdedit.classPathCount=2
mdedit.cp0=
mdedit.cp1=../lib/mysql.jar
# Workbench Ant configuration (not used at runtime)
ant.disabled=false
ant.buildfile=../build.xml
ant.compile=enhance
ant.show.all.targets=false
# Workbench Scripts (not used at runtime)
# JDO Genie Class Diagrams (not used at runtime)
diagram.count=1
diagram0.name=Diagram 1
diagram0.legend=299,24
diagram0.general=13,Y,Y
diagram0.class=Y,N,Y,N,N,Y,N,Y,N,N,N,N,N,N,N,N,N
diagram0.field=Y,Y,N,N,N,N
diagram0.class.count=3
diagram0.class0=jdobbs.Post,127,48
diagram0.class1=jdobbs.Reply,213,222
diagram0.class2=jdobbs.Topic,33,206
实际上,我们如果直接手工编写这两个文件的话,也可以不必进行前面那些配置工作。当然,估计很少有人会喜欢手工编码多过使用图形界面。
2 初步测试JDOGenie
至此我们已经有了数据库结构、有了增强过的数据类,已经可以正式开始使用JDO了。为了加强感性认识,我们先看看JDOGenie的运行情况:我们点击工作台左边的按钮“Grid”进入数据类列表区,选中某个类,点击上面工具条的第11个按钮“View all instances of the selected class…”或者直接按Ctrl+E,系统就会列出该类在数据库中的所有实例对象。不过我们这里还没有任何数据,所以什么也没列出来,以后可以通过这个功能查看某个类的所有实例。如果需要更细致的查询,可以进入左边四个按钮进入JDO查询区,使用JDOQL或者SQL来查询数据。JDOQL的查询界面如图:
好了,关于JDOGenie的细节已经讲完了。之所以用这么大的篇幅介绍JDOGenie的配置,目的是让读者能够最快速地了解JDO需要的配置信息,并能最快地看到JDO产品的运行。下面我们来真正地开始实现我们的论坛功能。
3 业务逻辑的实现
为了简单、直观地显示基于JDO的业务开发流程,我们在接下来的功能实现中采用JSP直接访问JDO API来实现数据对象的获取和操纵,就不再累赘地使用JavaBean对话控制器了。
为了使每个页面的代码更简单,并且又具备调用JDO API、使用常用的Java包、使用JSTL的功能,我们写一个专门的头页面,用于其它页面包含,这样,可以使用其它页面的代码更简洁易读。
<%@page
contentType="text/html; CHARSET=utf8"
import="jdobbs.*,java.util.*,javax.jdo.*,java.io.*"
%>
<%@taglib prefix="c" uri="http://java.sun.com/jstl/core" %>
<%
//预置几个日期格式化工具
pageContext.setAttribute("fullTime",new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
pageContext.setAttribute("shortTime",new java.text.SimpleDateFormat("MM-dd HH:mm"));
pageContext.setAttribute("fullDate",new java.text.SimpleDateFormat("yyyy-MM-dd"));
pageContext.setAttribute("shortDate",new java.text.SimpleDateFormat("MM-dd"));
%>
这里我们预置了几个用于显示日期的格式化对象,可以方便后面日期数据的显示。在Resin3中,可以用${shortTime.format(someDate)}来以显示类似“09-10 15:33”样式的日期字符串。也许在JSP2.0最终出台之前,这种方式未必规范,但既然服务器支持,就先这么用吧。
回顾一下论坛的功能需求,主要有以下几点:
1. 网友进入主页时,系统列出目前所有的主题,按时间顺序从近到远排列。
2. 网友可以在主页下方的发贴表单中发表新主题,包括标题和内容
3. 其它网友在主页的贴子列表中点击某个标题可以阅读这个主题的详细内容,包括所有的回贴内容。所有回贴按照时间顺序排列在主题贴后面 4. 可以在主题详细内容页面尾部的回贴表单中回复这个主题
5. 每个网友发表主题或贴子时,系统需要记录该贴子发表时的客户端IP地址
6. 系统提供一个高级搜索功能,让网友可以根据时间、主题标题、内容或回复内容、IP搜索主题
我们接下来一个一个地实现这些功能。
3.1 主页:列出所有主题
我们在index.jsp中执行一个不带条件的JDO查询,并按时间倒序用HTML中的表格来显示一条一条的主题。我们将index.jsp的代码改动如下:
<%@page contentType="text/html; CHARSET=utf8" %>
<%@include file="/inc.jsp" %>
<%
//先取得主题列表:
Query q = Sys.pm().newQuery(Topic.class,"");
q.setOrdering("postTime descending");
pageContext.setAttribute("topics",q.execute());
%>
<title>JDO BBS on ${pageContext.request.serverName}</title>
您现在的位置:<b>JDO BBS 首页</b>
<h3>欢迎访问JDOBBS!</h3>
<table border=1>
<tr><th>标题</th><th>内容长度</th><th>发表时间</th><th>IP地址</th><th>回复数</th><th>最后回复</th></tr>
<c:forEach var="topic" items="${topics}">
<tr>
<td><a href="topic.jsp?id=${topic.jdoGetObjectId()}">${topic.title}</a></td>
<td>${topic.length}</td>
<td>${shortTime.format(topic.postTime)}</td>
<td>${topic.ip}</td>
<td>${topic.replyCount}</td>
<td>${shortTime.format(topic.lastUpdate)}</td>
</tr>
</c:forEach>
</table>
在这段代码中,我们先是使用了一人JDO的javax.jdo.Query来执行查询,对这个Query设置了一个排序参数:“postTime descending”,这个设置使JDO生成“… order by POST_TIME”的SQL子句,完成排序。在JDO中,也可以使用多个排序字段,用“,”号隔开即可。值得一提的是,JDO的排序字段可以是通过引用到达的另一个对象的属性,比如查询Reply对象时,可以按其所回复的主题的回复数进行排序,代码是:
Query query = pm.newQuery(Reply.class,””);
query.setOrdering(“topic.replyCount ascending”); //注意正序也必须写上“ascending”
这两行简单易懂的代码将产生类似
select …
FROM post a LEFT JOIN post AS b ON (a.topic = b.post_id)
WHERE a.jdo_class = 54451616
ORDER BY b.reply_count
的SQL语句。如果数据库不是MySQL而是Oracle或其它数据库的话,这条SQL语句还会有不同的形式。从中也可以看出JDO的透明性,你只需要按最直接的想法设置排序(“topic.replyCount ascending”),而不用去考虑联表的SQL如何编写,更不用考虑不同的数据库的语法细节。
由于数据库中还没有数据,我们这里就不给出显示效果,留待后面再说。需要说明的一点是:使用JSP2.0编写的这个index.jsp页面很容易在一般的页面编辑器中进行维护。比如这个页面在DreamweaverMX中编辑时的界面是:
也许熟悉DreamWeaver的读者会觉得这个表格会将页面撑得比较宽影响美工设计,比如只需要显示顶多3位数字的“回复数”字段在编辑页面时得写成“${topic.replyCount}”,从而将这一单元格撑得过宽,实际上,这样的问题可以通过一个小技巧解决:将该处先写成一个随意的数,再将这个数包(Wrap)上层JSTL表达式“c:out”即可,相应的HTML源码是:<td><c:out value=”${topic.replyCount}”>123</c:out></td>。关于这方面的技巧还有一些,都属于JSP2.0页面美工的内容,这里不再一一描述。
现在我们继续完成下面的“发表新主题”功能来产生数据。
3.2 发表新主题
为了新发表贴子,我们需要一个输入表单,现在将这个表单做到首页主题列表的后面,并让这个表单提交到一个名为index!post.jsp的页面,这个接收页面解析提交的参数,并使用JDO API生成新的主题贴子,保存到数据库中。
先在index.jsp尾部增加一个表单:
<form name=fmPost method=post action="index!post.jsp">
请在这里发表新主题。
<br>标题:<br><input name=title maxlength=100 size=100>
<br>内容:<br><textarea name=content cols=100 rows=10></textarea>
<br><input type=submit value=提交>
</form>
然后编写接收发贴表单提交的index!post.jsp:
<%@page contentType="text/html; CHARSET=utf8" %>
<%@include file="/inc.jsp" %>
<%
Topic topic = new Topic();
topic.setTitle(request.getParameter("title"));
topic.setContent(request.getParameter("content"));
topic.setLength(topic.getContent().length());
topic.setPostTime(new Date());
topic.setLastUpdate(new Date());
topic.setIp(request.getRemoteAddr());
Sys.pm().currentTransaction().begin();
Sys.pm().makePersistent(topic);
Sys.pm().currentTransaction().commit();
pageContext.setAttribute("topic",topic);
%>
您的主题已经发表,请选择下列操作之一:
<p><a href="index.jsp">返回主题列表</a>
<p><a href="topic.jsp?id=${topic.jdoGetObjectId()}">进入该主题页面</a>
我们发贴的过程先是生成一个Topic对象,然后使用JDO的API开始一个事务,标记这个新生成的topic对象为存储实例,然后提交事务,这样就完成了对象的保存过程。当事务提交后,这个topic对象就具备了由JDOGenie生成的一个数据库标识,所以我们可以在页面尾部的“进入该主题页面”链接中使用其标识:${topic.jdoGetObjectId()}
现在我们就可以发表主题了。发表了一系列主题后,我们的论坛主页就比较充实了:
[img]http://lcspace.nease.net/c-j2ee/new-9.files/CSDN_Dev_Image_2003-12-62248498.jpg[img]
3.3 阅读主题及回复
前面的主题列表页面中的每个主题已经有链接到topic.jsp页面,现在我们来编写这个页面:
<%@page contentType="text/html; CHARSET=utf8" %>
<%@include file="/inc.jsp" %>
<%
//先取得主题:
String oid = request.getParameter("id");
Object objectId = Sys.pm().newObjectIdInstance(Topic.class,oid);
Topic topic = (Topic)Sys.pm().getObjectById(objectId,false);
pageContext.setAttribute("topic",topic);
%>
<title>主题:${topic.title}</title>
您现在的位置:<a href=".">JDO BBS 首页</a> --> <b>主题:${topic.title} </b>
<p>内容:<pre><b>${topic.content}</b></pre>
下面是回复列表:
<table border=1>
<tr><th>回复时间</th><th>IP地址</th><th>内容长度</th><th>回复内容</th></tr>
<c:forEach var="reply" items="${topic.replies}">
<tr> <a name="#${reply.jdoGetObjectId()}">
<td>${shortTime.format(reply.postTime)}</td>
<td>${reply.ip}</td>
<td>${reply.length}</td>
<td><pre>${reply.content}</td>
</tr>
</c:forEach>
</table>
我们在页面开始先通过访问本页面时必须给出的“id”参数得到一个Topic对象,并将其设置到pageContext中,剩下的代码就是利用JSP2.0显示其内容,包括所有的回复。我们先不给出显示效果,而是接着实现回复主题功能:
3.4 回复主题
先在刚才的主题页面尾部加上回复表单:
<form name=fmReply method=post action="topic!reply.jsp">
请在这里回复本主题。
<input type=hidden name=topicId value="${topic.jdoGetObjectId()}">
<br>内容:<br><textarea name=content cols=100 rows=10></textarea>
<br><input type=submit value=回复>
</form>
然后,编写一个接收回复提交的页面:topic!reply.jsp:
<%@page contentType="text/html; CHARSET=utf8" %>
<%@include file="/inc.jsp" %>
<%
Sys.pm().currentTransaction().begin();
//先取得被回复的主题:
String oid = request.getParameter("topicId"); //回复表单中引用的主题贴标识
Object objectId = Sys.pm().newObjectIdInstance(Topic.class,oid);
Topic topic = (Topic)Sys.pm().getObjectById(objectId,false);
//增加回复:
Reply reply = new Reply();
reply.setContent(request.getParameter("content"));
reply.setLength(reply.getContent().length());
reply.setPostTime(new Date());
reply.setIp(request.getRemoteAddr());
topic.getReplies().add(reply); //将新回复加到主题的回复列表中
topic.setReplyCount(topic.getReplyCount()+1); //回复计数加1
topic.setLastUpdate(new Date());
Sys.pm().currentTransaction().commit();
pageContext.setAttribute("topic",topic);
%>
您的回复已经发表,请选择下列操作之一:
<p><a href="index.jsp">返回主题列表</a>
<p><a href="topic.jsp?id=${param.topicId}">进入所回复的主题页面</a>
在这个页面的开始部分,我们先通过回复表单传递过来的被回复的主题标识取得该主题,然后新生成一个Reply对象,设置好其属性后,将这个新生成的Reply对象添加到Topic的replies列表中。我们这段代码中并没有使用pm.makePersistent()方法,因为将reply对象加到topic对象的replies列表中时,JDO已经标记该reply对象需要保存,这个特性就是JDO中的“可达性存储”概念。如果一个新生成的对象被一个已经存在于数据库中的对象引用,那么,提交事务时这个新对象将被保存,甚至如果这个新对象又引用了另一个新生成的对象,另一个新对象也会被保存。这个特性有时候可以减少我们的代码复杂性。
细心的读者可能注意到了,最后一行代码中使用了“${param.topicId}”来直接将提交参数中的主题贴标识拼装到返回主题页面的链接中。这使用到了JSP2.0的隐含环境变量param。
我们测试回复功能,回复了几个贴子后,主题页面的效果如图所示:
而回到论坛首页,界面如下图:
3.5 高级搜索功能
到此,我们已经完成了基本的发贴和回复功能,现在做一个组合条件搜索的功能,以体现JDOQL的特点。
回顾一下搜索功能需求:“让网友可以根据时间、主题标题、内容或回复内容、IP搜索主题”,我们需要编写一个搜索页面,包含一个搜索条件输入表单,这个表单直接提交到本页面,提交后,表单中将显示所输入的条件,而在表单下方,列出符合条件的主题。
源码 search.jsp:
<%@page contentType="text/html; CHARSET=utf8" %>
<%@include file="/inc.jsp" %>
<title>主题:${topic.title}</title>
您现在的位置:<a href=".">JDO BBS 首页</a> --> <b>搜索论坛主题</b>
<form name=fmSearch>
请在下面输入搜索条件:
<br>发贴日期:
<select name="postTime">
<option value="">不限</option>
<option value="week">一周内</option>
<option value="month">一月内</option>
<option value="year">一年内</option>
</select>
<script>fmSearch.postTime.value = "${param.postTime}"; </script>
<br>标题包含:
<input name=title value="${param.title}">
<br>内容或回复贴的内容包含:
<input name=content value="${param.content}">
<br>发贴IP:
<input name=ip value="${param.ip}">
<br><input type=submit value=搜索>
</form>
<%
//这里进行JDO查询:
Query q = Sys.pm().newQuery(Topic.class);
String jdoql = "";
String paramNames = "";
List params = new ArrayList();
String postTime = request.getParameter("postTime");
if(postTime != null && !postTime.equals("")) {
jdoql += " && postTime >= _time";
paramNames += ",Date _time";
if(postTime.equals("week")) { //最近一周
params.add(new Date(System.currentTimeMillis()-7l*24*60*60*1000));
} else if(postTime.equals("month")) { //最近一月
params.add(new Date(System.currentTimeMillis()-30l*24*60*60*1000));
} else { //最近一年
params.add(new Date(System.currentTimeMillis()-365l*24*60*60*1000));
}
}
String title = request.getParameter("title");
if(title != null && !title.trim().equals("")) {
jdoql += " && title.startsWith(_title)";
paramNames += ",String _title";
params.add("%"+title);
}
String content = request.getParameter("content");
if(content != null && !content.trim().equals("")) {
//主题内容或者某个回复的内容包含子串:
jdoql += " && (content.startsWith(_content) || replies.contains(aReply) && aReply.content.startsWith(_content))";
paramNames += ",String _content";
params.add("%"+content);
q.declareVariables("Reply aReply");
}
String ip = request.getParameter("ip");
if(ip != null && !ip.trim().equals("")) {
jdoql += " && ip == _ip";
paramNames += ",String _ip";
params.add(ip.trim());
}
if(jdoql.startsWith(" && ")) jdoql = jdoql.substring(4);
if(paramNames.startsWith(",")) paramNames = paramNames.substring(1);
q.setFilter(jdoql);
q.declareParameters(paramNames);
pageContext.setAttribute("topics",q.executeWithArray(params.toArray()));
%>
下面是符合条件的主题列表: &&<a href="search.jsp">搜索主题</a>&&<input type=button value="发表新主题" onclick="fmPost.title.focus()">
<table border=1>
<tr><th>标题</th><th>内容长度</th><th>发表时间</th><th>IP地址</th><th>回复数</th><th>最后回复</th></tr>
<c:forEach var="topic" items="${topics}">
<tr>
<td><a href="topic.jsp?id=${topic.jdoGetObjectId()}">${topic.title}</a></td>
<td>${topic.length}</td>
<td>${shortTime.format(topic.postTime)}</td>
<td>${topic.ip}</td>
<td>${topic.replyCount}</td>
<td>${shortTime.format(topic.lastUpdate)}</td>
</tr>
</c:forEach>
</table>
这个页面执行了一个根据提交的搜索条件拼装的JDOQL,完成了搜索功能。界面如下:
4 功能扩展
本章中将讨论一些对这个论坛功能进行增强或者解决一些传统JDBC难以解决的问题。
4.1 字符串长度限制
可能大家有过这样的经验:在一个论坛上原创了一篇很长的文章,结果提交时服务器却提示数据库字段长度不够,只能拆成几贴来发表。这种情况并不少见,有点影响发贴者的积极性。于是,我们希望能够做到:对这种长文本类型的输入信息不作长度限制,想输多少就输多少。即使要限制长度,也是通过其它方式限制,而不是被动地受数据库服务器的限制。
现在我们可以利用JDOGenie对java.util.List的支持(主流的JDO产品都支持这个JDO可选特性)来技巧性地实现无限制的字符串。方法是将Post.content属性的声明改为List,并在system.jdo中设置该List的元素类型为java.lang.String(通过JDOGenie工作台进行设置),并在Post.content的getter/setter方法中进行一些处理(方法的声明不需要改变,调用的JSP也不需要改变)。具体方法请参考JDOCentral上的笔者共享出来的一个JDO长字符串处理工具类:{http://www.jdocentral.com/forums/index.php?act=ST&f=11&t=564&s=56744171186d115a997fdefbdb46d4a5} 。
原理清楚后,我们将这段工具代码放到Sys类中,在Sys.java中加入两个新的函数:
/**
* 将长字符串分割存放在一个列表中
* @param value 需要设置的字符串
* @return List类型的字符串片断列表,请将之设到JDO对象的相关属性中。
*/
public static List setLongString(String value) {
if(value == null) return null;
int len = value.length();
int count = (len+partSize-1)/partSize;
List list = new ArrayList(count);
for(int i = 0; i < count; i++) {
int from = i*partSize;
list.add(value.substring(from,Math.min(from+partSize,len)));
}
return list;
}
/**
* 从片断列表中取得长字符串
* @param list 片断列表
* @return 拼装后的长字符串
*/
public static String getLongString(List list) {
if(list == null) return null;
if(list.size() == 1) return (String)list.get(0); //for better performance
//here some cache may be used for faster speed
StringBuffer sb = new StringBuffer();
for(Iterator itr = list.iterator(); itr.hasNext(); ) sb.append(itr.next());
return sb.toString();
}
private static int partSize = 2000;
然后,在Post类中进行一下修改:
/** 贴子内容 */
List content;
public String getContent() {
return Sys.getLongString(content);
}
public void setContent(String value) {
content = Sys.setLongString(value);
}
最后,还需要修改一下元数据文件system.jdo(可在JDOGenie工作台中进行):
<?xml version="1.0" encoding="UTF-8"?>
<jdo>
<package name="jdobbs">
<class name="Post">
<field name="content">
<collection element-type="java.lang.String" />
</field>
</class>
<class name="Reply" persistence-capable-superclass="Post" />
<class name="Topic" persistence-capable-superclass="Post">
<field name="replies">
<collection element-type="Reply">
<extension vendor-name="jdogenie" key="inverse" value="topic" />
</collection>
</field>
</class>
</package>
</jdo>
到此为止,代码就算是修改完成了,现在只剩下数据库结构还没有同步。如果不用保留前面的数据,我们简单地在JDOGenie工作台中重建数据库即可,如果需要保留数据,我们可以看看新的数据结构与旧的有什么差别,进行相应的“ALTER TABLE …”和数据复制“INSERT INTO … ”即可。本例中需要进行的SQL操作是:
create table post_content (
post_id INTEGER not null,
seq INTEGER not null,
val VARCHAR(255),
constraint pk_post_content primary key (post_id, seq)
) TYPE = InnoDB;
insert into post_content
select post_id,1,content from post;
alter table post drop column content;
还有一点需要注意的是,针对Post.content进行查询的JDOQL需要从
content.startsWith(…)
改成:
content.contains(s) && s.startsWith(…)
4.2 更多扩展功能
到现在为止,我们已经用JDO和JSP2.0完成了一个具备基本功能的论坛系统,在此基础上,我们还可以扩展更多的功能,比如:
l 贴子列表的分页处理
2 论坛分板块
3 会员功能
4 个性化
5 版主功能
6 审核
对发贴内容进行审查,通过才能显示出来
7 积分
为调动用户积极性,加上积分功能,以及引申而出的排行榜、评价投票等等
8 文件上传
允许用户上传图片或其它与贴子相关的附件
9 反恶意灌水机制
隔段时间才能再发贴,或图片数字验证等方式
l0 后台日志
为记录论坛会员和管理员的各种动作,最好在系统中建立一套日志机制,便于出现问题时查询。这方面,首选Log4j({http://jakarta.apache.org/log4j/} )这个第三方组件来完成日志输出任务。
不过这些功能已经超出本文的范围,这里不再深入开发,有兴趣的读者可以自己完成。
4.3 Resin数据库连接池的使用
如果希望将数据库的配置放到应用之外进行,我们可以采用Resin的数据库连接池作为JDO的数据库入口。这里,我们可以采用一个虚拟的JDBC驱动来将Resin的连接池包装一下,象普通数据库一样给JDOGenie提供数据库连接服务。
具体的方法请参考JDOCentral上的代码共享论坛中的贴子:
{http://www.jdocentral.com/forums/index.php?act=ST&f=11&t=547&s=a8f11e9b4343a987a4dc8ed4988c7607}
4.4 团队开发的建议
如果读者在阅读完本篇文章后,带领一个团队JDO来真正开发WebApp数据库应用的话,最好采用CVS(客户端建议WinCVS)和BugZilla来控制代码变更、Bug跟踪和需求变化。当然,这些与JDO没有多大的关系,不过都是笔者实践过程中非常有用的经验。
5 JDO1.0局限性与JDO2.0
目前我们还只能在JDO1.0上开发数据库应用,由于JDO还比较年轻,自然存在一些局限性,下面列出影响较大的一些:
1. 增加额外步骤,配置复杂(相对于直接的JDBC)
2. 对数据模型有一定限制(必须有一个无参构造器,属性访问需要getter和setter)
3. 双向对象关系的处理太欠缺(JDO2.0计划中的自动维护的对象关系将解决这些)
4. JDOQL的API稍显累赘(declare一大堆东西,比ODMG的OQL标准还是不如)
5. 没有数据库统计功能(count(),max(),avg()等等,不过已经在JDO2.0计划中)
JDO2.0将会有专门针对关系数据库的子规范:JDO/R,其中将使JDO能覆盖大多数常用的数据库功能(基于SQL92标准)。当然,有些复杂的SQL操作还是需要JDBC才能完成的,笔者就处理过一些长达上百行的SQL语句。
JDO2.0专家组目前已经成立,并且已经召开JDO2.0启动会议,指派了负责规范的各个方面的人员,相信很快就会有一个新的Java规范请求(JSR)正式出现在{www.jcp.org}中。据专家估计,一年半后,JDO2.0的规范将会最终完成,同时,也将出现各个厂家推出的JDO2.0产品。
6 参考资料
JDO出世后,在Java世界引起很大反响,各种褒贬不一的文章层出不穷,包括JavaPro、IBM开发社区、JavaSkyline等技术网站中都有很多文章发表。O’Reilly、PrenticeHall等出版社也先后出版了几本JDO的书。相信以后还会有更多的资料出现。下面介绍一些最为重要的资源。
6.1 主力网站:{file:///D:/_BB/JavaResearch/www.jdocentral.com}
这个网站是JDO的核心网站,是推动JDO发展的主力。主流的JDO产品和文章都在这个网站上。Sun公司本身也是这个网章的主要成员之一。
有趣的是,这个网站的经典文章区不光收录了主要媒体上发表过的一些JDO相关文章,还收录了几乎所有的反对JDO的文章,这方面也体现了一点客观、公正的态度。
6.2 主力讨论区:{file:///D:/_BB/JavaResearch/www.jdocentral.com/forums}
作为JDO主力网站中的论坛,这里是JDO的功能、规范、接口、实例等等方面的热烈讨论的地方,很多JDO规范和细节在这里都有体现。这也是作为局外人(JDO Specification Expert Group之外)对JDO提出各种改进意见的最好的地点。目前规范中的一些API和JDO2.0提出的一些API方案就是来源于这里的讨论。
6.3 中文资源
前面提到的都主要是英文方面的网站,虽然权威,但对不熟英文的开发人员来说,帮助不是很大,下面介绍一些中文方面的JDO资源。
6.3.1 文章与评论集锦{file:///D:/_BB/JavaResearch/www.CSDN.net}
在{file:///D:/_BB/JavaResearch/www.csdn.net}上的文档搜索条中,输入“JDO”,并选择“文档标题”作为要搜索的目标,并点击“搜索”,将会列出一堆关于JDO的文章。当然,也有很多笔者的文章在其中,读者也可以查看笔者的专栏:{http://www.csdn.net/develop/author/netauthor/sun2bin/}
6.3.2 Q&A论坛:{http://www.javaresearch.org/forum/forum.jsp?column=308}
这是一个中文的专门为JDBC和JDO开发设立的论坛板块,有什么问题可以直接去这个论坛提问,或者看看别人都经历过什么样的问题。有很多常见问题都已经有详细的回答。
6.3.3 精华资料下载:{http://www.javaresearch.org/dn.jsp}
这个下载区中有一些关于JDO的书籍(PDF)、教程、示范代码等等,是学习的好材料,也是笔者当初学习JDO的入门资料。
本文的版权属于笔者本人,但欢迎转载,前提是注明出处和原作者。另外,欢迎在我的专栏中查看我的另几篇文章,并提出宝贵意见!
对该文的评论 人气:5318
JasonCao (2004-2-13 0:16:10)
建议开发使用MVC模式(比如,应用struts框架),这样就可以专注使用JDO构造MODEL一块的功能了。并且建议不要使用PersistentCapable接口的方法,因为这是JDOHancer自动在字节码上增加的,可以使用JDOHelper的一些方法,比如JDOHelper.getObjectId(pc)等。不过,本文也可以作为基本教程了,但作为实际应用开发,还是要使用MVC的,否则无法控制项目规模。
Reve (2004-1-29 23:51:06)
tutorial不错,学习这个tutorial,感觉不错,也准备用JDO的方法来改造自己的一个东西。不过JDOGenie并非免费,感觉可能会受限制其实有点盼望,mysql自己出自己的JDO的实现
andersonmao (2004-1-18 17:17:33)
JSP2.0的JSTL(如J2SDKEE1.4中) 不是 <%@taglib prefix="c" uri="http://java.sun.com/jstl/core" %> 是 <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> =============================================================== JSP EL 在Tomcat里 ${topic.jdoGetObjectId()} ${shortTime.format(topic.lastUpdate)} 方法不能运行,不是JSP2.0的规范吗?
andersonmao (2004-1-5 16:29:44)
支持一下
sun2bin (2003-12-19 19:08:35)
最新消息:JDOGenie2.2.0beta9今天发布了,改动数据类后可自动同步数据库结构! http://www.jdogenie.com/download.html
ycflypig (2003-12-18 22:22:17)
最近的J2EE中有JSP2.0
nanye18 (2003-12-18 12:28:07)
jeffyan77你搞错了!上SUN上看看哪个是新版吧。
jeffyan77 (2003-12-18 6:15:16)
JSP的最新版本是1.2,怎么会有JSP 2.0?
dengzi725 (2003-12-15 11:50:13)
lihai
mechiland (2003-12-12 18:48:49)
作者很用心,不错,捧一下场,辛苦了!
我们前面已经完成了一个基本的数据对象模型,并且,每个数据类的属性都用getter/setter进行了封装。这里重温一下这个数据模型:
这个类图是非常重要的,相当于传统JDBC开发的数据库结构图,但又比数据库结构图更高级一层。实际上,这个类图很容易使用UML工具生成,包括所有数据类的Java源代码。后面的真正的业务逻辑开发过程才是我们的重点,但这个类图却是贯穿整个开发过程的核心,数据库管理员可以根据这个类图来配置并生成数据库结构;开发人员按照这个类图进行对象获取和访问;美工人员可以根据这个类图来使用流行的可视化页面设计工具美化JSP页面(比如显示某个回复的主题标题,就按图中的结构使用${a_reply.topic.title})。
现在我们已经看到这个类图的重要性,并且也知道这个类图可以通过UML工具快速生成。因此,我们在开发之前几乎不需要任何手工编码,工作的重点就是与根据需求与相关开发人员讨论并确定这个类图结构。这里也体现了JDO开发的一个优点,就是开发之前不需要太多地在意具体的代码,而只需要在面向对象的层面上讨论确立类图的结构即可。讨论类图比讨论数据库结构要容易得多,一方面容易记忆,另一方面一般来说,一个类图会比相应的数据库结构图简单得多(比如一些双向的关系直接在类图上体现,不用象数据库一样需要一个额外的关联表来对应)。
接下来,我们在《配置篇》的基础上继续进行JDO版论坛的开发。请先快速地在脑海中回顾一下我们使用JDOGenie的工作台生成本应用的项目配置文件,并将数据类导入的过程,下面介绍与前面那些图形界面的操作等价的配置文件。对图形界面不感兴趣的读者也可以直接从这里开始阅读。
1 等价的配置文件代码
实际上,在《配置篇》中的一切配置步骤,就是为了生成两个文件:一个是描述所有数据类的元数据文件:WEB-INF/classes/system.jdo,其源码如下:
<?xml version="1.0" encoding="UTF-8"?>
<jdo>
<package name="jdobbs">
<class name="Post" />
<class name="Reply" persistence-capable-superclass="Post" />
<class name="Topic" persistence-capable-superclass="Post">
<field name="replies">
<collection element-type="Reply">
<extension vendor-name="jdogenie" key="inverse" value="topic" />
</collection>
</field>
</class>
</package>
</jdo>
另一个生成的文件是JDOGenie专用的配置文件,包含数据库连接、JDOGenie缓冲配置等等信息。源码如下:
# Server name (must be unique on a machine)
server=jdogenie1
remote.access=true
remote.pm=false
hyperdrive=true
# JDO Genie version
version=2.2.0beta7 (28 Nov 2003)
# Data stores
storeCount=1
store0.name=main
store0.type=jdbc
store0.driver=com.mysql.jdbc.Driver
store0.url=jdbc\:mysql\://localhost/test?useUnicode\=true&characterEncoding\=GBK
store0.db=mysql
store0.user=
store0.password=
store0.properties=
store0.maxActive=5
store0.maxIdle=3
store0.minIdle=2
store0.init.sql=
store0.validate.sql=
store0.retry.interval.ms=1000
store0.retry.count=10
store0.pscache.max=
store0.ext.jdbc-key-generator=-
store0.jdbc.namegen=-
# .jdo resources
jdoFileCount=1
jdo0=system.jdo
# Properties for JDOHelper.getPersistenceManagerFactory(...)
javax.jdo.PersistenceManagerFactoryClass=za.co.hemtech.jdo.client.BootstrapPMF
javax.jdo.option.Optimistic=true
javax.jdo.option.RetainValues=false
javax.jdo.option.RestoreValues=false
javax.jdo.option.IgnoreCache=false
javax.jdo.option.NontransactionalRead=true
javax.jdo.option.NontransactionalWrite=false
javax.jdo.option.Multithreaded=false
# Event logging
event.logging=-
log.downloader=-
# Cache settings
cache.enabled=true
cache.maxobjects=10000
cache.listener=-
query.cache.enabled=true
query.cache.max.queries=10000
# Data store 0 mappings
store0.jdbc.type.count=0
store0.jdbc.javatype.count=0
# Workbench properties (not used at runtime)
mdedit.classPathCount=2
mdedit.cp0=
mdedit.cp1=../lib/mysql.jar
# Workbench Ant configuration (not used at runtime)
ant.disabled=false
ant.buildfile=../build.xml
ant.compile=enhance
ant.show.all.targets=false
# Workbench Scripts (not used at runtime)
# JDO Genie Class Diagrams (not used at runtime)
diagram.count=1
diagram0.name=Diagram 1
diagram0.legend=299,24
diagram0.general=13,Y,Y
diagram0.class=Y,N,Y,N,N,Y,N,Y,N,N,N,N,N,N,N,N,N
diagram0.field=Y,Y,N,N,N,N
diagram0.class.count=3
diagram0.class0=jdobbs.Post,127,48
diagram0.class1=jdobbs.Reply,213,222
diagram0.class2=jdobbs.Topic,33,206
实际上,我们如果直接手工编写这两个文件的话,也可以不必进行前面那些配置工作。当然,估计很少有人会喜欢手工编码多过使用图形界面。
2 初步测试JDOGenie
至此我们已经有了数据库结构、有了增强过的数据类,已经可以正式开始使用JDO了。为了加强感性认识,我们先看看JDOGenie的运行情况:我们点击工作台左边的按钮“Grid”进入数据类列表区,选中某个类,点击上面工具条的第11个按钮“View all instances of the selected class…”或者直接按Ctrl+E,系统就会列出该类在数据库中的所有实例对象。不过我们这里还没有任何数据,所以什么也没列出来,以后可以通过这个功能查看某个类的所有实例。如果需要更细致的查询,可以进入左边四个按钮进入JDO查询区,使用JDOQL或者SQL来查询数据。JDOQL的查询界面如图:
好了,关于JDOGenie的细节已经讲完了。之所以用这么大的篇幅介绍JDOGenie的配置,目的是让读者能够最快速地了解JDO需要的配置信息,并能最快地看到JDO产品的运行。下面我们来真正地开始实现我们的论坛功能。
3 业务逻辑的实现
为了简单、直观地显示基于JDO的业务开发流程,我们在接下来的功能实现中采用JSP直接访问JDO API来实现数据对象的获取和操纵,就不再累赘地使用JavaBean对话控制器了。
为了使每个页面的代码更简单,并且又具备调用JDO API、使用常用的Java包、使用JSTL的功能,我们写一个专门的头页面,用于其它页面包含,这样,可以使用其它页面的代码更简洁易读。
<%@page
contentType="text/html; CHARSET=utf8"
import="jdobbs.*,java.util.*,javax.jdo.*,java.io.*"
%>
<%@taglib prefix="c" uri="http://java.sun.com/jstl/core" %>
<%
//预置几个日期格式化工具
pageContext.setAttribute("fullTime",new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
pageContext.setAttribute("shortTime",new java.text.SimpleDateFormat("MM-dd HH:mm"));
pageContext.setAttribute("fullDate",new java.text.SimpleDateFormat("yyyy-MM-dd"));
pageContext.setAttribute("shortDate",new java.text.SimpleDateFormat("MM-dd"));
%>
这里我们预置了几个用于显示日期的格式化对象,可以方便后面日期数据的显示。在Resin3中,可以用${shortTime.format(someDate)}来以显示类似“09-10 15:33”样式的日期字符串。也许在JSP2.0最终出台之前,这种方式未必规范,但既然服务器支持,就先这么用吧。
回顾一下论坛的功能需求,主要有以下几点:
1. 网友进入主页时,系统列出目前所有的主题,按时间顺序从近到远排列。
2. 网友可以在主页下方的发贴表单中发表新主题,包括标题和内容
3. 其它网友在主页的贴子列表中点击某个标题可以阅读这个主题的详细内容,包括所有的回贴内容。所有回贴按照时间顺序排列在主题贴后面 4. 可以在主题详细内容页面尾部的回贴表单中回复这个主题
5. 每个网友发表主题或贴子时,系统需要记录该贴子发表时的客户端IP地址
6. 系统提供一个高级搜索功能,让网友可以根据时间、主题标题、内容或回复内容、IP搜索主题
我们接下来一个一个地实现这些功能。
3.1 主页:列出所有主题
我们在index.jsp中执行一个不带条件的JDO查询,并按时间倒序用HTML中的表格来显示一条一条的主题。我们将index.jsp的代码改动如下:
<%@page contentType="text/html; CHARSET=utf8" %>
<%@include file="/inc.jsp" %>
<%
//先取得主题列表:
Query q = Sys.pm().newQuery(Topic.class,"");
q.setOrdering("postTime descending");
pageContext.setAttribute("topics",q.execute());
%>
<title>JDO BBS on ${pageContext.request.serverName}</title>
您现在的位置:<b>JDO BBS 首页</b>
<h3>欢迎访问JDOBBS!</h3>
<table border=1>
<tr><th>标题</th><th>内容长度</th><th>发表时间</th><th>IP地址</th><th>回复数</th><th>最后回复</th></tr>
<c:forEach var="topic" items="${topics}">
<tr>
<td><a href="topic.jsp?id=${topic.jdoGetObjectId()}">${topic.title}</a></td>
<td>${topic.length}</td>
<td>${shortTime.format(topic.postTime)}</td>
<td>${topic.ip}</td>
<td>${topic.replyCount}</td>
<td>${shortTime.format(topic.lastUpdate)}</td>
</tr>
</c:forEach>
</table>
在这段代码中,我们先是使用了一人JDO的javax.jdo.Query来执行查询,对这个Query设置了一个排序参数:“postTime descending”,这个设置使JDO生成“… order by POST_TIME”的SQL子句,完成排序。在JDO中,也可以使用多个排序字段,用“,”号隔开即可。值得一提的是,JDO的排序字段可以是通过引用到达的另一个对象的属性,比如查询Reply对象时,可以按其所回复的主题的回复数进行排序,代码是:
Query query = pm.newQuery(Reply.class,””);
query.setOrdering(“topic.replyCount ascending”); //注意正序也必须写上“ascending”
这两行简单易懂的代码将产生类似
select …
FROM post a LEFT JOIN post AS b ON (a.topic = b.post_id)
WHERE a.jdo_class = 54451616
ORDER BY b.reply_count
的SQL语句。如果数据库不是MySQL而是Oracle或其它数据库的话,这条SQL语句还会有不同的形式。从中也可以看出JDO的透明性,你只需要按最直接的想法设置排序(“topic.replyCount ascending”),而不用去考虑联表的SQL如何编写,更不用考虑不同的数据库的语法细节。
由于数据库中还没有数据,我们这里就不给出显示效果,留待后面再说。需要说明的一点是:使用JSP2.0编写的这个index.jsp页面很容易在一般的页面编辑器中进行维护。比如这个页面在DreamweaverMX中编辑时的界面是:
也许熟悉DreamWeaver的读者会觉得这个表格会将页面撑得比较宽影响美工设计,比如只需要显示顶多3位数字的“回复数”字段在编辑页面时得写成“${topic.replyCount}”,从而将这一单元格撑得过宽,实际上,这样的问题可以通过一个小技巧解决:将该处先写成一个随意的数,再将这个数包(Wrap)上层JSTL表达式“c:out”即可,相应的HTML源码是:<td><c:out value=”${topic.replyCount}”>123</c:out></td>。关于这方面的技巧还有一些,都属于JSP2.0页面美工的内容,这里不再一一描述。
现在我们继续完成下面的“发表新主题”功能来产生数据。
3.2 发表新主题
为了新发表贴子,我们需要一个输入表单,现在将这个表单做到首页主题列表的后面,并让这个表单提交到一个名为index!post.jsp的页面,这个接收页面解析提交的参数,并使用JDO API生成新的主题贴子,保存到数据库中。
先在index.jsp尾部增加一个表单:
<form name=fmPost method=post action="index!post.jsp">
请在这里发表新主题。
<br>标题:<br><input name=title maxlength=100 size=100>
<br>内容:<br><textarea name=content cols=100 rows=10></textarea>
<br><input type=submit value=提交>
</form>
然后编写接收发贴表单提交的index!post.jsp:
<%@page contentType="text/html; CHARSET=utf8" %>
<%@include file="/inc.jsp" %>
<%
Topic topic = new Topic();
topic.setTitle(request.getParameter("title"));
topic.setContent(request.getParameter("content"));
topic.setLength(topic.getContent().length());
topic.setPostTime(new Date());
topic.setLastUpdate(new Date());
topic.setIp(request.getRemoteAddr());
Sys.pm().currentTransaction().begin();
Sys.pm().makePersistent(topic);
Sys.pm().currentTransaction().commit();
pageContext.setAttribute("topic",topic);
%>
您的主题已经发表,请选择下列操作之一:
<p><a href="index.jsp">返回主题列表</a>
<p><a href="topic.jsp?id=${topic.jdoGetObjectId()}">进入该主题页面</a>
我们发贴的过程先是生成一个Topic对象,然后使用JDO的API开始一个事务,标记这个新生成的topic对象为存储实例,然后提交事务,这样就完成了对象的保存过程。当事务提交后,这个topic对象就具备了由JDOGenie生成的一个数据库标识,所以我们可以在页面尾部的“进入该主题页面”链接中使用其标识:${topic.jdoGetObjectId()}
现在我们就可以发表主题了。发表了一系列主题后,我们的论坛主页就比较充实了:
[img]http://lcspace.nease.net/c-j2ee/new-9.files/CSDN_Dev_Image_2003-12-62248498.jpg[img]
3.3 阅读主题及回复
前面的主题列表页面中的每个主题已经有链接到topic.jsp页面,现在我们来编写这个页面:
<%@page contentType="text/html; CHARSET=utf8" %>
<%@include file="/inc.jsp" %>
<%
//先取得主题:
String oid = request.getParameter("id");
Object objectId = Sys.pm().newObjectIdInstance(Topic.class,oid);
Topic topic = (Topic)Sys.pm().getObjectById(objectId,false);
pageContext.setAttribute("topic",topic);
%>
<title>主题:${topic.title}</title>
您现在的位置:<a href=".">JDO BBS 首页</a> --> <b>主题:${topic.title} </b>
<p>内容:<pre><b>${topic.content}</b></pre>
下面是回复列表:
<table border=1>
<tr><th>回复时间</th><th>IP地址</th><th>内容长度</th><th>回复内容</th></tr>
<c:forEach var="reply" items="${topic.replies}">
<tr> <a name="#${reply.jdoGetObjectId()}">
<td>${shortTime.format(reply.postTime)}</td>
<td>${reply.ip}</td>
<td>${reply.length}</td>
<td><pre>${reply.content}</td>
</tr>
</c:forEach>
</table>
我们在页面开始先通过访问本页面时必须给出的“id”参数得到一个Topic对象,并将其设置到pageContext中,剩下的代码就是利用JSP2.0显示其内容,包括所有的回复。我们先不给出显示效果,而是接着实现回复主题功能:
3.4 回复主题
先在刚才的主题页面尾部加上回复表单:
<form name=fmReply method=post action="topic!reply.jsp">
请在这里回复本主题。
<input type=hidden name=topicId value="${topic.jdoGetObjectId()}">
<br>内容:<br><textarea name=content cols=100 rows=10></textarea>
<br><input type=submit value=回复>
</form>
然后,编写一个接收回复提交的页面:topic!reply.jsp:
<%@page contentType="text/html; CHARSET=utf8" %>
<%@include file="/inc.jsp" %>
<%
Sys.pm().currentTransaction().begin();
//先取得被回复的主题:
String oid = request.getParameter("topicId"); //回复表单中引用的主题贴标识
Object objectId = Sys.pm().newObjectIdInstance(Topic.class,oid);
Topic topic = (Topic)Sys.pm().getObjectById(objectId,false);
//增加回复:
Reply reply = new Reply();
reply.setContent(request.getParameter("content"));
reply.setLength(reply.getContent().length());
reply.setPostTime(new Date());
reply.setIp(request.getRemoteAddr());
topic.getReplies().add(reply); //将新回复加到主题的回复列表中
topic.setReplyCount(topic.getReplyCount()+1); //回复计数加1
topic.setLastUpdate(new Date());
Sys.pm().currentTransaction().commit();
pageContext.setAttribute("topic",topic);
%>
您的回复已经发表,请选择下列操作之一:
<p><a href="index.jsp">返回主题列表</a>
<p><a href="topic.jsp?id=${param.topicId}">进入所回复的主题页面</a>
在这个页面的开始部分,我们先通过回复表单传递过来的被回复的主题标识取得该主题,然后新生成一个Reply对象,设置好其属性后,将这个新生成的Reply对象添加到Topic的replies列表中。我们这段代码中并没有使用pm.makePersistent()方法,因为将reply对象加到topic对象的replies列表中时,JDO已经标记该reply对象需要保存,这个特性就是JDO中的“可达性存储”概念。如果一个新生成的对象被一个已经存在于数据库中的对象引用,那么,提交事务时这个新对象将被保存,甚至如果这个新对象又引用了另一个新生成的对象,另一个新对象也会被保存。这个特性有时候可以减少我们的代码复杂性。
细心的读者可能注意到了,最后一行代码中使用了“${param.topicId}”来直接将提交参数中的主题贴标识拼装到返回主题页面的链接中。这使用到了JSP2.0的隐含环境变量param。
我们测试回复功能,回复了几个贴子后,主题页面的效果如图所示:
而回到论坛首页,界面如下图:
3.5 高级搜索功能
到此,我们已经完成了基本的发贴和回复功能,现在做一个组合条件搜索的功能,以体现JDOQL的特点。
回顾一下搜索功能需求:“让网友可以根据时间、主题标题、内容或回复内容、IP搜索主题”,我们需要编写一个搜索页面,包含一个搜索条件输入表单,这个表单直接提交到本页面,提交后,表单中将显示所输入的条件,而在表单下方,列出符合条件的主题。
源码 search.jsp:
<%@page contentType="text/html; CHARSET=utf8" %>
<%@include file="/inc.jsp" %>
<title>主题:${topic.title}</title>
您现在的位置:<a href=".">JDO BBS 首页</a> --> <b>搜索论坛主题</b>
<form name=fmSearch>
请在下面输入搜索条件:
<br>发贴日期:
<select name="postTime">
<option value="">不限</option>
<option value="week">一周内</option>
<option value="month">一月内</option>
<option value="year">一年内</option>
</select>
<script>fmSearch.postTime.value = "${param.postTime}"; </script>
<br>标题包含:
<input name=title value="${param.title}">
<br>内容或回复贴的内容包含:
<input name=content value="${param.content}">
<br>发贴IP:
<input name=ip value="${param.ip}">
<br><input type=submit value=搜索>
</form>
<%
//这里进行JDO查询:
Query q = Sys.pm().newQuery(Topic.class);
String jdoql = "";
String paramNames = "";
List params = new ArrayList();
String postTime = request.getParameter("postTime");
if(postTime != null && !postTime.equals("")) {
jdoql += " && postTime >= _time";
paramNames += ",Date _time";
if(postTime.equals("week")) { //最近一周
params.add(new Date(System.currentTimeMillis()-7l*24*60*60*1000));
} else if(postTime.equals("month")) { //最近一月
params.add(new Date(System.currentTimeMillis()-30l*24*60*60*1000));
} else { //最近一年
params.add(new Date(System.currentTimeMillis()-365l*24*60*60*1000));
}
}
String title = request.getParameter("title");
if(title != null && !title.trim().equals("")) {
jdoql += " && title.startsWith(_title)";
paramNames += ",String _title";
params.add("%"+title);
}
String content = request.getParameter("content");
if(content != null && !content.trim().equals("")) {
//主题内容或者某个回复的内容包含子串:
jdoql += " && (content.startsWith(_content) || replies.contains(aReply) && aReply.content.startsWith(_content))";
paramNames += ",String _content";
params.add("%"+content);
q.declareVariables("Reply aReply");
}
String ip = request.getParameter("ip");
if(ip != null && !ip.trim().equals("")) {
jdoql += " && ip == _ip";
paramNames += ",String _ip";
params.add(ip.trim());
}
if(jdoql.startsWith(" && ")) jdoql = jdoql.substring(4);
if(paramNames.startsWith(",")) paramNames = paramNames.substring(1);
q.setFilter(jdoql);
q.declareParameters(paramNames);
pageContext.setAttribute("topics",q.executeWithArray(params.toArray()));
%>
下面是符合条件的主题列表: &&<a href="search.jsp">搜索主题</a>&&<input type=button value="发表新主题" onclick="fmPost.title.focus()">
<table border=1>
<tr><th>标题</th><th>内容长度</th><th>发表时间</th><th>IP地址</th><th>回复数</th><th>最后回复</th></tr>
<c:forEach var="topic" items="${topics}">
<tr>
<td><a href="topic.jsp?id=${topic.jdoGetObjectId()}">${topic.title}</a></td>
<td>${topic.length}</td>
<td>${shortTime.format(topic.postTime)}</td>
<td>${topic.ip}</td>
<td>${topic.replyCount}</td>
<td>${shortTime.format(topic.lastUpdate)}</td>
</tr>
</c:forEach>
</table>
这个页面执行了一个根据提交的搜索条件拼装的JDOQL,完成了搜索功能。界面如下:
4 功能扩展
本章中将讨论一些对这个论坛功能进行增强或者解决一些传统JDBC难以解决的问题。
4.1 字符串长度限制
可能大家有过这样的经验:在一个论坛上原创了一篇很长的文章,结果提交时服务器却提示数据库字段长度不够,只能拆成几贴来发表。这种情况并不少见,有点影响发贴者的积极性。于是,我们希望能够做到:对这种长文本类型的输入信息不作长度限制,想输多少就输多少。即使要限制长度,也是通过其它方式限制,而不是被动地受数据库服务器的限制。
现在我们可以利用JDOGenie对java.util.List的支持(主流的JDO产品都支持这个JDO可选特性)来技巧性地实现无限制的字符串。方法是将Post.content属性的声明改为List,并在system.jdo中设置该List的元素类型为java.lang.String(通过JDOGenie工作台进行设置),并在Post.content的getter/setter方法中进行一些处理(方法的声明不需要改变,调用的JSP也不需要改变)。具体方法请参考JDOCentral上的笔者共享出来的一个JDO长字符串处理工具类:{http://www.jdocentral.com/forums/index.php?act=ST&f=11&t=564&s=56744171186d115a997fdefbdb46d4a5} 。
原理清楚后,我们将这段工具代码放到Sys类中,在Sys.java中加入两个新的函数:
/**
* 将长字符串分割存放在一个列表中
* @param value 需要设置的字符串
* @return List类型的字符串片断列表,请将之设到JDO对象的相关属性中。
*/
public static List setLongString(String value) {
if(value == null) return null;
int len = value.length();
int count = (len+partSize-1)/partSize;
List list = new ArrayList(count);
for(int i = 0; i < count; i++) {
int from = i*partSize;
list.add(value.substring(from,Math.min(from+partSize,len)));
}
return list;
}
/**
* 从片断列表中取得长字符串
* @param list 片断列表
* @return 拼装后的长字符串
*/
public static String getLongString(List list) {
if(list == null) return null;
if(list.size() == 1) return (String)list.get(0); //for better performance
//here some cache may be used for faster speed
StringBuffer sb = new StringBuffer();
for(Iterator itr = list.iterator(); itr.hasNext(); ) sb.append(itr.next());
return sb.toString();
}
private static int partSize = 2000;
然后,在Post类中进行一下修改:
/** 贴子内容 */
List content;
public String getContent() {
return Sys.getLongString(content);
}
public void setContent(String value) {
content = Sys.setLongString(value);
}
最后,还需要修改一下元数据文件system.jdo(可在JDOGenie工作台中进行):
<?xml version="1.0" encoding="UTF-8"?>
<jdo>
<package name="jdobbs">
<class name="Post">
<field name="content">
<collection element-type="java.lang.String" />
</field>
</class>
<class name="Reply" persistence-capable-superclass="Post" />
<class name="Topic" persistence-capable-superclass="Post">
<field name="replies">
<collection element-type="Reply">
<extension vendor-name="jdogenie" key="inverse" value="topic" />
</collection>
</field>
</class>
</package>
</jdo>
到此为止,代码就算是修改完成了,现在只剩下数据库结构还没有同步。如果不用保留前面的数据,我们简单地在JDOGenie工作台中重建数据库即可,如果需要保留数据,我们可以看看新的数据结构与旧的有什么差别,进行相应的“ALTER TABLE …”和数据复制“INSERT INTO … ”即可。本例中需要进行的SQL操作是:
create table post_content (
post_id INTEGER not null,
seq INTEGER not null,
val VARCHAR(255),
constraint pk_post_content primary key (post_id, seq)
) TYPE = InnoDB;
insert into post_content
select post_id,1,content from post;
alter table post drop column content;
还有一点需要注意的是,针对Post.content进行查询的JDOQL需要从
content.startsWith(…)
改成:
content.contains(s) && s.startsWith(…)
4.2 更多扩展功能
到现在为止,我们已经用JDO和JSP2.0完成了一个具备基本功能的论坛系统,在此基础上,我们还可以扩展更多的功能,比如:
l 贴子列表的分页处理
2 论坛分板块
3 会员功能
4 个性化
5 版主功能
6 审核
对发贴内容进行审查,通过才能显示出来
7 积分
为调动用户积极性,加上积分功能,以及引申而出的排行榜、评价投票等等
8 文件上传
允许用户上传图片或其它与贴子相关的附件
9 反恶意灌水机制
隔段时间才能再发贴,或图片数字验证等方式
l0 后台日志
为记录论坛会员和管理员的各种动作,最好在系统中建立一套日志机制,便于出现问题时查询。这方面,首选Log4j({http://jakarta.apache.org/log4j/} )这个第三方组件来完成日志输出任务。
不过这些功能已经超出本文的范围,这里不再深入开发,有兴趣的读者可以自己完成。
4.3 Resin数据库连接池的使用
如果希望将数据库的配置放到应用之外进行,我们可以采用Resin的数据库连接池作为JDO的数据库入口。这里,我们可以采用一个虚拟的JDBC驱动来将Resin的连接池包装一下,象普通数据库一样给JDOGenie提供数据库连接服务。
具体的方法请参考JDOCentral上的代码共享论坛中的贴子:
{http://www.jdocentral.com/forums/index.php?act=ST&f=11&t=547&s=a8f11e9b4343a987a4dc8ed4988c7607}
4.4 团队开发的建议
如果读者在阅读完本篇文章后,带领一个团队JDO来真正开发WebApp数据库应用的话,最好采用CVS(客户端建议WinCVS)和BugZilla来控制代码变更、Bug跟踪和需求变化。当然,这些与JDO没有多大的关系,不过都是笔者实践过程中非常有用的经验。
5 JDO1.0局限性与JDO2.0
目前我们还只能在JDO1.0上开发数据库应用,由于JDO还比较年轻,自然存在一些局限性,下面列出影响较大的一些:
1. 增加额外步骤,配置复杂(相对于直接的JDBC)
2. 对数据模型有一定限制(必须有一个无参构造器,属性访问需要getter和setter)
3. 双向对象关系的处理太欠缺(JDO2.0计划中的自动维护的对象关系将解决这些)
4. JDOQL的API稍显累赘(declare一大堆东西,比ODMG的OQL标准还是不如)
5. 没有数据库统计功能(count(),max(),avg()等等,不过已经在JDO2.0计划中)
JDO2.0将会有专门针对关系数据库的子规范:JDO/R,其中将使JDO能覆盖大多数常用的数据库功能(基于SQL92标准)。当然,有些复杂的SQL操作还是需要JDBC才能完成的,笔者就处理过一些长达上百行的SQL语句。
JDO2.0专家组目前已经成立,并且已经召开JDO2.0启动会议,指派了负责规范的各个方面的人员,相信很快就会有一个新的Java规范请求(JSR)正式出现在{www.jcp.org}中。据专家估计,一年半后,JDO2.0的规范将会最终完成,同时,也将出现各个厂家推出的JDO2.0产品。
6 参考资料
JDO出世后,在Java世界引起很大反响,各种褒贬不一的文章层出不穷,包括JavaPro、IBM开发社区、JavaSkyline等技术网站中都有很多文章发表。O’Reilly、PrenticeHall等出版社也先后出版了几本JDO的书。相信以后还会有更多的资料出现。下面介绍一些最为重要的资源。
6.1 主力网站:{file:///D:/_BB/JavaResearch/www.jdocentral.com}
这个网站是JDO的核心网站,是推动JDO发展的主力。主流的JDO产品和文章都在这个网站上。Sun公司本身也是这个网章的主要成员之一。
有趣的是,这个网站的经典文章区不光收录了主要媒体上发表过的一些JDO相关文章,还收录了几乎所有的反对JDO的文章,这方面也体现了一点客观、公正的态度。
6.2 主力讨论区:{file:///D:/_BB/JavaResearch/www.jdocentral.com/forums}
作为JDO主力网站中的论坛,这里是JDO的功能、规范、接口、实例等等方面的热烈讨论的地方,很多JDO规范和细节在这里都有体现。这也是作为局外人(JDO Specification Expert Group之外)对JDO提出各种改进意见的最好的地点。目前规范中的一些API和JDO2.0提出的一些API方案就是来源于这里的讨论。
6.3 中文资源
前面提到的都主要是英文方面的网站,虽然权威,但对不熟英文的开发人员来说,帮助不是很大,下面介绍一些中文方面的JDO资源。
6.3.1 文章与评论集锦{file:///D:/_BB/JavaResearch/www.CSDN.net}
在{file:///D:/_BB/JavaResearch/www.csdn.net}上的文档搜索条中,输入“JDO”,并选择“文档标题”作为要搜索的目标,并点击“搜索”,将会列出一堆关于JDO的文章。当然,也有很多笔者的文章在其中,读者也可以查看笔者的专栏:{http://www.csdn.net/develop/author/netauthor/sun2bin/}
6.3.2 Q&A论坛:{http://www.javaresearch.org/forum/forum.jsp?column=308}
这是一个中文的专门为JDBC和JDO开发设立的论坛板块,有什么问题可以直接去这个论坛提问,或者看看别人都经历过什么样的问题。有很多常见问题都已经有详细的回答。
6.3.3 精华资料下载:{http://www.javaresearch.org/dn.jsp}
这个下载区中有一些关于JDO的书籍(PDF)、教程、示范代码等等,是学习的好材料,也是笔者当初学习JDO的入门资料。
本文的版权属于笔者本人,但欢迎转载,前提是注明出处和原作者。另外,欢迎在我的专栏中查看我的另几篇文章,并提出宝贵意见!
对该文的评论 人气:5318
JasonCao (2004-2-13 0:16:10)
建议开发使用MVC模式(比如,应用struts框架),这样就可以专注使用JDO构造MODEL一块的功能了。并且建议不要使用PersistentCapable接口的方法,因为这是JDOHancer自动在字节码上增加的,可以使用JDOHelper的一些方法,比如JDOHelper.getObjectId(pc)等。不过,本文也可以作为基本教程了,但作为实际应用开发,还是要使用MVC的,否则无法控制项目规模。
Reve (2004-1-29 23:51:06)
tutorial不错,学习这个tutorial,感觉不错,也准备用JDO的方法来改造自己的一个东西。不过JDOGenie并非免费,感觉可能会受限制其实有点盼望,mysql自己出自己的JDO的实现
andersonmao (2004-1-18 17:17:33)
JSP2.0的JSTL(如J2SDKEE1.4中) 不是 <%@taglib prefix="c" uri="http://java.sun.com/jstl/core" %> 是 <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> =============================================================== JSP EL 在Tomcat里 ${topic.jdoGetObjectId()} ${shortTime.format(topic.lastUpdate)} 方法不能运行,不是JSP2.0的规范吗?
andersonmao (2004-1-5 16:29:44)
支持一下
sun2bin (2003-12-19 19:08:35)
最新消息:JDOGenie2.2.0beta9今天发布了,改动数据类后可自动同步数据库结构! http://www.jdogenie.com/download.html
ycflypig (2003-12-18 22:22:17)
最近的J2EE中有JSP2.0
nanye18 (2003-12-18 12:28:07)
jeffyan77你搞错了!上SUN上看看哪个是新版吧。
jeffyan77 (2003-12-18 6:15:16)
JSP的最新版本是1.2,怎么会有JSP 2.0?
dengzi725 (2003-12-15 11:50:13)
lihai
mechiland (2003-12-12 18:48:49)
作者很用心,不错,捧一下场,辛苦了!
相关推荐
总之,JDO2-API-2.0是一个强大且灵活的数据持久化框架,它简化了Java应用与数据库之间的交互,提高了开发效率,并允许开发者专注于业务逻辑而不是底层的数据库操作。通过深入理解和熟练使用JDO,可以构建高效、可...
《Spring2.0开发参考手册(中文)》是一份深入探讨Spring框架2.0版本的权威指南,旨在帮助开发者全面理解和应用这一强大的Java企业级应用框架。Spring以其依赖注入、AOP(面向切面编程)和模块化设计,极大地简化了...
Java 数据对象(JDO,Java Data Objects)是一种用于在Java应用程序中访问关系数据库的标准API。...通过理解JDO的基本原理和开发流程,开发者可以有效地利用它来构建高效、易于维护的Java应用程序。
开发JDO应用的基本步骤包括: - **设计数据模型**:定义Java类作为数据模型,通常继承自`javax.jdo.PersistenceCapable`接口。 - **编写元数据**:使用XML文件或注解来定义Java类与数据库表的映射。 - **配置JDO...
《JDO原理及开发》文档详细介绍了JDO的基础知识和实际应用,对理解JDO的工作原理和开发模式具有极大帮助。通过学习,开发者可以更好地利用JDO来简化数据访问层的开发,提高开发效率,同时保持代码的可维护性和灵活性...
对JDO 1.0/2.0的支持。外部依赖spring-jdbc, JDO API, (spring-web)。
SQL Map 使用简单的 XML 配置文件将 Java Bean 映射成 SQL 语句,对比其他的数据库持续层和 ORM 框架(如 JDO 的实现, Hibernate 等), SQL Map 最大的优点在于它简单易学。要使用 SQL Map,只要熟悉Java Bean, ...
【JAVA Web应用开发:J2EE课件】涵盖了J2EE平台的核心技术和开发流程,适合初学者和进阶者深入理解。以下是对每个文件主要内容的详细解析: 1. **1. 介绍.ppt** - 这部分内容通常会涵盖J2EE(Java 2 Platform, ...
3.8.4. ApplicationContext在WEB应用中的实例化 3.9. 粘合代码和可怕的singleton 3.9.1. 使用Singleton-helper类 4. 资源 4.1. 简介 4.2. Resource 接口 4.3. 内置 Resource 实现 4.3.1. UrlResource 4.3.2. ...
综上所述,将JDO技术应用于Struts框架不仅可以充分发挥Struts框架在Web应用开发方面的优势,还可以利用JDO技术在数据管理和持久化方面的特点,实现更加高效和灵活的Web应用开发。通过构建一个多层体系结构模型,可以...
本书介绍了JSP在数据库系统中的运用,并对如何构建高效的电子商务应用系统,开发各种中间...本书可作为ERP应用开发人员和广大程序设计人员的指导书,也可作为高等院校师生教学和自学参考书、各类培训机构的培训用书。
4. **Web MVC框架**:Spring Web MVC框架在2.0版本中也有了显著提升,提供了模型-视图-控制器架构,支持自定义视图解析器,以及与各种视图技术(如JSP、FreeMarker、Velocity等)的集成,让Web应用开发更加灵活。...
jdo2.jar.................
查询语言的改进是JDO2.0规范中的重要环节,本文从较高的层面阐述JDO2.0所提供的一些新功能。由于JDO2.0规范还未进入公开草案状态,目前还没有任何内容敲定下来,一切都还可能面临变化。不过,JDO2.0将会很快进入最后...
在基于JSP的Web应用开发中,选择合适的数据库连接技术对于提升应用性能和可维护性至关重要。JDBC作为基础的数据库连接技术,虽然功能强大,但在实际应用中通常会被更高级的技术所替代或封装。JDO和Hibernate等技术因...
Struts2、Spring、JDO(Java Data Objects)和AJAX(Asynchronous JavaScript and XML)是四个在Web应用开发中非常关键的技术。这篇博客“Struts2,Spring,JDO,AJAX on GAE”可能探讨了如何在Google App Engine (GAE)...
4. **Web MVC 框架**:Spring 2.0 对 Web 层进行了重大改进,提供了更丰富的视图解析器、拦截器等机制,使得 Web 应用程序的开发更加灵活。 5. **数据访问层**:增加了对 JDBC、Hibernate 和 JDO 等持久化技术的...
查询语言的改进是JDO2.0规范中的重要环节,本文从较高的层面阐述JDO2.0所提供的一些新功能。由于JDO2.0规范还未进入公开草案状态,目前还没有任何内容敲定下来,一切都还可能面临变化。不过,JDO2.0将会很快进入最后...
### JDO 持久层框架教程 #### 安装 Devtool 插件 ##### 要求 在开始安装 Devtool 插件之前,请确保满足以下条件: 1. **Java 运行环境**:必须安装 Java 2 SDK 1.4 或更高版本。 2. **Eclipse 版本**:必须安装...
Spring框架是一个全面的企业级应用开发框架,核心特性包括依赖注入(Dependency Injection,DI)、面向切面编程(Aspect-Oriented Programming,AOP)以及支持多种数据访问技术,如JDBC、Hibernate和JDO。Spring 2.0...