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