让我们首先来看一下什么是CRUD,CRUD即Create-Read-Update-Delete的英文缩写,不过笔者还要在这里加上个L,即List,合起来即CRUDL。CRUDL这种场景或者说模式在绝大部分的应用当中都很常见,那么,每写一个应用如果都要重复一遍这样的劳动显然显得十分的繁琐,那有没有一种优雅、干净的办法实现简单的CRUDL呢?笔者在此就向大家介绍一种只需要极少量的代码和简单的配置就能实现完整的CRUDL功能。
如果说要使用一种通用的,可复用的方法来实现基本的CRUDL功能的话,那我们就需要分析和观察大部分的应用是如何完成CRUDL的功能,并在此基础上总结出一般性的规律,然后根据这个规律实现一个通用的解决方法。
我们以在博客上发帖为例,当用户点击发帖(add)按钮时,当前页面就跳转到编辑页面<edit>,当用户编辑完成点击发布(save)后,用户编辑的内容会被保存,同时页面跳转到新发布的文章<view>。当用户在浏览博客文章的列表时<browse>,点击编辑(edit)按钮时,当前页面就跳转到编辑页面<edit>,当用户编辑完成点击发布(save)后,用户编辑的内容会被保存,同时页面跳转到用户所有已发布博客文章的列表<browse>。在浏览的时候博客文章或列表<view><browse>的时候,点击删除(remove)后,文章即被删除,同时页面跳转到博客文章的列表页面<browse>。
读者朋友肯定已经注意到了,在上面这段描述中,对于一些关键词,笔者一共使用了两种不同的格式。对了,()表示一种行为,<>表示一个页面。一个行为正好可以映射到Struts2中的一个aciton,而一个页面正好可以映射到Struts2中的一个result。不过这里要注意的是,<view><browse>的页面与<edit>页面有所不同,<view>自身还包括了一个(show)action,即页面本身include一个show aciton和这个aciton所生成的页面;<browse>自身还包括了一个(list)aciton,即页面本身include一个list aciton和这个aciton所生成的页面。
也许上面的解释不一定清楚,不过没关系,我们再来看一下下面这个表格。
Action方法 | Action方法描述 | 返回页面 | 返回页面描述 |
add | 跳转到编辑页面 | edit | 编辑页面 |
edit | 先SELECT出该博客文章,再跳转到编辑页面 | edit | 编辑页面 |
remove | commit DELETE | browse | 包含list action的页面 |
save | commit INSERT或者UPDATE | view | 包含show action的页面 |
list | 根据一个reference id(通常为foreign key,这里是loginId)SELECT 出一个列表 | list | 该reference id(在这里是loginId)下的文章列表 |
show | 根据primary key SELECT出该博客文章 | show | 该博客文章的内容 |
在这里我把browse和list分开,把view和show分开主要是考虑到了页面组件的可重用性,比如菜单栏、导航栏等组件几乎贯穿所有页面,可以放在browse或view页面中,但不应该由remove或者show action方法渲染出。
另外有些aciotn方法名和页面名相重复,但代表着完全不同意思,读者朋友不要搞混了,(以后会考虑重构)。
就象大家在本系列的上几篇中看到的那样,拦截器始终贯穿着Struts2,这次也一样,在介绍如何用一个baseAction实现所有的CRUDL功能之前,我们先来看几个Struts2内置的拦截器。首先是params,这个拦截器相比大家都很熟悉,只要你用Struts2就几乎没有不用params的,它的职责就是把request中的参数注入到action中,另一个我们要引入的拦截器是prepare,prepare拦截器在aciton执行前给用户个机会用来做一些准备工作,比如初始化等。另一个要出场的是modelDriven拦截器,这个拦截器可以把aciton中的property push到valueStack上。Struts2中发行包中本身已经内置了许多拦截器和拦截器堆栈,除了我们大家熟知的defaultStack之外其中还有一个叫做paramsPrepareParamsStack,就如同它的名字一样,在两个params拦截器中夹了一个prepare,当然还有我们的modelDriven,这个拦截器堆栈似乎就是为我们今天的任务所设计的,用好它可以发挥出让人意想不到的威力。
<interceptor-stack name="paramsPrepareParamsStack"> <interceptor-ref name="exception"/> <interceptor-ref name="alias"/> <interceptor-ref name="params"/> <interceptor-ref name="servletConfig"/> <interceptor-ref name="prepare"/> <interceptor-ref name="i18n"/> <interceptor-ref name="chain"/> <interceptor-ref name="modelDriven"/> <interceptor-ref name="fileUpload"/> <interceptor-ref name="checkbox"/> <interceptor-ref name="staticParams"/> <interceptor-ref name="params"/> <interceptor-ref name="conversionError"/> <interceptor-ref name="validation"> <param name="excludeMethods">input,back,cancel</param> </interceptor-ref> <interceptor-ref name="workflow"> <param name="excludeMethods">input,back,cancel</param> </interceptor-ref> </interceptor-stack>
下面我们来看一下我们的博客action类,是的,大家不要惊讶,就是这么简单。
public class PostAction extends BaseAction<Post, Cookie> implements Preparable, ModelDriven<Post> {
private Post model;
@Override
public Post getModel(){
return model;
}
public void prepare() throws Exception {
if(getRequestId()==null || getRequestId().isEmpty()){
model = new Post();
}else{
Result result = ao.doGet(getRequestId());
model = (Post)result.getDefaultModel();
}
}
}
这里ao是我们的业务逻辑类,result是以一个工具类,主要用来在aciton层与业务逻辑层之间传递消息用。当拦截器堆栈里的第一个params拦截器被调用时,它会去request当中寻找requestId,并把值注入到aciton中。然后是prepare拦截器被调用,若requestId值为空,它会new一个post类,并把值赋给model;如果值不为空,它会调用ao取得这个post对象,并把它赋给model,当然了如果调用的aciton的list方法,因为list方法也需要一个requestId,但是list方法往往需要的是一个foreign key而非entity id(primary key)所以此requestId非彼requestId会取不到post对象,不过这并不影响什么。然后是modelDriven拦截器会把这个model对象push到valueStack上。最后,当第二个params拦截器被调用的时候,若用户调用的是save方法,则params拦截器会把用户提交的表单内容注入到post对象中。
不过这里要注意的是因为ao在baseAciton中定义,而对于每个不同的aciton类来说,需要对应不同的ao,所以这里我们的action需要用spring来配置,ao也由spring来注入。下面是对于这个action的配置,可以看到aciton对应的class不再是一个类名,而是spring配置中的bean id,另外别忘了要使用ParamsPrepareParamsStack而不是默认的那个,在这里由于我们增加了cookie拦截器,所以用的是自定义的cookieParamsPrepareParamsStack。还可以看到对于show和list两个result来说我们用了freemarker模板;而对于view跟browse两个result来说我们使用了上一篇当中介绍的dispatcherAction,他们实际对应的是两个jsp页面,每个jsp页面中又分别include了show和list两个aciton(方法)。
<action name="post" class="post"> <interceptor-ref name="cookieParamsPrepareParamsStack" /> <result name="show" type="freemarker">/WEB-INF/ftl/demo/showPost.ftl</result> <result name="list" type="freemarker">/WEB-INF/ftl/demo/listPost.ftl</result> <result name="view" type="redirect">/demo/viewPost.action?requestId=${requestId}</result> <result name="browse" type="redirect">/demo/browse.action</result> <result name="edit">/WEB-INF/jsp/demo/editPost.jsp</result> <result name="input">/WEB-INF/jsp/demo/editPost.jsp</result> <result name="error">/WEB-INF/jsp/demo/error.jsp</result> <result name="none">/WEB-INF/jsp/demo/error.jsp</result> </action>
spring对aciton的配置,对于在Struts2中如何用spring来配置aciton这里就不再赘述了,网上入门的资料很多,读者可以自行查阅。
<bean id="post" class="com.meidusa.pirateweb.web.post.PostAction" scope="prototype"> <property name="ao" ref="postAO"/> </bean>
下面我们再来看一下baseAction这个基类。ClientCookie我们在上一篇当中已经介绍过,这里不再重复,pageable接口以及Paging类等是跟分页相关的,笔者会在本系列的下一篇当中介绍。这里需要指出的是这个baseAction使用了泛型模板,T用来指代model类,K用来指代用户自定义的Cookie类,如果读者对于泛型不是很熟悉的话也请自行查阅相关资料,这里不再赘述(下面还会一直用到)。
public abstract class BaseAction<T,K extends ClientCookie> extends ActionSupport implements ClientCookieAware<K>, Pageable{
protected BaseAO<T> ao;
protected static Logger logger = Logger.getLogger(BaseAction.class);
protected String requestId;
protected List<T> list;
protected K cookie;
protected Paging paging = new Paging();
public Paging getPaging() {
return paging;
}
public void setPaging(Paging paging) {
this.paging = paging;
}
public K getCookie() {
return cookie;
}
public void setClientCookie(K cookie){
this.cookie = cookie;
}
public Date getCurrDateTime(){
return new Date();
}
public List<T> getList() {
return list;
}
public void setList(List<T> list) {
this.list = list;
}
@SkipValidation
public String add() {
return Constants.EDIT;
}
@SkipValidation
public String edit() {
return Constants.EDIT;
}
@SkipValidation
public String show() {
if (getModel()==null){
return ERROR;
}
return Constants.SHOW;
}
public String save() {
Result result = ao.doSave(getModel(),getForeignKey());
if (!result.isSuccess()){
ResultCode resultCode = result.getResultCode();
this.addActionError(resultCode.getMessage().getMessage());
return ERROR;
}
requestId = (String)result.getModels().get("requestId");
return Constants.VIEW;
}
@SkipValidation
public String remove() {
Result result = ao.doRemove(getModel(),getForeignKey());
if (!result.isSuccess()){
ResultCode resultCode = result.getResultCode();
this.addActionError(resultCode.getMessage().getMessage());
return ERROR;
}
requestId = (String)result.getDefaultModel();
if (!list().equals(ERROR)){
return Constants.BROWSE;
}else{
return ERROR;
}
}
@SkipValidation
public String list() {
Result result = ao.doList(requestId, paging);
if (result.isSuccess()){
list = (List)result.getDefaultModel();
return Constants.LIST;
}else {
ResultCode resultCode = result.getResultCode();
this.addActionError(resultCode.getMessage().getMessage());
return ERROR;
}
}
public String getRequestId() {
return requestId;
}
public void setRequestId(String requestId) {
this.requestId = requestId;
}
public void setAo(BaseAO<T> ao) {
this.ao = ao;
}
protected String getForeignKey(){
return cookie.getLoginId();
}
public abstract T getModel();
}
这里还要指出的是public abstract T getModel()方法是抽象的,需要子类来实现。另外每个子类的ao都需要不同实现所以不能要在spring里手工配置来注入而不能直接autowire by name。还有就是对于那些不需要验证的方法使用了@SkipValidation的标注。Struts2中的验证框架是针对单独一个aciton的,而不是aciton中的某个具体的方法,而在我们这个aciton中包含了多个方法,所以对于那些不要验证的方法可以使用框架所提供的标注跳过验证。
下面我们再来看一下baseAO
public interface BaseAO<T> {
public Result doSave(T model, String foreignKey);
public Result doRemove(T model, String foreignKey);
public Result doGet(String id);
public Result doList(String foreignKey, Paging paging);
public Result doPaging(int rows, String loginId);
}
这个baseAO是一个接口,对于CRUDL功能来说,它是action层对业务层调用的规范。一共有5个方法,其中doPaging是跟分读取页相关的,我们会在下一篇中介绍,这里就不再展开。另外4个方法分别对应保存(包括新建和修改)、删除、读取和列表。
下面我们有一个对于这个接口的抽象实现。在这个抽象实现中抽象了基本的CRUDL方法,并给出了默认的实现。
public abstract class AbstractAO<T extends AbstractModel, K extends AbstractExample<? extends AbstractExample.Criteria>> implements BaseAO<T>{
protected AbstractDAO<T,K> dao;
public void setDao(AbstractDAO<T, K> dao) {
this.dao = dao;
}
protected AbstractDAO<T, K> getDao(){
return dao;
}
public Result doSave(T model, String foreignKey){
Result result = new ResultSupport();
result.setSuccess(false);
if (model.getPrimaryKey() == null || model.getPrimaryKey().isEmpty()) {
model.setPrimaryKey(GuidTool.generateFormattedGUID());
model.setForeignKey(foreignKey);
dao.insert(model);
}else{
if (!model.getForeignKey().equals(foreignKey)){
result.setResultCode(ResultCode.ACCESS_VIOLATION);
return result;
}
dao.updateByPrimaryKeySelective(model);
}
result.setDefaultModel("requestId", model.getPrimaryKey());
result.setSuccess(true);
return result;
}
public Result doRemove(T model, String foreignKey){
Result result = new ResultSupport();
result.setSuccess(false);
if (!model.getForeignKey().equals(foreignKey)){
result.setResultCode(ResultCode.ACCESS_VIOLATION);
return result;
}
dao.deleteByPrimaryKey(model.getPrimaryKey());
result.setDefaultModel(model.getForeignKey());
result.setSuccess(true);
return result;
}
public Result doGet(String id){
Result result = new ResultSupport();
result.setSuccess(false);
T model = dao.selectByPrimaryKey(id);
if (model == null){
result.setResultCode(ResultCode.MODEL_OBJECT_NOT_EXIST);
return result;
}
result.setDefaultModel(model);
result.setSuccess(true);
return result;
}
public Result doList(String foreignKey, Paging paging){
Result result = new ResultSupport();
result.setSuccess(false);
K example = doExample(foreignKey);
if (paging.getRows()!=0){
example.setOffset(paging.getOffset());
example.setRows(paging.getRows());
}
List<T> list = dao.selectByExampleWithoutBLOBs(example);
result.setDefaultModel(list);
result.setSuccess(true);
return result;
}
public Result doPaging(int rows, String foreignKey){
Result result = new ResultSupport();
result.setSuccess(false);
K example = doExample(foreignKey);
int totalRecord = dao.countByExample(example);
int totalPage = (int)Math.ceil(totalRecord / (double)rows);
result.setDefaultModel("totalRecord", totalRecord);
result.setDefaultModel("totalPage", totalPage);
result.setSuccess(true);
return result;
}
protected abstract K doExample(String foreignKey);
}
而我们的客户客户代码只需要实现一个doExample方法就可以了。
public class PostAO extends AbstractAO<Post, PostExample> {
@Override
protected PostExample doExample(String foreignKey) {
PostExample example = new PostExample();
example.createCriteria().andLoginidEqualTo(foreignKey);
return example;
}
}
在DAO层
抽象model类
public abstract class AbstractModel {
public abstract String getPrimaryKey();
public abstract void setPrimaryKey(String primaryKey);
public abstract String getForeignKey();
public abstract void setForeignKey(String foreignKey);
}
抽象DAO接口
public interface AbstractDAO<T,K> {
int countByExample(K example);
int deleteByExample(K example);
int deleteByPrimaryKey(String id);
void insert(T record);
void insertSelective(T record);
List<T> selectByExampleWithBLOBs(K example);
List<T> selectByExampleWithoutBLOBs(K example);
T selectByPrimaryKey(String id);
int updateByExampleSelective(T record, K example);
int updateByExampleWithBLOBs(T record, K example);
int updateByExampleWithoutBLOBs(T record, K example);
int updateByPrimaryKeySelective(T record);
int updateByPrimaryKeyWithBLOBs(T record);
int updateByPrimaryKeyWithoutBLOBs(T record);
}
抽象example类
public abstract class AbstractExample<T extends AbstractExample.Criteria> {
protected String orderByClause;
protected List<T> oredCriteria;
public AbstractExample() {
oredCriteria = new ArrayList<T>();
}
protected int offset;
protected int rows;
protected boolean distinct;
public boolean isDistinct() {
return distinct;
}
public void setDistinct(boolean distinct) {
this.distinct = distinct;
}
public void setOffset(int offset){
this.offset = offset;
}
public int getOffset(){
return offset;
}
public void setRows(int rows){
this.rows = rows;
}
public int getRows(){
return rows;
}
protected AbstractExample(AbstractExample<T> example) {
this.orderByClause = example.orderByClause;
this.oredCriteria = example.oredCriteria;
this.offset = example.offset;
this.rows = example.rows;
this.distinct = example.distinct;
}
public void setOrderByClause(String orderByClause) {
this.orderByClause = orderByClause;
}
public String getOrderByClause() {
return orderByClause;
}
public List<T> getOredCriteria() {
return oredCriteria;
}
public void or(T criteria) {
oredCriteria.add(criteria);
}
public T createCriteria() {
T criteria = createCriteriaInternal();
if (oredCriteria.size() == 0) {
oredCriteria.add(criteria);
}
return criteria;
}
protected abstract T createCriteriaInternal();
public void clear() {
oredCriteria.clear();
orderByClause = null;
distinct = false;
offset = 0;
rows = 0;
}
public static abstract class Criteria {
protected List<String> criteriaWithoutValue;
protected List<Map<String, Object>> criteriaWithSingleValue;
protected List<Map<String, Object>> criteriaWithListValue;
protected List<Map<String, Object>> criteriaWithBetweenValue;
protected Criteria() {
super();
criteriaWithoutValue = new ArrayList<String>();
criteriaWithSingleValue = new ArrayList<Map<String, Object>>();
criteriaWithListValue = new ArrayList<Map<String, Object>>();
criteriaWithBetweenValue = new ArrayList<Map<String, Object>>();
}
public boolean isValid() {
return criteriaWithoutValue.size() > 0 || criteriaWithSingleValue.size() > 0
|| criteriaWithListValue.size() > 0 || criteriaWithBetweenValue.size() > 0;
}
public List<String> getCriteriaWithoutValue() {
return criteriaWithoutValue;
}
public List<Map<String, Object>> getCriteriaWithSingleValue() {
return criteriaWithSingleValue;
}
public List<Map<String, Object>> getCriteriaWithListValue() {
return criteriaWithListValue;
}
public List<Map<String, Object>> getCriteriaWithBetweenValue() {
return criteriaWithBetweenValue;
}
protected void addCriterion(String condition) {
if (condition == null) {
throw new RuntimeException("Value for condition cannot be null");
}
criteriaWithoutValue.add(condition);
}
protected void addCriterion(String condition, Object value, String property) {
if (value == null) {
throw new RuntimeException("Value for " + property + " cannot be null");
}
Map<String, Object> map = new HashMap<String, Object>();
map.put("condition", condition);
map.put("value", value);
criteriaWithSingleValue.add(map);
}
protected void addCriterion(String condition, List<? extends Object> values, String property) {
if (values == null || values.size() == 0) {
throw new RuntimeException("Value list for " + property
+ " cannot be null or empty");
}
Map<String, Object> map = new HashMap<String, Object>();
map.put("condition", condition);
map.put("values", values);
criteriaWithListValue.add(map);
}
protected void addCriterion(String condition, Object value1, Object value2, String property) {
if (value1 == null || value2 == null) {
throw new RuntimeException("Between values for " + property + " cannot be null");
}
List<Object> list = new ArrayList<Object>();
list.add(value1);
list.add(value2);
Map<String, Object> map = new HashMap<String, Object>();
map.put("condition", condition);
map.put("values", list);
criteriaWithBetweenValue.add(map);
}
public abstract Criteria andForeignKeyEqualTo(String value);
}
}
我们需要为每个抽象类提供一个实现并且还要配置好ibatis的sqlMapper,看起来工作量比较大,不过还好我们有工具。使用过iBatis的朋友一定知道在Apache基金会iBatis项目下还有一个叫IBator的子项目,它是专门为iBatis自动生成代码的工具,即可以在命令行下运行,也可以作为eclipse的一个插件来运行,不过大家一般都会在eclipse下使用。笔者在IBator的基础上修改了一下,重新做了一个eclipse插件, 笔者叫它ibatorPlus,呵呵。ibatorPlus可以让生成的代码分别自动继承AbstractModel ,AbstractExample,实现AbstractDAO接口。在代码生成完之后只需要实现AbstractModel中的4个抽象方法就可以了。
@Override
public String getForeignKey() {
return loginid;
}
@Override
public String getPrimaryKey() {
return id;
}
@Override
public void setForeignKey(String foreignKey) {
this.loginid = foreignKey;
}
@Override
public void setPrimaryKey(String primaryKey) {
this.id = primaryKey;
}
Ibator与ibatorPlus,使用了这个工具之后,在dao层所有需要手工写的代码只有上面区区4个。
使用ibatorPlus生成的sqlmap还可以自动包含分页的代码limit $offset$, $rows$(mysql数据库)。自动分页功能我们会在本系列的下一篇详细介绍,这里就不在赘述。
<select id="selectByExample" parameterClass="com.meidusa.demo.dal.model.PostExample" resultMap="BaseResultMap"> <!-- WARNING - @ibatorgenerated This element is automatically generated by Apache iBATIS Ibator, do not modify. This element was generated on Thu Feb 04 16:15:41 CST 2010. --> select <isParameterPresent> <isEqual compareValue="true" property="distinct"> distinct </isEqual> </isParameterPresent> <include refid="post.Base_Column_List" /> from post <isParameterPresent> <include refid="post.Example_Where_Clause" /> <isNotNull property="orderByClause"> order by $orderByClause$ </isNotNull> <isNotEqual compareValue="0" property="rows"> limit $offset$, $rows$ </isNotEqual> </isParameterPresent> </select>
至此,本篇总算是介绍完了,使用本篇所介绍的方法来我们只需手工写非常少量的代码就能获得全部的CRUDL功能。下面我们再把需要手工书写的代码罗列一下。
web层action
public class PostAction extends BaseAction<Post, Cookie> implements Preparable, ModelDriven<Post> {
private Post model;
@Override
public Post getModel(){
return model;
}
public void prepare() throws Exception {
if(getRequestId()==null || getRequestId().isEmpty()){
model = new Post();
}else{
Result result = ao.doGet(getRequestId());
model = (Post)result.getDefaultModel();
}
}
}
业务逻辑层ao
public class PostAO extends AbstractAO<Post, PostExample> {
@Override
protected PostExample doExample(String foreignKey) {
PostExample example = new PostExample();
example.createCriteria().andLoginidEqualTo(foreignKey);
return example;
}
}
数据访问层dao
@Override
public String getForeignKey() {
return loginid;
}
@Override
public String getPrimaryKey() {
return id;
}
@Override
public void setForeignKey(String foreignKey) {
this.loginid = foreignKey;
}
@Override
public void setPrimaryKey(String primaryKey) {
this.id = primaryKey;
}
其它就仅仅只剩下了xml的配置。