Spring-data-jpa 查询

作者: 东方一号蓝 | 来源:发表于2017-07-18 15:38 被阅读436次

    原创性声明:本文完全为笔者原创,请尊重笔者劳动力。转载务必注明原文地址。

    背景

    要认识spring-data-jpa,就要和jpa(Java Persistence API:java 持久层API) 区分开来。这是两个不同的东西。jpasun公司官方的ORM框架,严格上说,是java ORM的一种规范,从jpa的组成基本都是一些接口类就可见一斑,而具体的实现更多的由第三方ORM框架完成,像Hibernate、TopLink、JDO等。因此可以说,jpa是一种规范标准,hibernate是具体的实现。sun公司之所以这样干,也是为了统一。

    Spring-data-jpa又是什么呢?在企业级java EE开发中,Spring的地位已经难以撼动,它几乎无所不能。而它的强大更多的体现在与第三方框架的整合上,对于持久化这一块,Spring不仅仅可以很好的整合hibernate,它自己也开了'自营业务',于是就有了Spring-data-**等各种用于持久化操作的包,包括Spring-data-jpa、Spring-data-mongodb、Spring-data-template等。

    而此文就是记录其中的Spring-data-jpa

    在使用持久化工具的时候,一般都有一个对象来操作数据库,在原生的Hibernate中叫做Session,在JPA中叫做EntityManager,在MyBatis中叫做SqlSession,通过这个对象来操作数据库。我们一般按照三层结构来看的话,Service层做业务逻辑处理,Dao层和数据库打交道,在Dao中,就存在着上面的对象。那么ORM框架本身提供的功能有什么呢?答案是基本的CRUD,所有的基础CRUD框架都提供,我们使用起来感觉很方便,很给力,业务逻辑层面的处理ORM是没有提供的,如果使用原生的框架,业务逻辑代码我们一般会自定义,会自己去写SQL语句,然后执行。在这个时候,Spring-data-jpa的威力就体现出来了,ORM提供的能力他都提供,ORM框架没有提供的业务逻辑功能Spring-data-jpa也提供,全方位的解决用户的需求。

    开始
    1. 引入(以maven为例)
    <!-- https://mvnrepository.com/artifact/org.springframework.data/spring-data-jpa -->
    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-jpa</artifactId>
        <version>2.0.0.M4</version>
    </dependency>
    

    如果使用的是Spring boot,那么可以这样引用:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    

    2.例如,我们有一个实体类Article,如下:

    @Entity
    @Table(name = "articles")
    public class Article extends AbstractAuditingEntity {
    
      @Id
      @Column(name = "id", unique = true, nullable = false)
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Long id;
        
      @Column(name = "title", nullable = false)
      private String title;
        
      @Column(name = "content", nullable = false)
      private String content;
    
      @Column(name = "if_set_top")
      @ColumnComment("是否置顶")
      private boolean ifSetTop = false; //默认false  
      
      // 省略get/set 方法...
     }
    

    3.我们需要创建一个接口去继承Spring-data-jpa提供的接口:

    public interface ArticleRepository extends JpaRepository<Article, Long> {
    
    }
    

    通常的,ArticleRepository的命名遵循约定俗称,其中指定的Article表明了映射的对象,Long指的是Article实体类的主键id类型。如此,就可以在这个接口类中,声明一些特定的方法,以满足简单的基本CRUD。

    基本查询

    参见一张从网上down下来的截图:

    spring-data-jpa简单查询.png

    参见这张截图,可以很简单的写出针对Article的一些查询,只需要在上面的ArticleRepository中定义这些方法即可,而不需要任何的实现,这就是Spring-data-jpa的强大之一。

    注意:1. 接口方法名的命名要严格遵循驼峰法则;2.有些方法在JpaRepository中就已经声明可以直接调用,例如findAll, findOne,save, saveAndFlush等。

    复杂查询

    Spring-data-jpa的复杂查询体现在强大的动态查询和分页查询上。

    4.我们需要让ArticleRepository接口再继承一个接口: JpaSpecificationExecutor<T>。如下:

    public interface ArticleRepository extends JpaRepository<Article, Long>, 
                                               JpaSpecificationExecutor<Article>  {
    
    }
    

    进入JpaSpecificationExecutor接口可以发现,一些用于动态查询和分页查询的方法:

    T findOne(Specification<T> spec);
    
    List<T> findAll(Specification<T> spec);
    
    Page<T> findAll(Specification<T> spec, Pageable pageable);
    
    List<T> findAll(Specification<T> spec, Sort sort);
    
    long count(Specification<T> spec);
    

    5.接下来,我们就可以在ArticleService中这样去调用ArticleRepository的方法了:

    @Service
    public class ArticleService {
      
      /**
       * 过滤文章,传入filter,和一个分页对象,filter可以匹配id和title。返回分页查询后的结果
       */
      public Page<Article> filterArticleByIdOrTitle(String filter, Pageable pageable) {
        return articleRepository.findAll(new Specification<Article>() {
          @Override
          public Predicate toPredicate(Root<Student> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
            Predicate predicate = null;
            if (filter != null && !filter.trim().equal("")) {
              if (StringUtils.isNumeric(filter)) {
                predicate  = cb.equal(root.<Long> get("id"), "%" + filter + "%");
              } else {
                predicate = cb.like(root.<String>get("title")), "%" + filter + "%");
              }
            }
            return predicate;        
          }
        }, new PageRequest(pageable.getPageNumber(), pageable.getPageSize(), pageable.getSort()));
      }
    }
    

    观察上面的代码,articleRepository.findAll(Specification spec, Pageable pageable),方法传入了两个接口作为参数。我们在new了这两个参数之后,进一步重载了toPredicate方法,这个方法是实现过滤查询的核心,它相当于在拼接最终复杂查询的sql,因此,其中可以去实现更为复杂的查询方法,包括但不限于: 排序、 分组、 关联其他表等

    predicate译文就是"断言"的意思,在sql中代指 where后面的内容。

    事实上,我们可以创建一个ArticleSpecification类,来继承Specification接口,从而实现toPredicate方法。这样的做法更加的优雅。

    6.创建ArticleSpecification类,用以继承Specification,并实现toPredicate方法。

    public class ArticleSpecification extends AbstractSpecification<Article> {
      
      private final ArticleCriteria criteria; // 动态查询的条件包装类
      
      public ArticleSpecification(ArticleCriteria criteria) { // 构造方法,用以传入criteria
        this.criteria = criteria;
      }
    
      @Override
      public Predicate toPredicate(Root<Article> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
        // 实现toPredicate
        Predicate predicate = cb.conjunction();
        List<Expression<Boolean>> expressions =  predicate.getExpressions();
        // 快速搜索: 匹配id或title
        if (StringUtils.isNotBlank(criteria.getFilter)) {
          if (StringUtils.isNumeric(filter)) {
             expressions.add(cb.equal(root.<Long> get("id"), "%" + criteria.getFilter()+ "%"));
          } else {
             expressions.add(cb.like(root.<String>get("title")), "%" + criteria.getFilter() + "%"));
          }
        }
        
        // 高级搜索: 精确匹配id
        if (criteria.getId() != null) {
          expressions.add(cb.equal(root.<Long> get("id"), "%" + criteria.getFilter()+ "%"));
        }
        // 高级搜索: 模糊匹配title
        if (StringUtils.isNotBlank(criteria.getTitle())) {
          expressions.add(cb.like(root.<String>get("title")), "%" + criteria.getFilter() + "%"));
        }
        return predicate;   
      }
    }
    

    上面这个类中,我们定义了一个属性ArticleCriteria 对象,它是一个条件包装类。如下:

    public class ArticleCriteria { 
      private Long id;
      private String title;
      private String filter;
      private Boolean ifSetTop;
      // 省略get/set方法
    }
    

    这样在ArticleService中,我们就可以这样去修改:

    @Service
    public class ArticleService {
    
      /**
       * 过滤文章,传入filter,和一个分页对象,filter可以匹配id和title。返回分页查询后的结果
       */
      public Page<Article> filterArticleByIdOrTitle(ArticleCriteria criteria, Pageable pageable) {
        return articleRepository.findAll(new ArticleSpecification(criteria), pageable);
      }
    }
    

    到这里,后端基本完成了。前端,我们就可以在页面视图上提供两种查询:简单查询高级查询
    简单查询下,只需提供一个 filter对应的输入框,而高级查询下,提供两个输入框,一个对应id,一个对应title。

    7.在客户端,我们只需要构造这样的POST请求即可(以angular为例):

    $scope.query = { // 搜索的条件
        filter: '',
        page: 1,
        size: 10
    }
    $http.post("api/article", {
      filter: $scope.query.filter,
      page: $scope.query.page - 1,
      size: $scope.query.size
    }).success(function(){
      // todo
    })
    

    至于数据如何与视图绑定,这里就不讲了。

    angular页面分页的组件可以使用angular materialangular-material-data-table

    补充

    如果我们查询的结果进行排序,例如根据id倒序,但是希望ifSetTop为true的排在前面。可以在toPredicate方法中这样去构造Predicate

    public class ArticleSpecification extends AbstractSpecification<Article> {
      
      private final ArticleCriteria criteria; // 动态查询的条件包装类
      
      public ArticleSpecification(ArticleCriteria criteria) { // 构造方法,用以传入criteria
        this.criteria = criteria;
      }
    
      @Override
      public Predicate toPredicate(Root<Article> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
        // 实现toPredicate
        Predicate predicate = cb.conjunction();
        List<Expression<Boolean>> expressions =  predicate.getExpressions();
        // 快速搜索: 匹配id或title
        if (StringUtils.isNotBlank(criteria.getFilter)) {
          if (StringUtils.isNumeric(filter)) {
             expressions.add(cb.equal(root.<Long> get("id"), "%" + criteria.getFilter()+ "%"));
          } else {
             expressions.add(cb.like(root.<String>get("title")), "%" + criteria.getFilter() + "%"));
          }
        }
        
        // 高级搜索: 精确匹配id
        if (criteria.getId() != null) {
          expressions.add(cb.equal(root.<Long> get("id"), "%" + criteria.getFilter()+ "%"));
        }
        // 高级搜索: 模糊匹配title
        if (StringUtils.isNotBlank(criteria.getTitle())) {
          expressions.add(cb.like(root.<String>get("title")), "%" + criteria.getFilter() + "%"));
        }
        
        // 利用query去排序(也可以做更复杂的逻辑处理)
        query.where(predicate);
        List<Order> orders = new ArrayList<>();
        orders.add(cb.desc(root.get("ifSetTop")));
        orders.add(cb.desc(root.get("id")));
        query.orderBy(orders);
    
        return query.getRestriction();
      }
    }
    

    涉及多表的复杂查询,例如,Article关联了另一个实体Book,需要根据BookId,过滤Article,或者过滤出不属于任何一本书的Article。则需要进一步熟练的在toPredicate方法中,构造predicate

    在上面的代码中,例如:

    root.get("id")
    

    也可以通过JPA的原模型对象来访问:

    root.get(Article_.id)
    

    前提是,在这之前,需要引入pom依赖:

    <!-- hibernate元模型生成器:生成jpa高级查询的 Domain_ 类 -->
    <dependency>
      <groupId>org.hibernate</groupId>
       <artifactId>hibernate-jpamodelgen</artifactId>
      <optional>true</optional>
    </dependency>
    

    再用maven重新编译即可生成Entity_这样的原模型对象。

    关于spring-data-jpa的更多内容,推荐这篇博客

    相关文章

      网友评论

      本文标题:Spring-data-jpa 查询

      本文链接:https://www.haomeiwen.com/subject/vyebhxtx.html