在这里,将创建一个简化的用户管理模块,演示怎样利用SpringSide提供的数据持久层的功能,包括怎样通过Hibernate的Annotation来配置多对一映射和多对多映射。
大家都知道,现在最流行用户管理模型的是RBAC,也就是基于角色的访问控制模型,在这种模型中,可以划分多个层次,如用户-角色-资源、用户-角色-权限-资源、用户-角色-角色组-权限-资源、用户-角色-角色组-权限-操作-资源等等,因此,想要创建一个完善而复杂的用户管理模块,是相当具有难度的。在Web2.0时代,有一个很重要的开发思想,那就是先让程序跑起来,以后再逐步添加复杂的功能。因此,在这里只创建一个简化的用户管理模块。
所谓简化,它具有如下几个特点:
1.在认证方式中,选择基于用户名和密码的认证,用户需要提供用户名、密码和昵称,用户名和昵称都要求不能重复,用户名不能包含中文,且不能够被修改,昵称可以为中文,也可以被修改。密码使用MD5加密。
2.不包含用户的真实信息,如姓名、年龄、性别、职业、地址、邮编等等,因为如果包含这些字段,那么还需要包含更多的额外字段来让用户决定是否公开这些信息,因此,去掉这些东西,可以简化开发过程,让网站能够尽快的跑起来。
3.联系方式只需要用户提供它的电子邮箱和QQ号码。
4.如果用户密码丢失,可以通过密码提示问题找回,随机产生的新密码会发到用户的电子邮箱。
5.省略用户的个性化设置,如个性化签名、自定义头像等。
6.要能够记录用户的注册时间和最后登录时间。
7.要具有完善的积分和排名机制。
8.用户删除的时候不做物理删除,只标记为该用户不可用。
8.具有简化的角色和权限管理机制,这里的简化主要有以下几点:每个用户只能属于一个角色,即多对一关系,而不是传统的多对多关系;角色不需要分组;没有专门的资源抽象层;在角色表中只使用一个字段来表示该角色具有的权限,权限以数字表示,以逗号分开,如“1,2”,“1,3,15”等等。
9.用户可以创建群和加入群,为了简化,群的创始人即为管理员,并不可改变,用户加入群需要管理员批准,一个用户可以加如多个群,即多对多关系。
从上面的描述可以看出,一个简化的用户管理系统最少需要三个表,即users,roles和groups表,其中users和roles之间为多对一映射,users和groups之间为多对多映射,为了实现多对多映射,并且用户加入群的时候需要管理员批准,需要一个中间表users_groups。下面是在MySQL中创建数据表的语句。
创建用户表:
create
table
users(
id
int
not
null
auto_increment
primary
key
,
name
varchar
(
20
)
not
null
,
password
char
(
32
)
not
null
,
monicker
varchar
(
30
)
not
null
,
question
varchar
(
30
)
not
null
,
answer
varchar
(
30
)
not
null
,
email
varchar
(
40
)
not
null
,
qq
varchar
(
12
)
not
null
,
roleid
int
not
null
,
score
int
not
null
default
'
0
'
,
regtime
timestamp
not
null
default
CURRENT_TIMESTAMP
,
logintime
timestamp
not
null
default
'
2007-01-01 00:00:00
'
,
isdeleted
varchar
(
2
)
not
null
default
'
0
'
,
index
(username),
index
(monicker));
为了加快查找用户的速度,在用户名和昵称列上创建了索引。
创建角色表:
create
table
roles(
id
int
not
null
auto_increment
primary
key
,
name
varchar
(
20
)
not
null
,
privilegesFlag
varchar
(
255
),
index
(rolename)
);
创建群组表:
create
table
groups(
id
int
not
null
auto_increment
primary
key
,
name
varchar
(
40
)
not
null
,
creatorid
int
not
null
,
createtime
timestamp
not
null
default
CURRENT_TIMESTAMP
,
isdeleted
varchar
(
2
)
not
null
default
'
0
'
,
index
(groupname));
creatorid代表组的创始人,同时也是管理员,这里同样设置群组不做物理删除。
创建用户群组多对多映射辅助表:
create
table
users_groups(
id
int
not
null
auto_increment
primary
key
,
userid
int
not
null
,
groupid
int
not
null
,
jointime
timestamp
,
status
tinyint
,
index
(userid),
index
(groupid)
);
其中status列代表用户是否通过了管理员的批准,为了加快查找速度,在userid和groupid列上建立索引。
设计完数据库,就该设计领域对象了,领域对象的设计方法为先设计简单的POJO,然后再在POJO上添加Hibernate Annotation来配置映射关系。在进行Annotation配置的时候,可以从以下几个方面进行思考。
1、使用什么样的数据类型映射数据库中的列类型?
2、对象之间是一对一、一对多还是多对多关系?
3、关联的对象之间哪一个作为主控方?
4、对象之间的关联是单向的还是双向的?
首先来看看users和roles之间的关系,考虑到加载一个用户数据的时候,往往同时需要知道他属于哪个角色,而加载一个角色的时候,就没有必要知道它管理哪些用户了,因此,它们是简单的单向关系,是多对一映射。当出现多对一映射的时候,永远都应该选择多的这一方作为主控方,道理很简单,打个比方,让一个国家元首记住全国人民的名字基本是不可能的,而让全国人民记住国家元首的名字就很简单了。因此,这里User作为主控方,Role作为被控方。
再来看看数据类型的映射,对于简单的int、varchar这样的就不用多说了。而日期时间类型的映射是一个重点,可以看到,前面的数据库创建语句中,所有需要时间的地方都使用了timestamp列类型,使用timestamp列类型的唯一目的就是为了能够使用default CURRENT_TIMESTAMP语句,使用date和datetime类型就不行,在MySQL中,timestamp只能表示从'1970-01-01 00:00:00'到2037年的范围。
MySQL中的timestamp和java.sql.Timestamp表现不一致,在MySQL中,timestamp和datetime类型精度是一样的,都只能储存到整数秒,而timestamp比datetime能表示的时间范围要小得多,在Java中,java.util.Date和MySQL的timestamp的精度是一致的,只能储存到整数秒,而java.sql.Timestamp还保存毫微秒,因此建议使用java.util.Date来映射timestamp列,使用java.sql.Timestamp只是浪费。
MySQL和Java在时间上面还有一个冲突,那就是MySQL支持全零的时间,如'0000-00-00 00:00:00',而Java不支持,因此如果在定义users表的logintime列时使用logintime timestamp not null default '0000-00-00 00:00:00',那么在使用Hibernate来获取User对象的时候就会出错,所以在创建数据库的时候要选择一个合法的默认时间,如'2007-01-01 00:00:00'。
下面请看User.java的代码:
package
com.xkland.domain;
import
java.io.Serializable;
import
java.util.Date;
import
org.springside.core.dao.extend.Undeletable;
import
org.hibernate.annotations.Cache;
import
org.hibernate.annotations.CacheConcurrencyStrategy;
import
javax.persistence.
*
;
@Entity
@Table(name
=
"
users
"
)
@Undeletable(status
=
"
isDeleted
"
)
public
class
User
implements
Serializable
{
private
Integer id;
private
String name;
private
String password;
private
String monicker;
private
String question;
private
String answer;
private
String email;
private
String qq;
private
Role role;
private
Integer score;
private
Date regTime;
private
Date loginTime;
private
Byte isDeleted;
@Id
@GeneratedValue(strategy
=
GenerationType.AUTO)
public
Integer getId()
{
return
id;
}
public
void
setId(Integer id)
{
this
.id
=
id;
}
public
String getName()
{
return
name;
}
public
void
setName(String name)
{
this
.name
=
name;
}
public
String getPassword()
{
return
password;
}
public
void
setPassword(String password)
{
this
.password
=
password;
}
public
String getMonicker()
{
return
monicker;
}
public
void
setMonicker(String monicker)
{
this
.monicker
=
monicker;
}
public
String getQuestion()
{
return
question;
}
public
void
setQuestion(String question)
{
this
.question
=
question;
}
public
String getAnswer()
{
return
answer;
}
public
void
setAnswer(String answer)
{
this
.answer
=
answer;
}
public
String getEmail()
{
return
email;
}
public
void
setEmail(String email)
{
this
.email
=
email;
}
public
String getQq()
{
return
qq;
}
public
void
setQq(String qq)
{
this
.qq
=
qq;
}
@ManyToOne
@JoinColumn(name
=
"
roleid
"
)
public
Role getRole()
{
return
role;
}
public
void
setRole(Role role)
{
this
.role
=
role;
}
@Column(name
=
"
score
"
,insertable
=
false
)
public
Integer getScore()
{
return
score;
}
public
void
setScore(Integer score)
{
this
.score
=
score;
}
@Column(name
=
"
regtime
"
,insertable
=
false
)
@Temporal(TemporalType.TIMESTAMP)
public
Date getRegTime()
{
return
regTime;
}
public
void
setRegTime(Date regTime)
{
this
.regTime
=
regTime;
}
@Column(name
=
"
logintime
"
,insertable
=
false
)
@Temporal(TemporalType.TIMESTAMP)
public
Date getLoginTime()
{
return
loginTime;
}
public
void
setLoginTime(Date loginTime)
{
this
.loginTime
=
loginTime;
}
@Column(name
=
"
isdeleted
"
,insertable
=
false
)
public
Byte getIsDeleted()
{
return
isDeleted;
}
public
void
setIsDeleted(Byte isDeleted)
{
this
.isDeleted
=
isDeleted;
}
}
这里只对几个特殊的Annotation做一下注释:
1、因为创建数据表的时候使用的是users,而实体类为User,单复数不同引发名称不一致,因此需要@Table(name="users");
2、因为该表中的数据不做物理删除,所以加上@Undeletable(status="isDeleted"),结合SpringSide提供的HibernateEntityExtendDao类,可以在调用remove方法的时候将isdeleted列设置为"-1";
3、创建数据表的时候,所有的列名都是用的小写字母,因此有的列映射需要明确指定,如@Column(name = "logintime",insertable=false);
4、对于在创建数据表的时候定义了默认值的列,如regtime、regtime、logintime、isdeleted,在向数据库中添加数据的时候,可以不在insert语句中指定这些列,而让它们使用默认值,因此,需要告诉Hibernate在生成insert语句的时候不要包含这些列,可以使用insertable=false语句,如@Column(name = "regtime",insertable=false);
5、指定时间精度,使用@Temporal(TemporalType.TIMESTAMP);
6、指定users表通过roleid和roles表进行多对一映射,使用@ManyToOne和@JoinColumn(name="roleid")
Role.java则比较简单,如下:
package
com.xkland.domain;
import
java.io.Serializable;
import
javax.persistence.Entity;
import
javax.persistence.GeneratedValue;
import
javax.persistence.GenerationType;
import
javax.persistence.Id;
import
javax.persistence.Table;
@Entity
@Table(name
=
"
roles
"
)
public
class
Role
implements
Serializable
{
private
Integer id;
private
String name;
private
String privilegesFlag;
@Id
@GeneratedValue(strategy
=
GenerationType.AUTO)
public
Integer getId()
{
return
id;
}
public
void
setId(Integer id)
{
this
.id
=
id;
}
public
String getName()
{
return
name;
}
public
void
setName(String name)
{
this
.name
=
name;
}
public
String getPrivilegesFlag()
{
return
privilegesFlag;
}
public
void
setPrivilegesFlag(String privilegesFlag)
{
this
.privilegesFlag
=
privilegesFlag;
}
}
下一步再来看看users和groups之间的映射关系,不难想象,当载入一个用户的资料时,往往需要知道他加入了哪些群,而载入一个群的资料时,往往需要知道它有哪些用户,因此,他们之间是一个双向的关系,同时,载入一个群的资料时,还需要知道它的管理员是谁,因此又同时存在一个单向的多对一关系。在多对多关系中,设定User为主控方,所以需要在User.java中添加如下代码?
private
List
<
Group
>
groups;
@ManyToMany(targetEntity
=
User.
class
,
cascade
=
{CascadeType.PERSIST, CascadeType.MERGE}
)
@JoinTable(name
=
"
users_groups
"
,
joinColumns
=
{@JoinColumn(name
=
"
userid
"
)}
,
inverseJoinColumns
=
{@JoinColumn(name
=
"
groupid
"
)}
)
public
List
<
Group
>
getGroups()
{
return
groups;
}
public
void
setGroups(List
<
Group
>
groups)
{
this
.groups
=
groups;
}
而整个Group.java的代码如下:
package
com.xkland.domain;
import
java.io.Serializable;
import
java.util.Date;
import
java.util.List;
import
org.springside.core.dao.extend.Undeletable;
import
org.hibernate.annotations.Cache;
import
org.hibernate.annotations.CacheConcurrencyStrategy;
import
javax.persistence.
*
;
@Entity
@Table(name
=
"
groups
"
)
@Undeletable(status
=
"
isDeleted
"
)
public
class
Group
implements
Serializable
{
private
Integer id;
private
String name;
private
User creator;
private
Date createTime;
private
String isDeleted;
private
List
<
User
>
users;
@Id
@GeneratedValue(strategy
=
GenerationType.AUTO)
public
Integer getId()
{
return
id;
}
public
void
setId(Integer id)
{
this
.id
=
id;
}
public
String getName()
{
return
name;
}
public
void
setName(String name)
{
this
.name
=
name;
}
@ManyToOne(cascade
=
{CascadeType.PERSIST, CascadeType.MERGE}
)
@JoinColumn(name
=
"
creatorid
"
)
public
User getCreator()
{
return
creator;
}
public
void
setCreator(User creator)
{
this
.creator
=
creator;
}
@Column(name
=
"
createtime
"
,insertable
=
false
)
@Temporal(TemporalType.TIMESTAMP)
public
Date getCreateTime()
{
return
createTime;
}
public
void
setCreateTime(Date createTime)
{
this
.createTime
=
createTime;
}
@Column(name
=
"
isdeleted
"
,insertable
=
false
)
public
String getIsDeleted()
{
return
isDeleted;
}
public
void
setIsDeleted(String isDeleted)
{
this
.isDeleted
=
isDeleted;
}
@ManyToMany(cascade
=
{CascadeType.PERSIST, CascadeType.MERGE}
,
mappedBy
=
"
groups
"
,
targetEntity
=
User.
class
)
public
List
<
User
>
getUsers()
{
return
users;
}
public
void
setUsers(List
<
User
>
users)
{
this
.users
=
users;
}
}
好了,该开始测试了,看看经过前面设计和配置的代码能否正常工作。首先,先创建三个Manager,这三个Manager都继承自org.springside.core.dao.extend.HibernateEntityExtendDao,至于HibernateEntityExtendDao的功能,请参考SpringSide的文档。代码如下:
UserManager.java:
package
com.xkland.manager;
import
org.springside.core.dao.extend.HibernateEntityExtendDao;
import
com.xkland.domain.User;
public
class
UserManager
extends
HibernateEntityExtendDao
<
User
>
{
}
RoleManager.java:
package
com.xkland.manager;
import
org.springside.core.dao.extend.HibernateEntityExtendDao;
import
com.xkland.domain.Role;
public
class
RoleManager
extends
HibernateEntityExtendDao
<
Role
>
{
}
GroupManager.java:
package
com.xkland.manager;
import
org.springside.core.dao.extend.HibernateEntityExtendDao;
import
com.xkland.domain.Group;
public
class
GroupManager
extends
HibernateEntityExtendDao
<
Group
>
{
}
下一步,将User.class、Role.class、Group.class等领域对象添加到src\main\resources\config\hibernate.cfg.xml中,如下:
<!
DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"
>
<
hibernate-configuration
>
<
session-factory
>
<
mapping
class
="com.xkland.domain.Role"
/>
<
mapping
class
="com.xkland.domain.User"
/>
<
mapping
class
="com.xkland.domain.Group"
/>
</
session-factory
>
</
hibernate-configuration
>
再下一步,将上面的三个Manager类交给Spring管起来,配置src\main\resources\spring\serviceContext.xml,如下:
<?
xml version="1.0" encoding="UTF-8"
?>
<!
DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "http://www.springframework.org/dtd/spring-beans-2.0.dtd"
>
<
beans
default-lazy-init
="true"
default-autowire
="byName"
>
<
bean
id
="roleManager"
class
="com.xkland.manager.RoleManager"
/>
<
bean
id
="userManager"
class
="com.xkland.manager.UserManager"
/>
<
bean
id
="groupManager"
class
="com.xkland.manager.GroupManager"
/>
</
beans
>
最后一步,编写一个Action类,用Spring将上面的三个Manager注入到Action中,测试能否顺利的操作数据库。Action类的代码如下:
package
com.xkland.action;
import
org.apache.struts.action.Action;
import
org.apache.struts.action.ActionForward;
import
org.apache.struts.action.ActionForm;
import
org.apache.struts.action.ActionMapping;
import
javax.servlet.http.HttpServletRequest;
import
javax.servlet.http.HttpServletResponse;
import
com.xkland.manager.
*
;
import
com.xkland.domain.
*
;
public
class
WelcomeAction
extends
Action
{
private
RoleManager roleManager;
private
UserManager userManager;
private
GroupManager groupManager;
//
以下代码的作用是注入三个Manager
public
void
setUserManager(UserManager userManager)
{
this
.userManager
=
userManager;
}
public
void
setRoleManager(RoleManager roleManager)
{
this
.roleManager
=
roleManager;
}
public
void
setGroupManager(GroupManager groupManager)
{
this
.groupManager
=
groupManager;
}
public
ActionForward execute(
ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response
)
{
//
以下代码测试能否添加role
Role role
=
new
Role();
role.setName(
"
第一个角色
"
);
role.setPrivilegesFlag(
"
1,2,3,4,
"
);
roleManager.save(role);
//
以下代码测试能否添加user
User user
=
new
User();
user.setAnswer(
"
aa
"
);
user.setEmail(
"
aa
"
);
user.setQq(
"
aa
"
);
user.setName(
"
abcdefg
"
);
user.setPassword(
"
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
"
);
user.setQuestion(
"
aa
"
);
user.setMonicker(
"
abcdefg
"
);
user.setRole(roleManager.get(
1
));
userManager.save(user);
//
以下代码测试能否添加group
Group group
=
new
Group();
group.setName(
"
第一个用户组
"
);
group.setCreator(user);
groupManager.save(group);
//
以下代码测试将user和group建立关联
user
=
userManager.get(
1
);
group
=
groupManager.get(
1
);
user.getGroups().add(group);
group.getUsers().add(user);
userManager.save(user);
groupManager.save(group);
//
重定向到
return
new
ActionForward(
"
/welcome.jsp
"
);
}
}
怎样配置Action这里就不用多嘴了,请参考SpringSide的文档。这里还要说一句,一定要记得修改src\main\resources\spring\applicationContext.xml中的事务配置中的package,否则运行会出错,配置文件片断如下:
<!--
以AspectJ方式 定义 AOP
-->
<
aop:config
proxy-target-class
="true"
>
<!--
注意,请把第2个*号换为项目package
-->
<
aop:advisor
pointcut
="execution(* *..manager.*Manager.*(..))"
advice-ref
="txAdvice"
/>
<
aop:advisor
pointcut
="execution(* org.springside.core.dao.*Dao.*(..))"
advice-ref
="txAdvice"
/>
</
aop:config
>
<!--
基本事务定义,使用transactionManager作事务管理,默认get*方法的事务为readonly,其余方法按默认设置.
默认的设置请参考Spring文档事务一章.
-->
<
tx:advice
id
="txAdvice"
>
<
tx:attributes
>
<
tx:method
name
="get*"
read-only
="true"
/>
<
tx:method
name
="find*"
read-only
="true"
/>
<
tx:method
name
="*"
/>
</
tx:attributes
>
</
tx:advice
>
如果有兴趣,还可以把hibernate.show_sql设置为true,以便观察Hibernate生成的SQL语句,如下图: