SpringDataJPA學習記錄(四)--使用QueryDSL

SpringDataJPA學習記錄(四)--使用QueryDSL

標簽(空格分隔): springJPA


1.問題的提出

最近再看公司項目中有如下代碼,看了后簡直不能忍.缺點列出來的話,如下:

  1. 返回類型Object[]數組,至于每一個下標對應哪個字段,沒法直觀的看到,例如object[11]是什么類型?字段名是什么?這個就無法直觀得知.
  2. sql中復雜的關系導致不可維護,每一個接手的人都要研究sql半天
  3. 該種動態拼接條件方法導致類似的代碼會大量重復,所以IDEA打開的時候黃了半邊天.
  4. 該查詢為分頁查詢,這樣寫的話,還要再copy一個count查詢才能拿到總數,無疑又是代碼重復.
  5. JPA這種框架目的就是少些原生sql語句,大量這樣的操作的話,還不如使用dbUtil這樣的工具類查詢.
    @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();
    }

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標示為源代碼目錄即可.

1.jpg

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();
    }

和上面不同之處在于這里使用了offsetlimit限制查詢結果.并且返回一個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是不同的對象.

Paste_Image.png

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: https://github.com/nl101531/JavaWEB

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容