QueryDSL 与 JPA 的配合

7,850 阅读6分钟
原文链接: zhuanlan.zhihu.com
作者 :蜕变之路 - 简书

1.问题的提出

最近再看公司项目中有如下代码

 @Override
 public List<Object[]> findByPcardCardOrder(
         PcardCardOrder pcardCardOrder,String applyInstName2,Integer page, Integer rows) {
     StringBuffer sql = new StringBuffer(
             "SELECT p.*"
                     +",p2.vcard_make_des" 
                     +",p3.cardnum_rule_id,p3.vtype_nm"
                     +",p4.cn_card_bin,p4.cn_nm"
                     +",p5.inst_id,p5.inst_name,p5.apply_range,p5.card_name,p5.card_type,p5.bin_card_material"
                     +",p6.inst_name AS apply_inst_name "
                     +",p7.inst_name AS apply_inst_name2"
                     + ",p8.inst_name as receive_inst_name"
                     + " FROM "
                     +" tbl_pcard_card_order p LEFT JOIN tbl_pcard_vcard_make p2 ON p.make_id = p2.vcard_make_id"
                  +" LEFT JOIN  tbl_pcard_vtype p3 ON p2.vcard_make_vtype_id=p3.vtype_id"
                  +" LEFT JOIN  tbl_pcard_cardnum_rule p4 ON p3.cardnum_rule_id=p4.cn_id"
                  +" LEFT JOIN  tbl_pcard_cardbin p5 ON p4.cn_card_bin=p5.card_bin"
                  +" LEFT JOIN  tbl_pcard_institution p6 ON p5.apply_range=p6.inst_id"
                  +" LEFT JOIN  tbl_pcard_institution p7 ON p.apply_inst_id=p7.inst_id"
                  +" LEFT JOIN  tbl_pcard_institution p8 ON p.receive_inst=p8.inst_id"
                  +" WHERE 1=1 ");
     int i = 1;
     Map<String, Object> map = new HashMap<String, Object>();

     if (!StringUtils.isEmpty(pcardCardOrder.getCordId())) {
         sql.append(" and p.cord_id=");
         sql.append("?" + i);
         map.put(i + "", pcardCardOrder.getCordId());
         i++;
     }
     if (!StringUtils.isEmpty(pcardCardOrder.getAppointMchtcard())) {
         sql.append(" and p.appoint_mchtcard=");
         sql.append("?" + i);
         map.put(i + "", pcardCardOrder.getAppointMchtcard());
         i++;
     }

     if (!StringUtils.isEmpty(pcardCardOrder.getMakeId())) {
         sql.append(" and p.make_id like ");
         sql.append("?" + i);
         map.put(i + "","%%"+ pcardCardOrder.getMakeId()+"%%");
         i++;
     }

     if (!StringUtils.isEmpty(applyInstName2)) {
         sql.append(" and p7.inst_name like ");
         sql.append("?"+i);
         map.put(i+"","%%"+applyInstName2+"%%");
         i++;
     }

     sql.append(" order by p.ct_dm desc");
     Query query = entityManager.createNativeQuery(sql.toString());
     for (String key : map.keySet()) {
         query.setParameter(key, map.get(key));
     }
     if (page != null && rows != null) {
         query.setFirstResult(rows * (page - 1));
         query.setMaxResults(rows);
     }
     return query.getResultList();
 }

看了后简直不能忍.缺点列出来的话,如下:

  1. 返回类型Object[]数组,至于每一个下标对应哪个字段,没法直观的看到,例如object[11]是什么类型?字段名是什么?这个就无法直观得知.
  2. sql中复杂的关系导致不可维护,每一个接手的人都要研究sql半天
  3. 该种动态拼接条件方法导致类似的代码会大量重复,所以IDEA打开的时候黄了半边天.
  4. 该查询为分页查询,这样写的话,还要再copy一个count查询才能拿到总数,无疑又是代码重复.
  5. JPA这种框架目的就是少些原生sql语句,大量这样的操作的话,还不如使用dbUtil这样的工具类查询.

2.学习QueryDSL

queryDSL就可以避免上面全部的问题,在解决问题之前先学习如何使用.

2.1 QueryDSL简介

  1. QueryDSL仅仅是一个通用的查询框架,专注于通过Java API构建类型安全的SQL查询。
  2. Querydsl可以通过一组通用的查询API为用户构建出适合不同类型ORM框架或者是SQL的查询语句,也就是说QueryDSL是基于各种ORM框架以及SQL之上的一个通用的查询框架。
  3. 借助QueryDSL可以在任何支持的ORM框架或者SQL平台上以一种通用的API方式来构建查询。目前QueryDSL支持的平台包括JPA,JDO,SQL,Java Collections,RDF,Lucene,Hibernate Search。
  4. 官网地址:点击进入

2.2配置到项目

首先对于queryDSL有两个版本,com.mysema.querydslcom.querydsl,前者是3.X系列后者是4.X系列,这里使用的是后者.

第一步:Maven引入依赖

 <!--query dsl-->
    <dependency>
      <groupId>com.querydsl</groupId>
      <artifactId>querydsl-jpa</artifactId>
      <version>${querydsl.version}</version>
    </dependency>
    <dependency>
      <groupId>com.querydsl</groupId>
      <artifactId>querydsl-apt</artifactId>
      <version>${querydsl.version}</version>
      <scope>provided</scope>
    </dependency>
    <!--query dsl end-->

第二步:加入插件,用于生成查询实例

<!--该插件可以生成querysdl需要的查询对象,执行mvn compile即可-->
      <plugin>
        <groupId>com.mysema.maven</groupId>
        <artifactId>apt-maven-plugin</artifactId>
        <version>1.1.3</version>
        <executions>
          <execution>
            <goals>
              <goal>process</goal>
            </goals>
            <configuration>
              <outputDirectory>target/generated-sources/java</outputDirectory>
              <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
            </configuration>
          </execution>
        </executions>
      </plugin>


执行mvn compile之后,可以找到该target/generated-sources/java,然后IDEA标示为源代码目录即可.

2.3实体类

城市类:

@Entity
@Table(name = "t_city", schema = "test", catalog = "")
public class TCity {
    //省略JPA注解标识
    private int id;
    private String name;
    private String state;
    private String country;
    private String map;
}

旅馆类:

@Entity
@Table(name = "t_hotel", schema = "test", catalog = "")
public class THotel {
    //省略JPA注解标识
    private int id;
    private String name;
    private String address;
    private Integer city;//保存着城市的id主键
}

2.4 单表动态分页查询

Spring Data JPA中提供了QueryDslPredicateExecutor接口,用于支持QueryDSL的查询操作,这样的话单表动态查询就可以参考如下代码:

//查找出Id小于3,并且名称带有`shanghai`的记录.

        //动态条件
        QTCity qtCity = QTCity.tCity;
        //该Predicate为querydsl下的类,支持嵌套组装复杂查询条件
        Predicate predicate = qtCity.id.longValue().lt(3)
                                       .and(qtCity.name.like("shanghai"));
        //分页排序
        Sort sort = new Sort(new Sort.Order(Sort.Direction.ASC,"id"));
        PageRequest pageRequest = new PageRequest(0,10,sort);
        //查找结果
        Page<TCity> tCityPage = tCityRepository.findAll(predicate,pageRequest);

2.5多表动态查询

QueryDSL对多表查询提供了一个很好地封装,看下面代码:

    /**
     * 关联查询示例,查询出城市和对应的旅店
     * @param predicate 查询条件
     * @return 查询实体
     */
    @Override
    public List<Tuple> findCityAndHotel(Predicate predicate) {
        JPAQueryFactory queryFactory = new JPAQueryFactory(em);
        JPAQuery<Tuple> jpaQuery = queryFactory.select(QTCity.tCity,QTHotel.tHotel)
                                        .from(QTCity.tCity)
                                        .leftJoin(QTHotel.tHotel)
                                        .on(QTHotel.tHotel.city.longValue().eq(QTCity.tCity.id.longValue()));
        //添加查询条件
        jpaQuery.where(predicate);
        //拿到结果
        return jpaQuery.fetch();
    }

城市表左连接旅店表,当该旅店属于这个城市时查询出两者的详细字段,存放到一个Tuple的多元组中.相比原生sql,简单清晰了很多.
那么该怎么调用这个方法呢?

 @Test
    public void findByLeftJoin(){
        QTCity qtCity = QTCity.tCity;
        QTHotel qtHotel = QTHotel.tHotel;
        //查询条件
        Predicate predicate = qtCity.name.like("shanghai");
        //调用
        List<Tuple> result = tCityRepository.findCityAndHotel(predicate);
        //对多元组取出数据,这个和select时的数据相匹配
        for (Tuple row : result) {
            System.out.println("qtCity:"+row.get(qtCity));
            System.out.println("qtHotel:"+row.get(qtHotel));
            System.out.println("--------------------");
        }
        System.out.println(result);
    }

这样做的话避免了返回Object[]数组,下面是自动生成的sql语句:

select
        tcity0_.id as id1_0_0_,
        thotel1_.id as id1_1_1_,
        tcity0_.country as country2_0_0_,
        tcity0_.map as map3_0_0_,
        tcity0_.name as name4_0_0_,
        tcity0_.state as state5_0_0_,
        thotel1_.address as address2_1_1_,
        thotel1_.city as city3_1_1_,
        thotel1_.name as name4_1_1_ 
    from
        t_city tcity0_ 
    left outer join
        t_hotel thotel1_ 
            on (
                cast(thotel1_.city as signed)=cast(tcity0_.id as signed)
            ) 
    where
        tcity0_.name like ? escape '!'

2.6 多表动态分页查询

分页查询对于queryDSL无论什么样的sql只需要写一遍,会自动转换为相应的count查询,也就避免了文章开始的问题4,下面代码是对上面的查询加上分页功能:

     @Override
    public QueryResults<Tuple> findCityAndHotelPage(Predicate predicate,Pageable pageable) {
        JPAQueryFactory queryFactory = new JPAQueryFactory(em);
        JPAQuery<Tuple> jpaQuery = queryFactory.select(QTCity.tCity.id,QTHotel.tHotel)
                                           .from(QTCity.tCity)
                                           .leftJoin(QTHotel.tHotel)
                                               .on(QTHotel.tHotel.city.longValue().eq(QTCity.tCity.id.longValue()))
                                           .where(predicate)
                                           .offset(pageable.getOffset())
                                               .limit(pageable.getPageSize());
        //拿到分页结果
        return jpaQuery.fetchResults();
    }

和上面不同之处在于这里使用了offset和limit限制查询结果.并且返回一个QueryResults,该类会自动实现count查询和结果查询,并进行封装.
调用形式如下:

  @Test
    public void findByLeftJoinPage(){
        QTCity qtCity = QTCity.tCity;
        QTHotel qtHotel = QTHotel.tHotel;
        //条件
        Predicate predicate = qtCity.name.like("shanghai");
        //分页
        PageRequest pageRequest = new PageRequest(0,10);
        //调用查询
        QueryResults<Tuple> result = tCityRepository.findCityAndHotelPage(predicate,pageRequest);
        //结果取出
        for (Tuple row : result.getResults()) {
            System.out.println("qtCity:"+row.get(qtCity));
            System.out.println("qtHotel:"+row.get(qtHotel));
            System.out.println("--------------------");
        }
        //取出count查询总数
        System.out.println(result.getTotal());
    }

生成的原生count查询sql,当该count查询结果为0的话,则直接返回,并不会再进行具体数据查询:

   select
        count(tcity0_.id) as col_0_0_ 
    from
        t_city tcity0_ 
    left outer join
        t_hotel thotel1_ 
            on (
                cast(thotel1_.city as signed)=cast(tcity0_.id as signed)
            ) 
    where
        tcity0_.name like ? escape '!'

生成的原生查询sql:

  select
        tcity0_.id as id1_0_0_,
        thotel1_.id as id1_1_1_,
        tcity0_.country as country2_0_0_,
        tcity0_.map as map3_0_0_,
        tcity0_.name as name4_0_0_,
        tcity0_.state as state5_0_0_,
        thotel1_.address as address2_1_1_,
        thotel1_.city as city3_1_1_,
        thotel1_.name as name4_1_1_ 
    from
        t_city tcity0_ 
    left outer join
        t_hotel thotel1_ 
            on (
                cast(thotel1_.city as signed)=cast(tcity0_.id as signed)
            ) 
    where
        tcity0_.name like ? escape '!' limit ?

查看打印,可以发现对应的city也都是同一个对象,hotel是不同的对象.

3.改造

有了上面的经验,改造就变得相当容易了.
首先前面的一堆sql可以写成如下形式,无非是多了一些select和left join,其中$是一个工具类,存储着实体查询类的引用.

JPAQueryFactory factory = new JPAQueryFactory(entityManager);
        factory.select($.pcardCardOrder)
               .select($.pcardVcardMake.vcardMakeDes)
               .select($.pcardVtype.cardnumRuleId,$.pcardVtype.vtypeNm)
               .select($.pcardCardbin)
               .leftJoin($.pcardVcardMake).on($.pcardCardOrder.makeId.eq($.pcardVcardMake.vcardMakeId))
               //......省略

查询条件使用Predicate代替,放在service拼接,或者写一个生产条件的工厂都可以.

 jpaQuery.where(predicate);

最后的分页处理就和之前的一样了

        jpaQuery.offset(pageable.getOffset())
                .limit(pageable.getPageSize());
        return jpaQuery.fetchResults();

个人感觉Query DSL和Spring Data JPA是绝配.更多请参考Demo代码:

github: nl101531/JavaWEB