导读
在项目开发中如何进行需求分析,如何规划数据库、数据库建模以及怎样将模式设计应用于具体的项目困扰了开发人员。本文以一个在线课程项目为线索全景展现软件开发的各个步骤。
阅读导航
导航
|
简介
|
CoursesOnline简介 | 简要介绍CoursesOnline系统的设计目的 |
开发环境 | 介绍开发工具 |
需求分析 | CoursesOnline系统开发初期的需求调研,数据库建模、数据字典、数据表的规划 |
CoursesOnline系统设计 | 在系统设计中引入设计模式,如何选择设计模式 |
创建数据库 | 在Oracle9i中创建数据库 |
CoursesOnline的具体实现 | 用JBuilder9开发CoursesOnline系统步骤 |
部分代码 | 部分步骤代码 |
附录 | Jbuilder9、Oracle9i、Weblogic7的安装配置;数据表SQL脚本 |
一、CoursesOnline简介
CoursesOnline是一个实验性质的系统,CoursesOnline是"在线课程"的意思,在这个CoursesOnline系统里,学生选择课程,老师可以开设课程,系统管理员则对学生、老师以及课程进行管理。
CoursesOnline使用J2EE来实现,目的是为其它EJB系统的开发提供一些参考。
二、开发环境
CoursesOnline使用Jbuilder9 + Oracle9i + WebLogic7的开发环境。
因为J2EE是一种行业标准,所以采用哪种开发环境的搭配并不是最重要的。目前其它常见开发环境的搭配还有Eclipse + MySQL + Jboss(都是OpenSource),VJA + DB2 + WebSphere,等等。
Jbuilder9 + Oracle9i + WebLogic 7开发环境的配置请参考附录A。
三、CoursesOnline需求分析
3.1 CoursesOnline用例
上图是CourcesOnline的Use Case Diagram。显而易见,系统中有学生、老师和系统管理员三种Actor。学生需要注册成系统用户后才能浏览课程和选课。
3.2 数据库建模(ER图)及数据字典
上图是CoursesOnline的数据库ER图,建模工具是ERWin。顺便提一下,ERWin的正向工程支持ER图直接生成数据库表结构,逆向工程支持数据库表结构生成ER图。
CoursesOnline使用到的表有5个,数据字典如下:
3.2.1 Actor登录信息表(Actor)
序号 | 字段名 | 数据类型 | 约束 | 备注 |
1 | ActorID | SmallInt | PK | Actor标识符 |
2 | UserName | Varchar(20) | Actor登录帐号 | |
3 | Password | Char(8) | Actor登录口令 | |
4 | ActorType | SmallInt | Actor类型,0:系统管理员;1:老师;2:学生 |
3.2.2 Actor基本信息表(ActorInfo)
序号 | 字段名 | 数据类型 | 约束 | 备注 |
1 | ActorID | SmallInt | FK(Actor) | Actor标识符 |
2 | ActorName | Varchar(20) | Actor姓名 | |
3 | Phone | Varchar(16) | 电话 | |
4 | Varchar(50) |
3.2.3 课程信息表(Courses)
序号 | 字段名 | 数据类型 | 约束 | 备注 |
1 | CoursesID | SmallInt | PK | 课程标识符 |
2 | CoursesName | Varchar(20) | 课程名称 | |
3 | StartDate | Date | 课程开始时间 | |
4 | EndDate | Date | 课程结束 | |
5 | ActorID | SmallInt | FK(Actor) | |
6 | RoomID | SmallInt | FK(Room) | 教室标识符 |
3.2.4 学生选课表(Appointment)
序号 | 字段名 | 数据类型 | 约束 | 备注 |
1 | ActorID | SmallInt | FK(Actor) | Actor标识符(学生,ActorType=2) |
2 | CoursesID | SmallInt | FK(Courses) | 课程标识符 |
3.2.5 教室信息表(Room)
序号 | 字段名 | 数据类型 | 约束 | 备注 |
1 | RoomID | SmallInt | PK | 教室标识符 |
2 | RoomName | Varchar(30) | 教室名称 |
四、CoursesOnline系统设计
4.1 Design Pattern的选择和思考
在系统设计上,Design Pattern的选择是很重要的。因为正确的Design Pattern不仅在开发阶段可以让开发人员思路清晰得心应手,而且在维护阶段也不至于让维护人员抓狂。除此之外,对于系统的健壮和运行效率而言也起着举足轻重的作用。如果采用了错误的Design Pattern,那么对于系统来说就像是在错误的时间错误的地点与错误的敌人打了一场错误的战争。
下面以CoursesOnline系统学生注册时采用的两种不同的处理方法为例来简单说明选择Design Pattern的重要性。
学生在Client提交注册信息,包括登录帐号,登录口令,姓名,电话和Email五项内容。从数据库ER图中我们可以看到学生信息被分散在两张表里,也就是说服务器端有两个Entity Bean来存取学生的注册信息,一个为Actor,另一个为ActorInfo。
方法一:客户端直接与Entity Bean沟通以完成工作
方法二:客户端与Session Bean交互,由Session Bean与Entity Bean沟通以完成工作
方法一的设计虽然也能完成对学生注册的处理,但是客户端必须写所有的业务逻辑代码,而且由于客户端直接访问Entity Bean,不但造成了多次的网络roundtrip,使执行效率大幅下降,也使客户端与Entity Bean形成强耦合,日后不管修改客户端还是Entity Bean,都会牵一发而动全身,对系统的改造简直就是一场灾难。
方法二不但大幅降低了网络的roundtrip,而且Session Facade分隔了客户端和Entity Bean,Entity Bean对于客户端来说是透明的,客户端需要关心的只是Session Facade提供的接口。这样一来系统的可扩展性就得到了质的提升。
4.2 CoursesOnline系统示意图
五、创建数据库
先有鸡再有蛋?还是先有蛋再有鸡?
先有数据库表再有Entity Bean?还是先有Entity Bean再有数据库表?
这两个问题有异曲同工之妙。鸡与蛋的问题已经讨论几千年了,哲学的Big Fans可能会争的脸红耳赤唾沫横飞;偶们只是普通的程序员,谁先谁后的问题还留给理论学家吧。
可能会有人先设计Entity Bean再建数据库表,也有可能反其道而行。在CoursesOnline这个实验性质系统的开发过程中,是先建数据库表然后才有Entity Bean。
5.1 新建一个数据库
Oracle可以在命令行模式下敲入dbca,或者直接在开始菜单里找到并运行Database Configuration Assistant,然后根据向导的提示新建一个名为CoursesDB的数据库。
Oracle 9i新建数据库的详细过程请参考Oracle的相关文档。
5.2 为数据库创建一个用户
在命令行下敲入oemapp console,或者直接在开始菜单里找到EnterPrise Manager Console打开Oracle管理控制中心,以SYSDBA的身份进入CoursesDB数据库后,在安全性->用户中新增一个用户,如chenxc,口令chenxc,并赋予dba的角色。这些设置在接下来的CourseseOnline系统具体开发中会用到。
Oracle 9i新建用户的具体操作请参考Oracle的相关文档。
5.3 建表及表的初始化
5.3.1 建表
建表的方法有N种:1)用sqlplus连上CoursesDB数据库后,用sql语句把数据字典中列出来的表结构敲进去;2)把数据字典中表结构写成sql脚本文件,然后用sqlplus连上CoursesDB,执行SQL>@@ c:\CoursesDB.sql;3)使用一种支持正向工程的数据库建模工具直接把ER图转换成数据库表结构,如ERWin;4)使用可视化工具建表,如PLSQL Developer。
其它的我知道的还有我还不知道的数据库的建表方法,在这里就不一一列举了,有兴趣的请自行研究,然后把经验告诉大家。
附录C提供了CoursesOnline建表及表的初始化的sql脚本。
5.3.2 表的初始化
从CoursesOnline用例图中可以看出,系统没有提供系统管理员、老师以及教室的管理的接口,系统管理员、老师和教室的信息在数据库建完后就应该初始化了。也就是说,这些信息是已经存在的,除非直接操作数据库,否则无法改变系统管理员、老师和教室的信息。
建完表后,我们为CoursesOnline系统初始化了一位系统管理员,名为sysadmin;三位老师,分别为任我行、东方不败和岳不群;除此之外还初始化了三间教室,分别为黑木崖教室1、黑木崖教室2和华山教室3。
附录C提供了CoursesOnline建表及表的初始化的sql脚本。
六、CoursesOnline的具体实现
6.1 在Jbuilder 9中新建一个工程
启动Jbuilder 9,File->New Project,项目名为CoursesOnline,选择路径后,点击finish
因为CoursesOnline项目使用Oracle 9i数据库,所以还需要把Oracle的驱动加载进来,Project -> Project Properties -> Path -> Required libraries,点击add,在Select One Or More Libraries窗口中选择OracleJDBCLib
OracleJDBCLib的配置请参考附录A.5.3 配置数据库驱动
6.2 创建Entity Bean(CMP)
6.2.1 新建一个EJB Module
File -> New -> Enterprise ->EJB Module
点击ok,并在接下来的窗口中为EJB Module命名为Courses,结果如下图
6.2.2 Import Schema From Database
在上图中的DataSources上点击右键,或者在Courses设计面板上点击右键,然后点击Import Schema From Database,在弹出的窗口中输入Driver,URL等参数,如下图所示
点击ok,这时数据库中的5个表在DataSources面板中显示出来,如下图所示
6.2.3 创建Entity Bean
在上图中DataSources面板中右键Actor,在弹出菜单中选择Create CMP 2.0 Entity Bean,结果如下图
左键点击Entity Bean Actor可以在弹出的窗口中编辑它的属性,在这里我们暂时使用默认值。接下来用同样的方法为其它四个表创建CMP 2.0 Entity Bean,结果如下图
从左边的Project下拉菜单中可以看到,每个Entity Bean都有三个(或四个)Java程序与之相对应,例如Actor.java、ActorBean.java和ActorHome.java。为了便于管理,以及避免和后面的程序胡搅蛮缠搞得眼睛高度紧张,我把Entity Bean对应的java程序都放在com.chenxc.coursesOnline.ejb20下。点击Entity Bean的名称,在Bean Properties窗口中点击Classes And Packages…按钮,然后为每个类指定路径。
6.2.4 阶段总结
这一节中创建了五个CMP 2.0 Entity Bean,是CMP而不是BMP,是2.0而不是1.x,是local接口而不是remote接口,而且还没有动手写过任何代码。本文并不打算深入探讨EJB的内部机制,而仅仅是举例子来说明使用EJB实现分层思想及分布式计算的方法。如果要研究EJB的细节,请参考附录B 参考资料列举的书籍或文章。
当前阶段的五个Entity Bean只能算是半成品,要完成业务逻辑还需要进一步加工,这些在接下来开发Session Bean的过程中会提到。欲知后事如何,请听下回分解。
6.3 创建Session Bean(Stateless)
6.3.1 SessionFacade - 连接Struts与Entity Bean的桥梁
新建一个名为SessionFacade的Session Bean。SessionFacade负责与Entity Bean沟通,并为Struts提供接口。
这个Session Bean扮演一个中间人的角色,就像是北京西客站的售票厅,旅客可以买T97的火车票去往广州,也可以买T31的火车票去往上海。在CoursesOnline系统中,Struts跟购买火车票的旅客一样,通过SessionFacade提供的接口完成系统登录、学生注册、学生选课等操作。
下面以Actor登录为例来说明SessionFacade是怎样扮演中间人的角色,如下图:
在SessionFacade中新增一个名为actorLogin的方法,返回值boolean,输入参数用户名,口令和用户类型(学生、老师或系统管理员),接口remote。
方法actorLogin提供给远程的Struts调用。
登录处理流程如下:
1、用户在页面上输入用户名、口令和用户类型并提交;
2、JavaBean存储用户在页面提交上来的用户名、口令和用户类型;
3、Struts读取JavaBean的用户名、口令和用户类型;
4、Struts通过远程接口调用SessionFacade的actorLogin(String username,String Password,int actortype)方法;
5、SessionFacade通过本地接口调用Entity Bean(Actor)的findByName(String username,int actortype)方法;
6、SessionFacade获取Entity Bean(Actor)的findByName(String username,int actortype)方法的返回值;
7、SessionFacade在actorLogin(String username,String Password,int actortype)方法中比较Entity Bean(Actor)的findByName(String username,int actortype)得到的password的值;
8、如果两个password的值相等,则actorLogin(String username,String Password,int actortype)返回true,否则返回false;
9、Struts根据SessionFacade的actorLogin(String username,String Password,int actortype)的返回值(true or false)判断登录是否成功,并导航到对应的页面。
6.3.2 登录流程的代码
/*--------------------------------------------------------------------------------------*/
//1、用户登录界面 - login.jsp
<!- 部分HTML代码此处省略 -- > <form name="teaForm" method="post" action="<%=contextPath%>/login.do"> <table border="0" cellspacing="0" cellpadding="0"> <tr> <td width="60"><font size="2">用户类型</font></td> <td> <input type="radio" value="0" name="actortype" checked><font size="2">系统管理员 </font> <input type="radio" value="1" name="actortype"><font size="2">学生 </font> <input type="radio" value="2" name="actortype"><font size="2">老师 </font> </td> </tr> <tr> <td width="60"><font size="2">用户名</font></td> <td><input name="username" value="" size="12"> *</td> </tr> <tr> <td width="60"><font size="2">口令</font></td> <td><input type="password" name="password" value="" size="8" maxlength="8"> *</td> </tr> </table> <br> <table border="0" cellspacing="0" cellpadding="0"> <tr> <td width="50"> </td> <td><input type="submit" value="登 录"></td> </tr> </table> </form> |
/*--------------------------------------------------------------------------------------*/
//2、Struts程序
//2.1 LoginForm.java(存储用户在页面上提交的用户名、口令和用户类型)
package com.chenxc.coursesonline.struts; /** * Title: LoginForm * Description: 存储登录信息 * Time: 2004-3-20 * Company: * Author: chenxc * version 1.0 */ import java.util.*; public class LoginForm extends org.apache.struts.action.ActionForm{ private String username; //用户名 private String password; //口令 private int actortype; //用户类型 //用户名 public void setUsername(String username) { this.username = username; } public String getUsername() { return username; } //口令 public void setPassword(String password) { this.password = password; } public String getPassword() { return password; } //用户类型 public void setActortype(int actortype) { this.actortype = actortype; } public int getActortype() { return actortype; } } |
//2.2 LoginAction.java(控制登录流程的走向)
package com.chenxc.coursesonline.struts; /** * Title: LoginAction * Description: 登录验证 * Time: 2004-3-20 * Company: * Author: chenxc * version 1.0 */ import java.io.*; import javax.servlet.*; import javax.servlet.http.*; import java.util.*; import org.apache.struts.action.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.ServletException; import com.chenxc.coursesonline.struts.*; public class LoginAction extends org.apache.struts.action.Action{ public ActionForward execute(ActionMapping mapping,ActionForm actionForm,HttpServletRequest request,HttpServletResponse response) throws Exception { ActionErrors errors = new ActionErrors(); HttpSession session = request.getSession(); ActionForward forward = null; LoginForm loginForm = (LoginForm)actionForm; FacadeBean facadeBean = new FacadeBean(); if(facadeBean.actorlogin(loginForm.getUsername(),loginForm.getPassword(),loginForm.getActortype())) { forward = mapping.findForward("success"); return forward; } return (new ActionForward(mapping.getInput())); } } |
//2.3 FacadeBean.java(实现登录验证)
package com.chenxc.coursesonline.struts; /** * Title: FacadeBean * Description: 负责与Session Bean沟通 * Time: 2004-3-20 * Company: * Author: chenxc * version 1.0 */ import com.chenxc.coursesonline.ejb20.*; import javax.naming.*; import java.util.Properties; import javax.rmi.PortableRemoteObject; import javax.ejb.CreateException; import java.rmi.RemoteException; public class FacadeBean extends Object { private static final String ERROR_NULL_REMOTE = "Remote interface reference is null. It must be created by calling one of the Home interface methods first."; private static final int MAX_OUTPUT_LINE_LENGTH = 100; private CoursesFacadeHome coursesFacadeHome = null; private CoursesFacade coursesFacade = null; //Construct the FacadeBean public FacadeBean() { initialize(); } public void initialize() { try { //get naming context Context context = getInitialContext(); //look up jndi name Object ref = context.lookup("CoursesFacade"); //look up jndi name and cast to Home interface coursesFacadeHome = (CoursesFacadeHome) PortableRemoteObject.narrow(ref,CoursesFacadeHome.class); } catch (Exception e) { e.printStackTrace(); } } private Context getInitialContext() throws Exception { String url = "t3://192.168.100.134:7001"; String user = null; String password = null; Properties properties = null; try { properties = new Properties(); properties.put(Context.INITIAL_CONTEXT_FACTORY, "weblogic.jndi.WLInitialContextFactory"); properties.put(Context.PROVIDER_URL, url); if (user != null) { properties.put(Context.SECURITY_PRINCIPAL, user); properties.put(Context.SECURITY_CREDENTIALS, password == null ? "" : password); } return new InitialContext(properties); } catch (Exception e) { throw e; } } //actorlogin public boolean actorlogin(String username, String password, int actortype) { try { coursesFacade = coursesFacadeHome.create(); if (coursesFacade.actorLogin(username, password, actortype)) { return true; } else { return false; } } catch (CreateException ex) { ex.printStackTrace(); } catch (RemoteException ex) { ex.printStackTrace(); } return false; } } |
/*--------------------------------------------------------------------------------------*/
//3、Session Bean程序
//3.1 SessionFacadeHome.java
package com.chenxc.coursesonline.ejb20; import javax.ejb.*; import java.util.*; import java.rmi.*; public interface SessionFacadeHome extends javax.ejb.EJBHome { public SessionFacade create() throws CreateException, RemoteException; } |
//3.2 SessionFacade.java
package com.chenxc.coursesonline.ejb20; import javax.ejb.*; import java.util.*; import java.rmi.*; public interface SessionFacade extends javax.ejb.EJBObject { public boolean actorLogin(String actor, String password, int actortype) throws RemoteException; } |
//3.2 SessionFacadeBean.java
package com.chenxc.coursesonline.ejb20; import javax.ejb.*; import javax.naming.*; import java.rmi.RemoteException; public class SessionFacadeBean implements SessionBean { SessionContext sessionContext; ActorHome actorHome; Actor actor; public void ejbCreate() throws CreateException { try { Context ctx = new InitialContext(); actorHome = (ActorHome)ctx.lookup("Actor"); } catch (Exception ex) { throw new EJBException(ex); } } public void ejbRemove() { /**@todo Complete this method*/ } public void ejbActivate() { /**@todo Complete this method*/ } public void ejbPassivate() { /**@todo Complete this method*/ } public void setSessionContext(SessionContext sessionContext) { this.sessionContext = sessionContext; } public boolean actorLogin(String username, String password, int actortype) { try { actor = actorHome.findByName(username,actortype); if(actor.getPassword().equals(password)) { System.out.println(username + " 登录成功!"); return true; } }catch(ObjectNotFoundException ex) { } catch(Exception ex) { ex.printStackTrace(); } return false; } } |
/*--------------------------------------------------------------------------------------*/
//4、为Entity Bean(Actor)新增一个findByName的Finder,如下图
Finder名findByName,返回值Actor,输入参数用户名,用户类型,接口local home,EJB-QL: SELECT OBJECT(a) FROM Actor AS a WHERE a.username = ?1 AND a.actortype = ?2
6.3.3 阶段总结
Client -> Struts -> Session Bean -> Entity Bean,实现一个登录流程用了十几个java程序,真是累人的说。一个JSP程序就可以实现的功能却要大阵战对待,杀鸡用牛刀乎?
答案是否定的,因为EJB存在的意义在于分布式计算和分层的思想。
6.4未完成的
写到这里,CoursesOnline系统只实现了系统登录验证的功能,主要的业务逻辑基本上没实现,但MVC框架,Facade Design Pattern都或多或少有所描述,继续完成CoursesOnline系统应该不会太困难。
另外由于使用CMP查询记录集时处理起来比较麻烦,所以在浏览课程或浏览学生等记录集查询时可以考虑用BMP来实现,或者干脆在Session Bean实现。
附录A 开发环境的配置
对于JBuilder、Oracle9i、WebLogic 7、WebLogic Domain的安装配置除参考相关的安装手册外,可参考本栏目发表的《Jbuilder9+Oracle9i+Weblogic8.1安装配置》
A. 配置Jbuilder 9
A.选择Tools->Configure Servers配置Server信息
Home Directory:选择Weblogic安装目录下的Server目录,如:C:\bea\weblogic700\server
Main Class,VM Parameters:系统会自动获得,不用修改
Working Directory:创建的新Domain目录 如:C:\bea\user_projects\mydomain
Class 中:由于没有Weblogic的SP包,删除weblogic_sp.jar包,仅保留weblogic.jar
JDK Install directory:选择安装Weblogic目录下的JDK目录 如:D:\bea\jdk131_03
BEA Home directory: 选择安装Weblogic目录 如:C:\bea User Name and Password:创建Domain时Administor的用户名称和密码。
Domain Name and Server Name:系统会自动获得
配置缺省工程的Server信息(Project->Default Projects Properties)
这样一来新建的工程默认情况下就使用这种Server配置
配置数据库驱动
点击Add按钮
在本机Oracle的目录下选择Oracle JDBC lib的路径,如D:\Oracle\Ora92\jdbc\lib\classes12.jar
配置数据库驱动后需重启JB才能使设置生效
附录B 参考资料
1)《Mastering EJB 2》
2)《Enterprise JavaBeans, 3rd Edition》
3)《EJB Design Pattern》
4)http://java.sun.com
5)http://dev2dev.bea.com.cn
附录C 建表及表初始化的sql脚本
C.1 建表
CREATE TABLE Appointment (ActorID SMALLINT NOT NULL,CoursesID SMALLINT NOT NULL); ALTER TABLE Appointment ADD ( PRIMARY KEY (ActorID, CoursesID) ) ; CREATE TABLE Courses ( CoursesID SMALLINT NOT NULL, CoursesName VARCHAR(30) NULL, StartDate DATE NULL, EndDate DATE NULL, RoomID SMALLINT NULL, ActorID SMALLINT NULL ); ALTER TABLE Courses ADD ( PRIMARY KEY (CoursesID) ) ; CREATE TABLE ActorInfo ( ActorID SMALLINT NOT NULL, ActorName VARCHAR(30) NULL, Phone VARCHAR(16) NULL, Email VARCHAR(50) NULL ); ALTER TABLE ActorInfo ADD ( PRIMARY KEY (ActorID) ) ; CREATE TABLE Actor ( ActorID SMALLINT NOT NULL, UserName VARCHAR(20) NULL, Password CHAR(8) NULL, ActorType SMALLINT NULL ); ALTER TABLE Actor ADD ( PRIMARY KEY (ActorID) ) ; CREATE TABLE Room (RoomID SMALLINT NOT NULL,RoomName VARCHAR(20) NULL); ALTER TABLE Room ADD ( PRIMARY KEY (RoomID) ) ; ALTER TABLE Appointment ADD ( FOREIGN KEY (CoursesID) REFERENCES Courses ) ; ALTER TABLE Appointment ADD ( FOREIGN KEY (ActorID) REFERENCES Actor ) ; ALTER TABLE Courses ADD ( FOREIGN KEY (ActorID) REFERENCES Actor ) ; ALTER TABLE Courses ADD ( FOREIGN KEY (RoomID) REFERENCES Room ) ; ALTER TABLE ActorInfo ADD ( FOREIGN KEY (ActorID) REFERENCES Actor ) ; |
C.2表的初始化
--1)初始化系统管理员
insert into actor values(1,'sysadmin','sysadmin',0); insert into actorinfo values(1,'sysadmin','66668888','chenxc@263.net'); |
--2)初始化老师
--增加任我行老师
insert into actor values(2,'rwx','rwx',1); insert into actorinfo values(2,'任我行','77778888','rwx@courses.com.cn'); |
--增加东方不败老师
insert into actor values(3,'dfbb','dfbb',1); insert into actorinfo values(3,'东方不败','88888888','dfbb@courses.com.cn'); |
--增加岳不群老师
insert into actor values(4,'ybq','ybq',1); insert into actorinfo values(4,'岳不群','99998888','ybq@courses.com.cn'); |
--3)初始化教室
insert into room values(1,'黑木崖教室1'); insert into room values(2,'黑木崖教室2'); insert into room values(3,'华山教室3'); |