Spring-data-jpa 查询

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

背景

要认识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的更多内容,推荐这篇博客

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容