用JSP/Servlet开发简单的用户注册系统
作者:板桥banq
上页
2.2.5 数据库设计
由于本项目需求比较简单,整个项目的基本对象是“用户”,围绕“用户”对象实现有关信息的查询和修改,因此,无需进行专门的基本业务对象设计过程。但是对于中大型项目,基本业务对象设计过程是系统设计的最先步骤。能否总结或设计出一个系统的最基本业务对象,是决定一个项目是否能开发成功的关键因素。完成了系统的基本业务对象设计,才可以进入数据对象的设计。
本项目围绕“用户”对象建立下列3个数据模型:用户基本资料、密码表以及用户查询密码表。
Profile是用户基本详细资料表,记载新用户注册后的个人资料,Profile表SQL语句如下(程序2-1):
程序2-1
CREATE TABLE profile (
userid varchar(20) NOT NULL, #用户id
username varchar(50) NOT NULL, #用户姓名
email varchar(50), #E-mail地址
性别
occupation tinyint(4), #职业
location varchar(200) #住址
city varchar(20), #城市
country tinyint(4), #国家
zipcode varchar(50), #邮政编码
homephone varchar(50), #家庭电话或联系电话
cardnumber varchar(20), #身份证号码
birthday date DEFAULT '0000-00-00', #出生日期
regip varchar(20), #注册时的IP,公安局追查用
regdate datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, #注册日期
PRIMARY KEY (userid),
UNIQUE email (email),
UNIQUE userid (userid)
);
Password是记载用户登录的信息,包括用户名和密码。因为用户和密码验证是整个系统的核心部分,而且会被频繁读取,可以设计成独立的数据表以提高性能。Password表的SQL语句如下:
CREATE TABLE password (
userid varchar(20) DEFAULT '' NOT NULL, #用户id
password varchar(16) DEFAULT '' NOT NULL, #密码
PRIMARY KEY (userid),
KEY password (password)
);
passwordassit是用户查询密码表,保存着用户获取密码时的问题提问和答案,其SQL语句如下:
CREATE TABLE passwordassit (
userid varchar(20) DEFAULT '' NOT NULL, #用户id
passwdtype tinyint(4), #密码提示问题
passwdanswer varchar(100), #密码回答
PRIMARY KEY (userid),
KEY passwdtype (passwdtype)
);
在本项目中将采用MySQL作为数据库系统,MySQL是目前应用广泛的快速数据系统。特别是MySQL 4.0的推出,将MySQL推向了企业数据库市场,而且发展迅速。MySQL的优点是快速,而且是开放源代码的,因此商业用户使用时,可以免费安装。
2.3 类的详细设计和实现
本项目中,JavaBeans的主要功能比较简单,就是将用户在JSP中输入的数据转存成数据库的数据表。因此,数据表的插入、修改和删除功能变成了JavaBeans的主要功能。
2.3.1 Facade模式
GOF的《设计模式》一书中关于外观Facade模式定义如下:为子系统中的一组接口提供一个一致的界面。Facade模式提供了一个更高的接口,使得子系统更加容易使用。
细分封装是面向对象编程中常用的设计方法,将一个系统划分为若干子系统有利于降低系统的复杂性,一直划分到子系统之间耦合度最小。所谓耦合度就是通信和相互依赖的关系,这类似一个建筑物的设计建造,建筑物有一个基本架构,而架构是由一些模块组成的,模块又是由一块块砖石组成的,而砖石是由一粒粒沙组成的。划分到如此细的部件,这样就可以重复利用这些部件来搭建任何建筑物了。
为了达到有效的划分系统,引入Facade模式。它其实类似一种管理Manager类,由Facade类管理负责一部分小类的运算和相互调用,外界不直接调用子系统中的子类,而是通过Facade类实现。
图2-3表示使用Facade模式后,客户端对一组对象的访问变得有规则,都通过Facade类来实行,简化了整个结构。
Facade模式是一个比较容易理解,而且会经常使用的模式。例如数据库操作,一般典型的数据库JDBC操作代码如下:

图2-3 使用Facade模式前后
public class DBOperator {
public void getResult(){
Connection conn = null;
PreparedStatement prep = null;
ResultSet rset = null;
try {
Class.forName( "<driver>" ).newInstance(); //获得数据库驱动实例
conn = DriverManager.getConnection( "<database>" ); //获得数据库连接
//实现数据库查询
String sql = "SELECT * FROM <table> WHERE <column name> = ?";
prep = conn.prepareStatement( sql );
prep.setString( 1, "<column value>" );
rset = prep.executeQuery();
if( rset.next() ) {//查询后打印出来数据表某列的值
System.out.println( rset.getString( "<column name" ) );
}
} catch( SException e ) {
e.printStackTrace();
} finally {
rset.close();
prep.close();
conn.close();
}
}
}
获得数据库驱动实例和获得数据库连接等,这些都是和JDBC相关支持类实现的调用,这些JDBC相关的支持类可以成为一个独立的子系统。如果按照上面的使用方法,应用系统的JavaBeans直接操作JDBC,将来对JDBC一些操作实现扩展修改就比较难。例如,上面代码中使用的是直接获得数据库连接,如果从连接池中要获得连接,那么就需要逐个修改Connection的获得方法;或者数据库更换了,需要更改<driver>的名称等。
为了尽量减少在扩展时修改已经成熟的程序类,降低因为修改带来的风险,需要使用Facade模式将JDBC的操作统一起来。应用系统的JavaBeans通过总的Facade管理类来实现JDBC的操作。
将上例中的数据库访问打包进入一个Facade类MySQL,那么代码就变为:
public class DBOperator {
public void getResult(){
String sql = "SELECT * FROM <table> WHERE <column name> = ?";
try {
Mysql msql=new Mysql(sql);
msql.setString( 1, "<column value>" );
rset = msql.executeQuery();
if( rset.next() ) {
System.out.println( rset.getString( "<column name" ) );
}
} catch( SException e ) {
e.printStackTrace();
} finally {
mysql.close();
}
}
}
这样MySQL就成为一个数据库通用操作类,不只是JDBC。邮件发送也可以采取Facade模式,通过专门的邮件发送总类,实现邮件AP的统一操作。
下面讨论MySQL是如何实现的。
2.3.2 JDBC通用操作类
关于数据库的主要操作有:获取数据库连接;数据库查询、插入、修改、删除;断开数据库连接。而数据库连接和断开数据库连接对于不同的数据表应该说都是统一的,因此,数据库的JDBC操作可以做成一个通用类,这样就能达到重用目的。代码如下:
public class Mysql {
private Connection conn = null;
private Statement stmt = null;
private PreparedStatement prepstmt = null;
/**
* 以创建Statement 初始化Mysql
*/
public Mysql() {
try {
Class.forName(Constants.dbdriver).newInstance();;
conn = DriverManager.getConnection(Constants.dburl);
stmt = conn.createStatement();
} catch (Exception e) {
System.err.println("Mysql init error: " + e);
}
}
/**
* 以创建PreparedStatement 初始化Mysql
*/
public Mysql(String sql) {
try {
Class.forName(Constants.dbdriver).newInstance();;
conn = DriverManager.getConnection(Constants.dburl);
prepareStatement(sql);
} catch (Exception e) {
System.err.println("Mysql init error: " + e);
}
}
public Connection getConnection() {
return conn;
}
public void prepareStatement(String sql) throws SQLException {
prepstmt = conn.prepareStatement(sql);
}
public void setString(int index, String value) throws SQLException {
prepstmt.setString(index, value);
}
public void setInt(int index, int value) throws SQLException {
prepstmt.setInt(index, value);
}
public void setBoolean(int index, boolean value) throws SQLException {
prepstmt.setBoolean(index, value);
}
public void setDate(int index, Date value) throws SQLException {
prepstmt.setDate(index, value);
}
public void setLong(int index, long value) throws SQLException {
prepstmt.setLong(index, value);
}
public void setFloat(int index, float value) throws SQLException {
prepstmt.setFloat(index, value);
}
public void setBinaryStream(int index, InputStream in, int length) throws
SQLException {
prepstmt.setBinaryStream(index, in, length);
}
public void clearParameters() throws SQLException {
prepstmt.clearParameters();
}
public PreparedStatement getPreparedStatement() { return prepstmt; }
public Statement getStatement() { return stmt; }
// 执行Statement查询语句
public ResultSet executeQuery(String sql) throws SQLException {
if (stmt != null) {
return stmt.executeQuery(sql);
} else
return null;
}
// 执行PreparedStatement查询语句
public ResultSet executeQuery() throws SQLException {
if (prepstmt != null) {
return prepstmt.executeQuery();
} else
return null;
}
// 执行Statement更改语句
public void executeUpdate(String sql) throws SQLException {
if (stmt != null) stmt.executeUpdate(sql);
}
// 执行PreparedStatement更改语句
public void executeUpdate() throws SQLException {
if (prepstmt != null) prepstmt.executeUpdate();
}
// 关闭连接
public void close() {
try {
if (stmt != null) {
stmt.close();
stmt = null;
}
if (prepstmt != null) {
prepstmt.close();
prepstmt = null;
}
conn.close();
conn = null;
} catch (Exception e) {
System.err.println("Mysql close error: " + e);
}
}
}
从Mysql.java可以看到,在Mysql构造方法中,将JDBC的Connection和PreparedStatement初始化,具体Constants.java的值如下:
//数据库MySQL的JDBC驱动程序名称
public static final String dbdriver = "com.mysql.jdbc.Driver";
//数据库连接URL 连接到名为user的数据库,用户banq,密码是1234
public static String dburl =
"jdbc:mysql://localhost/user?user=banq&password=1234";
这里数据库连接URL是根据MySQL的数据库设置的,MySQL在安装后,需要设置允许访问的用户名和密码,只要在MySQL控制台输入:
GRANT ALL PRIVILEGES ON *.* TO banq@localhost IDENTIFIED BY '1234' WITH GRANT OPTION;
就可以授权用户banq以密码1234访问MySQL,这部分属于MySQL设置部分。
Mysql.java中使用PreparedStatement访问数据库,这种访问方式比Statement访问速度更快,相关技术细节可参考有关介绍,因此,本项目中均采取这种方式访问数据库。
使用Mysql很简单,查询功能代码如下:
private boolean testQuery(String userId){
boolean check = false;
String sql = "SELECT userid,password FROM password WHERE userid=?";
Mysql mysql = new Mysql(sql);
try {
//将参数输入,1的意思表示对应sql语句中第一个"?"
//上面sql变量中第一个"?"是userid
mysql.setString(1, userId);
ResultSet rs = mysql.executeQuery();
if (rs.next()) check = true;
} catch (Exception e) {
System.err.println(e);
}
finally {
mysql.close();
mysql = null;
}
}
PreparedStatement是先将SQL语句("SELECT userid,password FROM password WHERE userid=?")送到数据库服务器端,还有一个userid的输入参数等待输入,通过setString(1, userId),就将输入参数userId送达数据库端服务器。因为这次送达的只是几个参数,而不是整条SQL语句,因此速度要快。
最后,一定要执行mysql.close()将有关数据库连接关闭,否则会耗费内存,导致系统死机。
将JDBC操作封装在一个通用类中,实际上是在应用程序和JDBC之间增加了一个隔离层,实现了双方的解耦。这样最大的好处是,可以根据不同的数据库实现数据库优化操作。例如MySQL支持setFetchSize和setMaxRows,但是Postgres只支持setMaxRows操作。为了实现在MySQL上能使用这两种方法,而一旦该应用系统移植到Postgres上,这两种方法能自动失效,可以在这个JDBC通用类中加入几个标志变量:
//是否支持大文本字段stream化
protected static boolean streamLargeText;
//是否支持Statement.setMaxRows() method
protected static boolean supportsMaxRows;
//是否支持Statement.setFetchSize() method
protected static boolean supportsFetchSize;
创建一个方法,用来自动判断数据库类型,如下:
private static void setMetaData(Connection con) throws SQLException {
DatabaseMetaData metaData = con.getMetaData(); //获得连接的metaData
supportsTransactions = metaData.supportsTransactions();
streamLargeText = false;
supportsMaxRows = true;
supportsFetchSize = true;
// 得到数据库名称
String dbName = metaData.getDatabaseProductName().toLowerCase();
String driverName = metaData.getDriverName().toLowerCase();
// Oracle properties.
if (dbName.indexOf("oracle") != -1) {
databaseType = DatabaseType.ORACLE;
streamLargeText = true;
// The i-net AUGURO JDBC driver
if (driverName.indexOf("auguro") != -1) {
streamLargeText = false;
supportsFetchSize = true;
supportsMaxRows = false;
}
}// Postgres properties
else if (dbName.indexOf("postgres") != -1) {
supportsFetchSize = false;
} // Interbase properties
else if (dbName.indexOf("interbase") != -1) {
supportsFetchSize = false;
supportsMaxRows = false;
} // SQLServer, JDBC driver i-net UNA properties
else if (dbName.indexOf("sql server") != -1 &&
driverName.indexOf("una") != -1) {
supportsFetchSize = true;
supportsMaxRows = false;
} // MySQL properties
else if (dbName.indexOf("mysql") != -1) {
databaseType = DatabaseType.MYSQL;
}
}
这样,实现大批量数据查询,只要根据supportsMaxRows等标志判断一下是否可用setMaxRows等操作。
2.3.3 E-mail发送通用类
按照JDBC操作通用类设计思路,也可以将E-mail发送封装设计成一个类,专门负责连接邮件发送服务器、用户名和密码验证以及邮件的发送。
/**
* 首先安装Java mail的mail.jar 和activation,jar
* 这和servlet.jar一样,JDK种没有,需要到java.sun.com下载
* 本类使用需要指定smtp服务器
*/
public class SendMail {
private String msgText = null; //邮件内容
private String subject = null; //邮件标题
private String smtpHost = null; //发送邮件的SMTP服务器
private String from = null; //邮件发送者
private String to = null; //邮件接受者
public void setMsgText(String msgText) { this.msgText = msgText; }
public void setSubject(String subject) { this.subject = subject; }
public void setSmtpHost(String smtpHost) { this.smtpHost = smtpHost; }
public void setFrom(String from) { this.from = from; }
public void setTo(String to) { this.to = to; }
public SendMail() {}
/**
* E-mail发送方法
* 相关语句可以参见 Java mail的API和使用手册
*/
public void sendnow() throws Exception {
try {
Properties props = new Properties();
props.setProperty("mail.smtp.host", smtpHost); //设置SMTP 服务器
Session session = Session.getDefaultInstance(props, null); //创建Session
MimeMessage message = new MimeMessage(session); //创建Message
message.setFrom(new InternetAddress(from)); //设置Message来源
InternetAddress[] address = { new InternetAddress(to)};
message.setRecipients(Message.RecipientType.TO, address); //设置Message目的
message.setSubject(subject); //设定主题
message.setSentDate(new Date()); //设定发送时间
message.setText(msgText); //设定Message内容
Transport.send(message); //发送邮件
}
catch (Exception ex) {
throw new Exception("SendMail.sendnow" + ex.getMessage());
}
}
}
只要设置好邮件主题、邮件内容和发往目的邮件地址,就可以直接发送E-mail,不过这需要指定一个SMTP服务器。本类就是将邮件连接到SMTP服务器,然后通过SMTP服务器发送出去。实际上类似Foxmail、OutLook这样的邮件程序。
该E-mail发送是同步即时功能。即只有等该功能完成后,其后续功能才会执行。在实际运行中,连接远程邮件服务器、发送邮件都是有可能失败的。因此不能让邮件发送功能影响系统主要功能的实现。
解决办法有两种:第一种是将本功能以后台线程方式运行;第二种是使用JMS等消息处理框架来发送。这种方式将在后面章节中讨论。
2.3.4 用户资料管理
各个数据表的具体数据库操作内容还是不一样,因此,需要建立专门的数据模型类。这也是对象化编程的一个基本思路,将每个数据表的操作封装,以便将和数据表相关的变动降低到最低程度。
另外一方面,这些数据模型类实际是数据库表在内存中映射,实际是一种缓冲,形成了一个数据层。
在本系统中,首先需要实现用户个人资料的管理,包括新增的用户资料、修改和查询用户个人资料。建立个人资料数据模型类Profile.java,它将实现对数据表profile新增、修改以及查询等操作。相关代码如下:
public class Profile {
//用户登陆Id
private String userid = "";
public void setUserid(String userid) { this.userid = userid; }
public String getUserid() { return userid; }
//用户密码
private String password = null;
public void setPassword(String password) { this.password = password; }
public String getPassword() { return password; }
//用户姓名
private String username = "";
public void setUsername(String username) { this.username = username; }
public String getUsername() { return username; }
//用户E-mail
private String email = "";
public void setEmail(String email) { this.email = email; }
public String getEmail() { return email; }
//更多的相关字段setXXX和getXXXXX
…
//* 新增新的个人资料
public int insert() throws Exception {
if (!isValid())
return Constants.FORM_ERROR;
if (EmailExist()) //检查E-mail的惟一性
return Constants.FORM_ERROR;
String birthday = new String(year + "-" + month + "-" + day);
String insertsql ="insert into profile values (?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
String passwordsql ="insert into password (userid,password) values (?,PASSWORD(?))";
String passwordassitsql ="insert into passwordassit (userid,oldpassword,passwdtype,passwdanswer)
values (?,?,?,?)";
Mysql mysql = new Mysql(insertsql);
try {
mysql.setString(1, this.userid);
mysql.setString(2, username);
mysql.setString(3, this.email);
… //其他字段赋值
java.text.SimpleDateFormat formatter = new java.text.SimpleDateFormat(
Constants.entimeformat);
String regdate = formatter.format(new java.util.Date());
mysql.setString(14, regdate);
mysql.executeUpdate(); //执行profile表的插入
mysql.prepareStatement(passwordsql);
mysql.setString(1, userid);
mysql.setString(2, password);
mysql.executeUpdate(); //执行password表的插入
mysql.prepareStatement(passwordassitsql);
mysql.setString(1, userid);
mysql.setString(2, password);
mysql.setInt(3, this.passwdtype);
mysql.setString(4, this.passwdanswer);
mysql.executeUpdate(); //执行passwordassit表的插入
} catch (Exception ex) {
throw new Exception("Profile.insert()" + ex.getMessage());
} finally {
mysql.close();
}
return Constants.OK;
}
// 修改个人资料
public int update() throws Exception {
if (!isValid())
return Constants.FORM_ERROR;
String birthday = new String(year + "-" + month + "-" + day);
char current = ' ';
String updatesql = "update profile set " +
"username=?,email=?,gender=?,occupation=?,location=?,city=?,country=?," +
"zipcode=?,homephone=?,cardnumber=?,birthday=? where userid=?";
Mysql mysql = new Mysql(updatesql);
try {
mysql.setString(1, username);
mysql.setString(2, this.email);
… //其他字段赋值
mysql.setString(12, this.userid);
mysql.executeUpdate(); //修改profile表
} catch (Exception ex) {
throw new Exception("Profile.update()" + ex.getMessage());
} finally {
mysql.close();
}
return Constants.OK;
}
// 查询个人资料
public boolean select() throws Exception {
String selectsql = "select *, year(birthday) as year,month(birthday) as
month,dayofmonth(birthday) as day from profile where userid = ?";
Mysql mysql = new Mysql(selectsql);
try {
mysql.setString(1, userid);
ResultSet rs = mysql.executeQuery();
//查询profile表,如果查询到,将数据表的值保存到本JavaBean的变量中
//不提倡直接对ResultSet操作
boolean next = rs.next();
if (next) {
username = rs.getString("username");
email = rs.getString("email");
… //获得其他字段
}
rs.close();
return next;
} catch (Exception ex) {
throw new Exception("Profile.update()" + ex.getMessage());
} finally {
mysql.close();
}
}
// 检查E-mail是否存在
public boolean EmailExist() throws Exception {
boolean exist = false;
String sql = "select * from profile where email = ?";
Mysql mysql = new Mysql(sql);
try {
mysql.setString(1, email);
ResultSet rs = mysql.executeQuery();
if (rs.next())
exist = true;
rs.close();
} catch (Exception ex) {
throw new Exception("Profile.update()" + ex.getMessage());
} finally {
mysql.close();
}
return exist;
}
}
在Profile.java中,既保存了有关profile表的变量和状态,又实现了一些行为。这样封装起来可以实现很好的低耦合性,修改、扩展起来非常方便,不会牵一动百。比如需要改变Profile的数据库字段,所做的修改只是Profile.java的变动,其他部分基本不需改动。
当然,这样做还不能和显示界面消除彻底的解耦。因为在这里隐含着将Profile表和Profile对象以及界面的Profile表单合二为一了,其实在复杂系统中,这是3个层次不同的概念,也必须分离开。EJB实体Bean实际是这个思路的继续深化,这也会在以后的章节中分析。
下页
first