SpringDataJPA學習記錄(四)--使用QueryDSL
標簽(空格分隔): springJPA
1.問題的提出
最近再看公司項目中有如下代碼,看了后簡直不能忍.缺點列出來的話,如下:
- 返回類型Object[]數組,至于每一個下標對應哪個字段,沒法直觀的看到,例如object[11]是什么類型?字段名是什么?這個就無法直觀得知.
- sql中復雜的關系導致不可維護,每一個接手的人都要研究sql半天
- 該種動態拼接條件方法導致類似的代碼會大量重復,所以IDEA打開的時候黃了半邊天.
- 該查詢為分頁查詢,這樣寫的話,還要再copy一個count查詢才能拿到總數,無疑又是代碼重復.
- 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簡介
- QueryDSL僅僅是一個通用的查詢框架,專注于通過Java API構建類型安全的SQL查詢。
- Querydsl可以通過一組通用的查詢API為用戶構建出適合不同類型ORM框架或者是SQL的查詢語句,也就是說QueryDSL是基于各種ORM框架以及SQL之上的一個通用的查詢框架。
- 借助QueryDSL可以在任何支持的ORM框架或者SQL平臺上以一種通用的API方式來構建查詢。目前QueryDSL支持的平臺包括JPA,JDO,SQL,Java Collections,RDF,Lucene,Hibernate Search。
- 官網地址:點擊進入
2.2配置到項目
首先對于queryDSL有兩個版本,com.mysema.querydsl
和com.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代碼: