本期主要是文章保存功能,涉及到草稿、文章发布、历史这三个要点。
项目github地址:github.com/pc859107393…
实时项目同步的地址是国内的码云:git.oschina.net/859107393/m…
我的简书首页是:www.jianshu.com/users/86b79…
上一期是:[手把手教程][第二季]java 后端博客系统文章系统——No5
工具
- IDE为idea16
- JDK环境为1.8
- gradle构建,版本:2.14.1
- Mysql版本为5.5.27
- Tomcat版本为7.0.52
- 流程图绘制(xmind)
- 建模分析软件PowerDesigner16.5
- 数据库工具MySQLWorkBench,版本:6.3.7build
本期目标
- mybatis一些简单的进阶使用
- 日志功能的记录
- 文章发布
- 草稿保存
实用技巧
- 通过面向对象建模后,我们可以逆向工程生成对应的bean甚至一些其他的代码
- mybatis可以使用动态sql语句执行复杂操作
- 使用pojo包装类型进行数据库操作
- 使用高级映射可以实现复杂的业务场景
- 延迟加载和缓存开发
- mybatis逆向工程工具
文章提交
上期我们通过对数据库的分析,仔仔细细的探索了一遍WordPress程序运行时候文章提交和草稿提交的区别和共同点,简略的概括下如下:
- 文章和草稿区别是postType不同
- 文章和草稿都有日志记录
- 日志的postStatus都是closed
- 日志的postParent始终是指向其文章的id
- ···
更为详细的文章,建议去我的上一篇文章查看。
既然我们已经大概明白了怎么去实现,那么现在我们需要的就是进行实验。
首先老规矩,先从我们的dao层实现开始,我们争取把各个功能模块解耦,那么我们就不能过于在dao层限制,所以我们需要在dao层实现数据(文章、草稿、历史记录)插入。所以我们先生成dao接口如下:
@Repository("postDao")
public interface PostDao extends Dao<PostCustom> {
/**
* 根据扩展类来查询文章信息,文章为postBean
* @param postCustom post的扩展类,使用注解来说明对象名称
* @return 受影响的行数
*/
@Override
int add(@Param(value = "postCustom") PostCustom postCustom);
//分割线///
@Override
int del(PostCustom postBean);
@Override
int update(PostCustom postBean);
@Override
PostCustom findOneById(Serializable postId);
@Override
List<PostCustom> findAll();
List<PostCustom> findAllNew();
/**
* 分页查询
*
* @param offset 起始位置
* @param limit 每页数量
* @return
*/
List<PostCustom> findAllPublish(@Param("offset") int offset, @Param("limit") int limit);
int getAllCount();
List<PostCustom> getAllPostDateCount();
}
//在上面分割线上面的,就是我们这次提交文章的接口,下面我们接着完成mapper
<insert id="add" parameterType="PostCustom">
<!--
selectKey是在insert语句执行的时候可以执行的语句。
keyProperty是指定需要记录的key
order是选择在insert执行之前还是之后执行selectKey中的语句
resultType是返回的数据类型
SELECT LAST_INSERT_ID();是返回最近执行insert后增加的内容的自增主键。注意,这个语句仅仅可用于自增主键。
-->
<selectKey keyProperty="postCustom.id" order="AFTER" resultType="java.lang.String">
SELECT LAST_INSERT_ID();
</selectKey>
<choose>
<when test="postCustom.id == null">
#文章id为空说明是刚开始插入新的文章
INSERT INTO `wp_posts`
(`post_author`, `post_date`, `post_date_gmt`, `post_content`, `post_title`, `post_excerpt`,
`post_status`, `comment_status`, `comment_count`, `ping_status`, `post_password`,
`post_name`, `to_ping`, `pinged`, `post_modified`, `post_modified_gmt`,
`post_content_filtered`, `post_parent`, `guid`, `menu_order`, `post_type`, `post_mime_type`)
VALUES (#{postCustom.postAuthor},#{postCustom.postDate},#{postCustom.postDateGmt},#{postCustom.postContent}, #{postCustom.postTitle}, #{postCustom.postExcerpt},
#{postCustom.postStatus},#{postCustom.commentStatus},#{postCustom.commentCount},#{postCustom.pingStatus},#{postCustom.postPassword},#{postCustom.postName},
#{postCustom.toPing},#{postCustom.pinged},#{postCustom.postModified},#{postCustom.postModifiedGmt},#{postCustom.postContentFiltered},
#{postCustom.postParent},#{postCustom.guid},#{postCustom.menuOrder},#{postCustom.postType},#{postCustom.postMimeType})
</when>
<when test="postCustom.id != null">
#id不为空说明现在是更新某个文章
# UPDATE wp_posts
# SET `post_author`=#{postAuthor},`post_date`=#{postDate}
# WHERE ID=#{id}
</when>
</choose>
</insert>复制代码
上面的语句中我们可以看到在mapper的 insert语句块中我们插入了selectKey语句块,主要是用来返回我们插入的条目的自增id。为什么这里我们非要获取到插入的id呢?主要是文章生成后的日志记录都是需要文章id才能生成对应的日志记录。
同时在inset语句块中,我们使用了choose+when语句块生成了动态sql,这样就能动态的选择程序执行。
同时在上面的代码中我们可以看到上面的when和下面的when语句块中关于bean的属性获取使用方式不一样!
重点:扩展类型,要使用父类的属性时,必须是对象名.属性名的形式,对象名从dao的接口设置。(否则直接使用属性的时候,会异常提示空指针,且提示对象的属性中没有该属性)
小技巧:
- 在mapper中使用属性时,最好是对象名.属性名的方式,同时在dao层中设置对象的名称(在我们上面dao层的add接口中有说明)
- mybatis的动态sql可以减少很多额外的方法和mapper中的sql编写,所以必须掌握动态sql
- 当属性较多的时候,我们一定不能忘记单元测试,单元测试可以简单直观的了解到哪里出错,减少在后续开发中错误检查的时间。
PostDao的单元测试
上面我们也是提到过单元测试,那么我们现在看看这个dao的单元测试应该如何完成呢?
首先给大家看看我们文章的bean:
public class PostBean implements Serializable {
/**
* 版本号
*/
private static final long serialVersionUID = 4477235816922585138L;
private String id;
private String postAuthor;
private Date postDate;
private Date postDateGmt;
private String postContent;
private String postTitle;
private String postExcerpt;
private String postStatus;
private String commentStatus;
private String pingStatus;
private String postPassword;
private String postName;
private String toPing;
private String pinged;
private Date postModified;
private Date postModifiedGmt;
private String postContentFiltered;
private String postParent = "0";
private String guid;
private Integer menuOrder;
private String postType;
private String postMimeType;
private Long commentCount = 0L;
//省略get、set
@Override
public String toString() {
return "PostBean{" +
"id='" + id + '\'' +
", postAuthor='" + postAuthor + '\'' +
", postDate=" + postDate +
", postDateGmt=" + postDateGmt +
", postContent='" + postContent + '\'' +
", postTitle='" + postTitle + '\'' +
", postExcerpt='" + postExcerpt + '\'' +
", postStatus='" + postStatus + '\'' +
", commentStatus='" + commentStatus + '\'' +
", pingStatus='" + pingStatus + '\'' +
", postPassword='" + postPassword + '\'' +
", postName='" + postName + '\'' +
", toPing='" + toPing + '\'' +
", pinged='" + pinged + '\'' +
", postModified=" + postModified +
", postModifiedGmt=" + postModifiedGmt +
", postContentFiltered='" + postContentFiltered + '\'' +
", postParent='" + postParent + '\'' +
", guid='" + guid + '\'' +
", menuOrder=" + menuOrder +
", postType='" + postType + '\'' +
", postMimeType='" + postMimeType + '\'' +
", commentCount=" + commentCount +
'}';
}
}复制代码
哈哈哈哈哈,是不是特别的多呢?又没有一种蛋疼菊紧的赶脚?
说实话,一看到这里的时候,我也很惆怅。灵机一动,感觉可以祭出这个系列教程我们自己手写的第一个设计模式了!开启我们的javaBean的建造者模式,go!
import cn.acheng1314.utils.StringUtils;
import com.sun.istack.internal.NotNull;
import java.util.Date;
/**
* Description:文章上传的扩展
*
* @author acheng
* @date
*/
public class PostCustom extends PostBean {
private Boolean isPublish;
public PostCustom() {
}
public PostCustom(String id, String postAuthor, Date postDate, Date postDateGmt, String postContent, String postTitle, String postExcerpt, String postStatus, String commentStatus, String pingStatus, String postPassword, String postName, String toPing, String pinged, Date postModified, Date postModifiedGmt, String postContentFiltered, String postParent, String guid, Integer menuOrder, String postType, String postMimeType, Long commentCount) {
setId(id);
setPostAuthor(postAuthor);
setPostDate(postDate);
setPostDateGmt(postDateGmt);
setPostContent(postContent);
setPostContentFiltered(postContentFiltered);
setPostTitle(postTitle);
setPostExcerpt(postExcerpt);
setPostStatus(postStatus);
setCommentStatus(commentStatus);
setPingStatus(pingStatus);
setPostPassword(postPassword);
setPostName(postName);
setToPing(toPing);
setPinged(pinged);
setPostModified(postModified);
setPostModifiedGmt(postModifiedGmt);
setPostParent(postParent);
setGuid(guid);
setMenuOrder(menuOrder);
setPostType(postType);
setPostMimeType(postMimeType);
setCommentCount(commentCount);
}
public Boolean getPublish() {
return isPublish;
}
public void setPublish(Boolean publish) {
isPublish = publish;
}
/**
* 建造者,参数过于多的时候考虑使用建造者完成。
* 静态内部类不使用的时候是不会被创建的。
*/
public static class Builder {
private String id;
private String postAuthor;
private Date postDate;
private Date postDateGmt;
private String postContent;
private String postTitle;
private String postExcerpt;
private String postStatus;
private String commentStatus;
private String pingStatus;
private String postPassword;
private String postName;
private String toPing;
private String pinged;
private Date postModified;
private Date postModifiedGmt;
private String postContentFiltered;
private String postParent;
private String guid;
private Integer menuOrder;
private String postType;
private String postMimeType;
private Long commentCount;
public Builder id(String id) {
this.id = id;
return this;
}
public Builder postAuthor(@NotNull String postAuthor) {
this.postAuthor = postAuthor;
return this;
}
public Builder postDate(Date postDate) {
this.postDate = postDate;
return this;
}
public Builder postDateGmt(Date postDateGmt) {
this.postDateGmt = postDateGmt;
return this;
}
public Builder postContent(String postContent) {
this.postContent = StringUtils.isEmpty(postContent) ? "" : postContent;
return this;
}
public Builder postTitle(String postTitle) {
this.postTitle = StringUtils.isEmpty(postTitle) ? "" : postTitle;
return this;
}
public Builder postExcerpt(String postExcerpt) {
this.postExcerpt = postExcerpt;
return this;
}
public Builder postStatus(String postStatus) {
this.postStatus = postStatus;
return this;
}
public Builder commentStatus(String commentStatus) {
this.commentStatus = commentStatus;
return this;
}
public Builder pingStatus(String pingStatus) {
this.pingStatus = pingStatus;
return this;
}
public Builder postPassword(String postPassword) {
this.postPassword = postPassword;
return this;
}
public Builder postName(String postName) {
this.postName = postName;
return this;
}
public Builder toPing(String toPing) {
this.toPing = toPing;
return this;
}
public Builder pinged(String pinged) {
this.pinged = pinged;
return this;
}
public Builder postModified(Date postModified) {
this.postModified = postModified;
return this;
}
public Builder postModifiedGmt(Date postModifiedGmt) {
this.postModifiedGmt = postModifiedGmt;
return this;
}
public Builder postContentFiltered(String postContentFiltered) {
this.postContentFiltered = postContentFiltered;
return this;
}
public Builder postParent(String postParent) {
this.postParent = postParent;
return this;
}
public Builder guid(String guid) {
this.guid = guid;
return this;
}
public Builder menuOrder(Integer menuOrder) {
this.menuOrder = menuOrder;
return this;
}
public Builder postType(String postType) {
this.postType = postType;
return this;
}
public Builder postMimeType(String postMimeType) {
this.postMimeType = postMimeType;
return this;
}
public Builder commentCount(Long commentCount) {
this.commentCount = commentCount;
return this;
}
public PostCustom build() {
return new PostCustom(id,
postAuthor,
postDate,
postDateGmt,
postContent,
postTitle,
postExcerpt,
postStatus,
commentStatus,
pingStatus,
postPassword,
postName,
toPing,
pinged,
postModified,
postModifiedGmt,
postContentFiltered,
postParent,
guid,
menuOrder,
postType,
postMimeType,
commentCount);
}
}
}复制代码
那么我们ben生成好了后,我们就需要开始写我们的单元测试,如下:
@Test
public void testInsertPost() {
Date atNow = new Date();
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
formatter.format(atNow);
// PostCustom postCustom = new PostCustom();
// postCustom.setId(null);
// postCustom.setPublish(true);
// postCustom.setPostAuthor("001");
// postCustom.setPostDate(atNow);
// postCustom.setPostDateGmt(atNow);
// postCustom.setPostContent("1111111");
// postCustom.setPostTitle("22222");
// postCustom.setPostExcerpt("11111289489589458");
// postCustom.setPostStatus("11111289489589458");
// postCustom.setCommentStatus("11111289489589458");
// postCustom.setPingStatus("11111289489589458");
// postCustom.setPostPassword("11111289489589458");
// postCustom.setPostName("11111289489589458");
// postCustom.setToPing("11111289489589458");
// postCustom.setPinged("11111289489589458");
// postCustom.setPostModified(atNow);
// postCustom.setPostModifiedGmt(atNow);
// postCustom.setPostContentFiltered(formatter.format(atNow));
// postCustom.setGuid(formatter.format(atNow));
// postCustom.setMenuOrder(11100111);
// postCustom.setCommentStatus();
PostCustom postCustom = PostInitUtils.setPostinfo("0", atNow, "我是一个兵999", "我是一个兵2111", "文章内容````嘛", "www.cc1tv.com");
postDao.add(postCustom);
System.out.print("id=" + postCustom.getId());
postCustom = PostInitUtils.insertPostLog(postCustom.getId(), postCustom.getPostAuthor(), postCustom.getPostTitle(), postCustom.getGuid(), postCustom.getPostContent());
postDao.add(postCustom);
}复制代码
大家可以看到上面我们的PostCustom还是从我们自己手动封装的PostInitUtils中获取的,同时我们的PostCustom还是我们的PostBean的子类。所以这里就用到了传说中的pojo包装类型相关的一些知识(我们这里并没有包装,不过已经用到了扩展类),让我们接着看看PostInitUtils的代码:
import cn.acheng1314.domain.PostCustom;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Description:文章上传
*
* @author acheng
* @date 2017/2/27
*/
public class PostInitUtils {
public final static String PUBLISH_POST_STATUS = "publish";
public final static String OPEN_COMMENT_STATUS = "open";
public final static String OPEN_PING_STATUS = "open";
public final static String POST_TYPE = "post";
public final static String INHERIT_POST_STATUS = "inherit";
public final static String CLOSED_COMMENT_STATUS = "closed";
public final static String REVISION_POST_MIME_TYPE = "revision";
/**
* 发布文章,这时候没有ID
*
* @param postAuthor 文章作者
* @param postDate 文章提交日期
* @param postTitle 文章标题
* @param postName 文章名字一般类说是文章标题进行url转码
* @param postContent 文章内容
* @param guid 文章的guid
* @return
*/
public static PostCustom setPostinfo(
String postAuthor
, Date postDate
, String postTitle
, String postName
, String postContent
, String guid) {
Date atNow = new Date();
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
formatter.format(atNow);
return new PostCustom.Builder()
.id(null)
.postAuthor(postAuthor)
.postDate(postDate)
.postDateGmt(atNow)
.postTitle(postTitle)
.postExcerpt("")
.postStatus(PUBLISH_POST_STATUS)
.commentStatus(OPEN_COMMENT_STATUS)
.commentCount(0L)
.pingStatus(OPEN_PING_STATUS)
.postPassword("")
.postName(postName)
.toPing("")
.pinged("")
.postModified(atNow)
.postModifiedGmt(atNow)
.postContentFiltered("")
.postContent(postContent)
.postParent("0")
.guid(guid)
.menuOrder(0)
.postType(POST_TYPE)
.postMimeType("")
.build();
}
/**
* 文章操作时候,插入日志
* @param parentId 修改文章的ID
* @param postAuthor 文章作者
* @param postTitle 文章标题
* @param guid 文章的访问标记
* @param postContent 文章内容
* @return
*/
public static PostCustom insertPostLog(String parentId, String postAuthor, String postTitle, String guid, String postContent){
Date atNow = new Date();
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
formatter.format(atNow);
return new PostCustom.Builder()
.id(null)
.postAuthor(postAuthor)
.postDate(atNow)
.postDateGmt(atNow)
.postTitle(postTitle)
.postContent(postContent)
.postExcerpt("")
.postStatus(INHERIT_POST_STATUS)
.commentStatus(CLOSED_COMMENT_STATUS)
.commentCount(0L)
.pingStatus(CLOSED_COMMENT_STATUS)
.postPassword("")
.postName(parentId+"-revision-v1")
.toPing("")
.pinged("")
.postModified(atNow)
.postModifiedGmt(atNow)
.postContentFiltered("")
.postParent(parentId)
.guid(guid)
.menuOrder(0)
.postType(REVISION_POST_MIME_TYPE)
.postMimeType("")
.build();
}
}复制代码
这里我们可以看到我们对外开放的核心也就文章核心信息相关的一些东西了,那么这样我们就能很好的控制数据。
现在我们可以看下数据库,最新多出来的数据和文章首页插入的数据如下:
我们今天的主干内容到此基本完成,剩下的从dao到前台界面适配等等基本上骚年们自己都可以完成至此不在赘述。
核心总结
- mapper中的文章写入处标准的主要数据库操作代码应该是insert,所以文章更新的应该单独开放接口来完成
- pojo实际上是文章的包装类型,javaBean中不但有扩展类型(子类是父类的扩展类型)更有包装类型,包装类型我们在常用的json输出的时候有用到,大家可以自己多多动脑。
- dao层应该专注于提供单一数据驱动,Service对外提供数据接口,可以适当参与业务场景的数据关系处理。