V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
javahih
V2EX  ›  Java

基于 Spring Data JPA 框架的文章归档实现

  •  
  •   javahih · 2018-01-15 11:17:39 +08:00 · 3447 次点击
    这是一个创建于 2538 天前的主题,其中的信息可能已经有所发展或是发生改变。

    目录

    [TOC]

    前言

    最近在写自己的个人博客系统,框架采用 SpringMVC、Spring4.0、Spring Data/JPA 组合,本博客就文档归档功能在 Spring Data JPA 框架下是如何实现的进行记录。 现在可以 star(收藏),watch(关注),但是还在开发中,所以还是还在先别下载 项目 github: https://github.com/u014427391/myblog

    文章信息设计

    数据暂时这样设计,仅供学习参考,对于文章评论回复,栏目之间的关联还没设计,不过本博客的目的是记录文档归档功能的实现,这个并不会影响 这里写图片描述

    VO 类:全部采用注解,注意因为我数据库表名为 article,所以不需要写 @Table 注解,表名为其它的话,就需要自己添加 @Table 注解了

    package net.myblog.entity;
    
    import java.util.Date;
    
    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.Id;
    import javax.persistence.Temporal;
    import javax.persistence.TemporalType;
    
    /**
     * 博客系统文章信息的实体类
     * @author Nicky
     */
    @Entity
    public class Article {
    	
    	/** 文章 Id,自增**/
    	private int articleId;
    	
    	/** 文章名称**/
    	private String articleName;
    	
    	/** 文章发布时间**/
    	private Date articleTime;
    	
    	/** 图片路径,测试**/
    	private String imgPath;
    	
    	/** 文章内容**/
    	private String articleContent;
    	
    	/** 查看人数**/
    	private int articleClick;
    	
    	/** 是否博主推荐。0 为否; 1 为是**/
    	private int articleSupport;
    	
    	/** 是否置顶。0 为; 1 为是**/
    	private int articleUp;
    	
    	/** 文章类别。0 为私有,1 为公开,2 为仅好友查看**/
    	private int articleType;
    	
    	@GeneratedValue
    	@Id
    	public int getArticleId() {
    		return articleId;
    	}
    
    	public void setArticleId(int articleId) {
    		this.articleId = articleId;
    	}
    
    	@Column(length=100, nullable=false)
    	public String getArticleName() {
    		return articleName;
    	}
    
    	public void setArticleName(String articleName) {
    		this.articleName = articleName;
    	}
    
    	@Temporal(TemporalType.DATE)
    	@Column(nullable=false, updatable=false)
    	public Date getArticleTime() {
    		return articleTime;
    	}
    
    	public void setArticleTime(Date articleTime) {
    		this.articleTime = articleTime;
    	}
    
    	@Column(length=100)
    	public String getImgPath() {
    		return imgPath;
    	}
    
    	public void setImgPath(String imgPath) {
    		this.imgPath = imgPath;
    	}
    
    	@Column(nullable=false)
    	public String getArticleContent() {
    		return articleContent;
    	}
    
    	public void setArticleContent(String articleContent) {
    		this.articleContent = articleContent;
    	}
    
    	public int getArticleClick() {
    		return articleClick;
    	}
    
    	public void setArticleClick(int articleClick) {
    		this.articleClick = articleClick;
    	}
    
    	public int getArticleSupport() {
    		return articleSupport;
    	}
    
    	public void setArticleSupport(int articleSupport) {
    		this.articleSupport = articleSupport;
    	}
    
    	public int getArticleUp() {
    		return articleUp;
    	}
    
    	public void setArticleUp(int articleUp) {
    		this.articleUp = articleUp;
    	}
    
    	@Column(nullable=false)
    	public int getArticleType() {
    		return articleType;
    	}
    
    	public void setArticleType(int articleType) {
    		this.articleType = articleType;
    	}
    }
    
    

    代码实现步骤

    文章表里有很多数据,要按照年月获取文章进行归档的话,我们可以使用如下 SQL 对数据进行分组

    SELECT YEAR(articleTime) AS 'year',MONTH(articleTime) AS 'month',COUNT(*) AS 'count' FROM article 
    	GROUP BY YEAR(articleTime) DESC,MONTH(articleTime);
    

    然后编写数据库层的 Repository 类,类实现 Spring Data JPA 提供的接口

    package net.myblog.repository;
    
    import java.util.Date;
    import java.util.List;
    
    import net.myblog.entity.Article;
    
    import org.springframework.data.jpa.repository.Query;
    import org.springframework.data.repository.PagingAndSortingRepository;
    import org.springframework.data.repository.query.Param;
    
    public interface ArticleRepository extends PagingAndSortingRepository<Article,Integer>{
    	/**
    	 * 文章归档信息获取
    	 * @return
    	 */
    	@Query(value="select year(a.articleTime) as year,month(a.articleTime) as month,"
    			+ "count(a) as count from Article a group by year(a.articleTime),month(a.articleTime)",
    			countQuery="select count(1) from (select count(1) from Article a group by year(a.articleTime),month(a.articleTime))")
    	public List<Object[]> findArticleGroupByTime();
    	
    }
    

    然后在 Service 类,用 @Autowired 注解调用

    package net.myblog.service;
    
    import java.util.Date;
    import java.util.List;
    
    import net.myblog.entity.Article;
    import net.myblog.repository.ArticleRepository;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.domain.Page;
    import org.springframework.data.domain.PageRequest;
    import org.springframework.data.domain.Sort.Direction;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    @Service
    public class ArticleService {
    	
    	@Autowired ArticleRepository articleRepository;
    	/**
    	 * 文章归档信息获取
    	 * @return
    	 */
    	@Transactional
    	public List<Object[]> findArticleGroupByTime(){
    		return articleRepository.findArticleGroupByTime();
    	}
    	
    }
    
    

    然后在 Controller 里调用,用的是 SpringMVC 框架

    package net.myblog.web.controller;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Vector;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import net.myblog.service.ArticleService;
    import net.sf.json.JSONArray;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.domain.Page;
    import org.springframework.data.domain.Sort.Direction;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.servlet.ModelAndView;
    
    @Controller
    public class BlogIndexController extends BaseController{
    	
    	@Autowired
    	ArticleService articleService;
    	
    	/**
    	 * 访问博客主页
    	 * @return
    	 */
    	@RequestMapping(value="/toblog",produces="text/html;charset=UTF-8")
    	public ModelAndView toBlog(HttpServletRequest request, HttpServletResponse response, Model model)throws ClassNotFoundException{
    		
    		//获取归档文章信息
    		List<Object[]> archiveArticles = articleService.findArticleGroupByTime();
    		
    		model.addAttribute("archiveArticles", archiveArticles);
    		mv.setViewName("myblog/frame/index");
    		return mv;
    	}
    	
    }
    
    

    在 JSP 页面调用显示:

    <h2><p>文章归档</p></h2>
    <ul class="news">
      <c:choose>
    	<c:when test="${not empty archiveArticles }">
    		<c:forEach items="${archiveArticles }" var="ac">
    		<li><a href="getArchiveArticles.do?yearmonth=${ac[0]}-${ac[1]}">
    		${ac[0]}年${ac[1]}月(${ac[2] })</a></li>
    		</c:forEach>
    	</c:when>
    	<c:otherwise>
    		<li><a href="#">没有相关数据</a></li>
    	</c:otherwise>  
      </c:choose>
    </ul>
    

    效果如图所示: 这里写图片描述

    文档归档信息查询

    然后介绍点击文档归档信息后,获取文章信息的实现,其实也就是按年月查询文档信息

    在 Repository 类里添加方法:

    /**
    	 * 按月份获取文章信息
    	 * @param month
    	 * 			月份数
    	 * @return
    	 */
    	@Query("from Article a where date_format(a.articleTime,'%Y%m')=date_format((:yearmonth),'%Y%m') "
    			+ "order by articleTime desc")
    	public List<Article> findArticleByMonth(@Param("yearmonth")Date yearmonth);
    
    

    Service 类里调用:

    	/**
    	 * 按月份获取文章信息
    	 * @param month
    	 * @return
    	 */
    	@Transactional
    	public List<Article> findArticleByMonth(Date month){
    		return articleRepository.findArticleByMonth(month);
    	}
    

    在 JSP 页面写入,getArchiveArticles.do 就是要访问的 url,传入 yearmonth 参数就可以

    <h2><p>文章归档</p></h2>
    <ul class="news">
      <c:choose>
    	<c:when test="${not empty archiveArticles }">
    		<c:forEach items="${archiveArticles }" var="ac">
    		<li><a href="getArchiveArticles.do?yearmonth=${ac[0]}-${ac[1]}">
    		${ac[0]}年${ac[1]}月(${ac[2] })</a></li>
    		</c:forEach>
    	</c:when>
    	<c:otherwise>
    		<li><a href="#">没有相关数据</a></li>
    	</c:otherwise>  
      </c:choose>
    </ul>
    

    在 Controller 类里进行处理:

    @RequestMapping("/getArchiveArticles")
    	public ModelAndView getArticleByMonth(HttpServletRequest request){
    		String yearMonthString = request.getParameter("yearmonth");
    		System.out.println("month:"+yearMonthString);
    		ModelAndView mv = this.getModelAndView();
    		
    		Date yearmonth = DateUtils.parse("yyyy-MM", yearMonthString);
    		
    		List<Article> articles = articleService.findArticleByMonth(yearmonth);
    
    		mv.addObject("articles", articles);
    		mv.setViewName("myblog/article/archive_articles");
    		return mv;
    	}
    

    这里写图片描述

    附录(工具类、公共类代码)

    DateUtils.java、BaseController.java 类 DateUtil.java

    package net.myblog.utils;
    
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.Locale;
    
    import net.myblog.core.Constants;
    
    public class DateUtils {
    	
    	public static String formatDate(Date date) throws ParseException{
    		SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    		return dateFormat.format(date);
    	}
    	
    	 /**
    	   * 解析日期,注:此处为严格模式解析,即 20151809 这样的日期会解析错误
    	   * 
    	   * @param pattern
    	   * @param date
    	   * @return
    	   */
    	  public static Date parse(String pattern, String date){
    	    return parse(pattern, date, Constants.LOCALE_CHINA);
    	  }
    
    	  /**
    	   * 解析日期,注:此处为严格模式解析,即 20151809 这样的日期会解析错误
    	   * 
    	   * @param pattern
    	   * @param date
    	   * @param locale
    	   * @return
    	   */
    	  public static Date parse(String pattern, String date, Locale locale){
    	    SimpleDateFormat format = new SimpleDateFormat(pattern, locale);
    	    format.setLenient(false);
    	    Date result = null;
    	    try{
    	      result = format.parse(date);
    	    }catch(Exception e){
    	      e.printStackTrace();
    	    }
    
    	    return result;
    	  }
    
    }
    
    

    BaseController.java:

    package net.myblog.web.controller;
    
    import javax.servlet.http.HttpServletRequest;
    
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    import org.springframework.web.servlet.ModelAndView;
    
    public class BaseController {
    	
    	/**
    	 * 得到 request 对象
    	 */
    	public HttpServletRequest getRequest() {
    		HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
    		
    		return request;
    	}
    	
    	/**
    	 * 得到 ModelAndView
    	 */
    	public ModelAndView getModelAndView(){
    		return new ModelAndView();
    	}
    
    
    }
    
    
    15 条回复    2018-12-20 06:00:30 +08:00
    cuqk
        1
    cuqk  
       2018-01-15 14:43:05 +08:00
    看到 DateUtils.formatDate()每次方法调用会生成一个 SimpleDataFormat...
    assiadamo
        2
    assiadamo  
       2018-01-15 16:44:54 +08:00
    同 1l,试试 java8 时间 api 或 joda-time
    pelloz
        3
    pelloz  
       2018-01-15 16:48:29 +08:00
    @cuqk SimpleDataFormat 线程不安全,每次使用 new 一个新的出来是正确的。
    如果是 java8 的话应该使用新的时间 API。
    wangmm
        4
    wangmm  
       2018-01-15 17:02:41 +08:00
    不错学习了
    cuqk
        5
    cuqk  
       2018-01-15 17:12:22 +08:00 via Android
    @pelloz 所以为什么不用 ThreadLocal
    pelloz
        6
    pelloz  
       2018-01-15 17:18:56 +08:00
    @cuqk 我觉得这里使用 ThreadLocal 增加了不必要的复杂度,new 一个 SimpleDataFormat 并不是一个很费的操作。
    letitbesqzr
        7
    letitbesqzr  
       2018-01-15 17:19:10 +08:00
    我来吐槽下 Spring data jpa 几点。
    Query creation from method names 这个功能简直难用,稍微条件多一点点的查询,那函数名看着真想打人。。
    用 Query 注解调用 jpql 或者 原生 sql 管理起来并没有 mybatis 方便。

    比较好用的还是 JpaSpecificationExecutor 和 JpaRepository 里的接口,非常灵活
    Charkey
        8
    Charkey  
       2018-01-15 17:20:55 +08:00
    @letitbesqzr 我是写惯了 mybatis
    letitbesqzr
        9
    letitbesqzr  
       2018-01-15 17:25:11 +08:00
    @Charkey
    我们一般对于性能要求比较高,系统逻辑不是那么复杂的用 mybatis。
    如果是关系特别复杂并且内部系统,迸发各方面不是那么多要求的会用 jpa。毕竟 jpa 在动态生成条件方面的确方便点,像很多内部系统为了开发速度,很多 sql 根本就不去后台写了,根据前台的表单名称就去自动的和 entity 对应修改了。。但互联网项目不敢这么来。。。
    cuqk
        10
    cuqk  
       2018-01-15 20:03:51 +08:00 via Android
    @pelloz 与其说是复杂度,倒不如说 ThreadLocal 比直接 new 的上手难度大一些些
    StevenTong
        11
    StevenTong  
       2018-01-15 21:02:47 +08:00
    entity 不用 lombok 看着难受
    chencn
        12
    chencn  
       2018-01-15 21:26:02 +08:00 via iPhone
    我倒是很喜欢 jpa 那个写一串又臭又长的方法名就把 sql 省了的设计
    carakan
        13
    carakan  
       2018-02-22 15:39:24 +08:00
    @letitbesqzr 为什么我觉得 Specification 也很难用啊。。。一查全字段都查出来了
    someonedeng
        14
    someonedeng  
       2018-02-27 20:57:51 +08:00
    写裸的 sql 挺难受。。
    Allianzcortex
        15
    Allianzcortex  
       2018-12-20 06:00:30 +08:00
    最近刚好在用,非感谢+收藏不足以表达对楼主的感激之情( D
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   953 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 32ms · UTC 20:39 · PVG 04:39 · LAX 12:39 · JFK 15:39
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.