讓我們首先來看一下什麼是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的配置。