在我們實際項目開發中,往往會遇到一種多表關聯查詢并且僅需要返回多表內的幾個字段最后組合成一個集合或者實體。這種情況在傳統的查詢中我們無法控制查詢的字段,只能全部查詢出后再做出分離,這種也是我們最不愿意看到的處理方式,這種方式會產生繁瑣、復雜、效率低、代碼閱讀性差等等問題。QueryDSL為我們提供了一個返回自定義對象的工具類型,而Java8新特性Collection中stream方法也能夠完成返回自定義對象的邏輯,下面我們就來看下這兩種方式如何編寫?
本章目標
基于SpringBoot平臺完成SpringDataJPA與QueryDSL整合查詢返回自定義對象的兩種方式。
構建項目
我們先來使用idea工具創建一個SpringBoot項目,預先添加相對應的依賴,pom.xml配置文件內容如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.yuqiyu.querydsl.sample</groupId>
<artifactId>chapter5</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>chapter5</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--阿里巴巴數據庫連接池,專為監控而生 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.26</version>
</dependency>
<!-- 阿里巴巴fastjson,解析json視圖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.15</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<!--<scope>provided</scope>-->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--queryDSL-->
<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>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.16</version>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!--添加QueryDSL插件支持-->
<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>
</plugins>
</build>
</project>
上面內的QueryDSL這里就不多做講解了,如有疑問請查看第一章:Maven環境下如何配置QueryDSL環境。
下面我們需要創建兩張表來完成本章的內容。
創建表結構
跟上一章一樣,我們還是使用商品信息表、商品類型表來完成編碼。
商品信息表
-- ----------------------------
-- Table structure for good_infos
-- ----------------------------
DROP TABLE IF EXISTS `good_infos`;
CREATE TABLE `good_infos` (
`tg_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵自增',
`tg_title` varchar(50) CHARACTER SET utf8 DEFAULT NULL COMMENT '商品標題',
`tg_price` decimal(8,2) DEFAULT NULL COMMENT '商品單價',
`tg_unit` varchar(20) CHARACTER SET utf8 DEFAULT NULL COMMENT '單位',
`tg_order` varchar(255) DEFAULT NULL COMMENT '排序',
`tg_type_id` int(11) DEFAULT NULL COMMENT '類型外鍵編號',
PRIMARY KEY (`tg_id`),
KEY `tg_type_id` (`tg_type_id`)
) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=latin1;
-- ----------------------------
-- Records of good_infos
-- ----------------------------
INSERT INTO `good_infos` VALUES ('1', '金針菇', '5.50', '斤', '1', '3');
INSERT INTO `good_infos` VALUES ('2', '油菜', '12.60', '斤', '2', '1');
商品類型信息表
-- ----------------------------
-- Table structure for good_types
-- ----------------------------
DROP TABLE IF EXISTS `good_types`;
CREATE TABLE `good_types` (
`tgt_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵自增',
`tgt_name` varchar(30) CHARACTER SET utf8 DEFAULT NULL COMMENT '類型名稱',
`tgt_is_show` char(1) DEFAULT NULL COMMENT '是否顯示',
`tgt_order` int(2) DEFAULT NULL COMMENT '類型排序',
PRIMARY KEY (`tgt_id`)
) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=latin1;
-- ----------------------------
-- Records of good_types
-- ----------------------------
INSERT INTO `good_types` VALUES ('1', '綠色蔬菜', '1', '1');
INSERT INTO `good_types` VALUES ('2', '根莖類', '1', '2');
INSERT INTO `good_types` VALUES ('3', '菌類', '1', '3');
創建實體
我們對應表結構創建實體并且添加對應的SpringDataJPA注解。
商品實體
package com.yuqiyu.querydsl.sample.chapter5.bean;
import lombok.Data;
import javax.persistence.*;
import java.io.Serializable;
/**
* 商品基本信息實體
* ========================
* Created with IntelliJ IDEA.
* User:恒宇少年
* Date:2017/7/10
* Time:22:39
* 碼云:http://git.oschina.net/jnyqy
* ========================
*/
@Entity
@Table(name = "good_infos")
@Data
public class GoodInfoBean
implements Serializable
{
//主鍵
@Id
@Column(name = "tg_id")
@GeneratedValue
private Long id;
//標題
@Column(name = "tg_title")
private String title;
//價格
@Column(name = "tg_price")
private double price;
//單位
@Column(name = "tg_unit")
private String unit;
//排序
@Column(name = "tg_order")
private int order;
//類型編號
@Column(name = "tg_type_id")
private Long typeId;
}
商品類型實體
package com.yuqiyu.querydsl.sample.chapter5.bean;
import lombok.Data;
import javax.persistence.*;
import java.io.Serializable;
/**
* 商品類別實體
* ========================
* Created with IntelliJ IDEA.
* User:恒宇少年
* Date:2017/7/10
* Time:22:39
* 碼云:http://git.oschina.net/jnyqy
* ========================
*/
@Entity
@Table(name = "good_types")
@Data
public class GoodTypeBean
implements Serializable
{
//類型編號
@Id
@GeneratedValue
@Column(name = "tgt_id")
private Long id;
//類型名稱
@Column(name = "tgt_name")
private String name;
//是否顯示
@Column(name = "tgt_is_show")
private int isShow;
//排序
@Column(name = "tgt_order")
private int order;
}
上面實體內的注解@Entity標識該實體被SpringDataJPA所管理,@Table標識該實體對應的數據庫內的表信息,@Data該注解則是lombok內的合并注解,根據idea工具的插件自動添加getter/setter、toString、全參構造函數等。
創建DTO
我們創建一個查詢返回的自定義對象,對象內的字段包含了商品實體、商品類型實體內的部分內容,DTO代碼如下所示:
package com.yuqiyu.querydsl.sample.chapter5.dto;
import lombok.Data;
import java.io.Serializable;
/**
* 商品dto
* ========================
* Created with IntelliJ IDEA.
* User:恒宇少年
* Date:2017/7/10
* Time:22:39
* 碼云:http://git.oschina.net/jnyqy
* ========================
*/
@Data
public class GoodDTO
implements Serializable
{
//主鍵
private Long id;
//標題
private String title;
//單位
private String unit;
//價格
private double price;
//類型名稱
private String typeName;
//類型編號
private Long typeId;
}
要注意我們的自定義返回的對象僅僅只是一個實體,并不對應數據庫內的表,所以這里不需要配置@Entity、@Table等JPA注解,僅把@Data注解配置上就可以了,接下來我們編譯下項目讓QueryDSL插件自動生成查詢實體。
生成查詢實體
idea工具為maven project自動添加了對應的功能,我們打開右側的Maven Projects,如下圖1所示:
我們雙擊compile命令執行,執行完成后會在我們pom.xml配置文件內配置生成目錄內生成對應實體的QueryDSL查詢實體。生成的查詢實體如下圖2所示:
QueryDSL配置JPA插件僅會根據@Entity進行生成查詢實體
創建控制器
我們來創建一個測試的控制器讀取商品表內的所有商品,在編寫具體的查詢方法之前我們需要實例化EntityManager對象以及JPAQueryFactory對象,并且通過實例化控制器時就去實例化JPAQueryFactory對象。控制器代碼如下所示:
package com.yuqiyu.querydsl.sample.chapter5.controller;
import com.querydsl.core.types.Projections;
import com.querydsl.jpa.impl.JPAQueryFactory;
import com.yuqiyu.querydsl.sample.chapter5.bean.QGoodInfoBean;
import com.yuqiyu.querydsl.sample.chapter5.bean.QGoodTypeBean;
import com.yuqiyu.querydsl.sample.chapter5.dto.GoodDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.PostConstruct;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.stream.Collectors;
/**
* 多表查詢返回商品dto控制器
* ========================
* Created with IntelliJ IDEA.
* User:恒宇少年
* Date:2017/7/10
* Time:23:04
* 碼云:http://git.oschina.net/jnyqy
* ========================
*/
@RestController
public class GoodController
{
//實體管理
@Autowired
private EntityManager entityManager;
//查詢工廠
private JPAQueryFactory queryFactory;
//初始化查詢工廠
@PostConstruct
public void init()
{
queryFactory = new JPAQueryFactory(entityManager);
}
}
可以看到我們配置的是一個@RestController該控制器返回的數據都是Json字符串,這也是RestController所遵循的規則。
QueryDSL & Projections
下面我們開始編寫完全基于QueryDSL形式的返回自定義對象方法,代碼如下所示:
/**
* 根據QueryDSL查詢
* @return
*/
@RequestMapping(value = "/selectWithQueryDSL")
public List<GoodDTO> selectWithQueryDSL()
{
//商品基本信息
QGoodInfoBean _Q_good = QGoodInfoBean.goodInfoBean;
//商品類型
QGoodTypeBean _Q_good_type = QGoodTypeBean.goodTypeBean;
return queryFactory
.select(
Projections.bean(
GoodDTO.class,//返回自定義實體的類型
_Q_good.id,
_Q_good.price,
_Q_good.title,
_Q_good.unit,
_Q_good_type.name.as("typeName"),//使用別名對應dto內的typeName
_Q_good_type.id.as("typeId")//使用別名對應dto內的typeId
)
)
.from(_Q_good,_Q_good_type)//構建兩表笛卡爾集
.where(_Q_good.typeId.eq(_Q_good_type.id))//關聯兩表
.orderBy(_Q_good.order.desc())//倒序
.fetch();
}
我們可以看到上面selectWithQueryDSL()查詢方法,里面出現了一個新的類型Projections,這個類型是QueryDSL內置針對處理自定義返回結果集的解決方案,里面包含了構造函數、實體、字段等處理方法,我們今天主要講解下實體。
JPAQueryFactory工廠select方法可以將Projections方法返回的QBean作為參數,我們通過Projections的bean方法來構建返回的結果集映射到實體內,有點像Mybatis內的ResultMap的形式,不過內部處理機制肯定是有著巨大差別的!bean方法第一個參數需要傳遞一個實體的泛型類型作為返回集合內的單個對象類型,如果QueryDSL查詢實體內的字段與DTO實體的字段名字不一樣時,我們就可以采用as方法來處理,為查詢的結果集指定的字段添加別名,這樣就會自動映射到DTO實體內。
運行測試
下面我們來運行下項目,訪問地址:http://127.0.0.1:8080/selectWithQueryDSL查看界面輸出的效果如下代碼塊所示:
[
{
"id": 2,
"title": "油菜",
"unit": "斤",
"price": 12.6,
"typeName": "綠色蔬菜",
"typeId": 1
},
{
"id": 1,
"title": "金針菇",
"unit": "斤",
"price": 5.5,
"typeName": "菌類",
"typeId": 3
}
]
我們可以看到輸出的Json數組字符串就是我們DTO內的所有字段反序列后的效果,DTO實體內對應的typeName、typeId都已經查詢出并且賦值。
下面我們來查看控制臺輸出自動生成的SQL,如下代碼塊所示:
Hibernate:
select
goodinfobe0_.tg_id as col_0_0_,
goodinfobe0_.tg_price as col_1_0_,
goodinfobe0_.tg_title as col_2_0_,
goodinfobe0_.tg_unit as col_3_0_,
goodtypebe1_.tgt_name as col_4_0_,
goodtypebe1_.tgt_id as col_5_0_
from
good_infos goodinfobe0_ cross
join
good_types goodtypebe1_
where
goodinfobe0_.tg_type_id=goodtypebe1_.tgt_id
order by
goodinfobe0_.tg_order desc
生成的SQL是cross join形式關聯查詢,關聯 形式通過where goodinfobe0_.tg_type_id=goodtypebe1_.tgt_id 代替了on goodinfobe0_.tg_type_id=goodtypebe1_.tgt_id,最終查詢結果集返回數據這兩種方式一致。
QueryDSL & Collection
下面我們采用java8新特性返回自定義結果集,我們查詢仍然采用QueryDSL形式,方法代碼如下所示:
/**
* 使用java8新特性Collection內stream方法轉換dto
* @return
*/
@RequestMapping(value = "/selectWithStream")
public List<GoodDTO> selectWithStream()
{
//商品基本信息
QGoodInfoBean _Q_good = QGoodInfoBean.goodInfoBean;
//商品類型
QGoodTypeBean _Q_good_type = QGoodTypeBean.goodTypeBean;
return queryFactory
.select(
_Q_good.id,
_Q_good.price,
_Q_good.title,
_Q_good.unit,
_Q_good_type.name,
_Q_good_type.id
)
.from(_Q_good,_Q_good_type)//構建兩表笛卡爾集
.where(_Q_good.typeId.eq(_Q_good_type.id))//關聯兩表
.orderBy(_Q_good.order.desc())//倒序
.fetch()
.stream()
//轉換集合內的數據
.map(tuple -> {
//創建商品dto
GoodDTO dto = new GoodDTO();
//設置商品編號
dto.setId(tuple.get(_Q_good.id));
//設置商品價格
dto.setPrice(tuple.get(_Q_good.price));
//設置商品標題
dto.setTitle(tuple.get(_Q_good.title));
//設置單位
dto.setUnit(tuple.get(_Q_good.unit));
//設置類型編號
dto.setTypeId(tuple.get(_Q_good_type.id));
//設置類型名稱
dto.setTypeName(tuple.get(_Q_good_type.name));
//返回本次構建的dto
return dto;
})
//返回集合并且轉換為List<GoodDTO>
.collect(Collectors.toList());
}
從方法開始到fetch()結束完全跟QueryDSL沒有任何區別,采用了最原始的方式進行返回結果集,但是從fetch()獲取到結果集后我們處理的方式就有所改變了,fetch()方法返回的類型是泛型List(List<T>),List繼承了Collection,完全存在使用Collection內非私有方法的權限,通過調用stream方法可以將集合轉換成Stream<E>泛型對象,該對象的map方法可以操作集合內單個對象的轉換,具體的轉換代碼可以根據業務邏輯進行編寫。
在map方法內有個lambda表達式參數tuple,我們通過tuple對象get方法就可以獲取對應select方法內的查詢字段。
tuple只能獲取select內存在的字段,如果select內為一個實體對象,tuple無法獲取指定字段的值。
運行測試
下面我們重啟下項目,訪問地址:127.0.0.1:8080/selectWithStream,界面輸出的內容如下代碼塊所示:
[
{
"id": 2,
"title": "油菜",
"unit": "斤",
"price": 12.6,
"typeName": "綠色蔬菜",
"typeId": 1
},
{
"id": 1,
"title": "金針菇",
"unit": "斤",
"price": 5.5,
"typeName": "菌類",
"typeId": 3
}
]
可以看到返回的數據跟上面方法是一致的,當然你們也能猜到自動生成的SQL也是一樣的,這里SQL就不做多解釋了。
總結
以上內容就是本章的全部內容,本章講解的兩種方法都是基于QueryDSL進行查詢只不過一種采用QueryDSL為我們提供的形式封裝自定義對象,而另外一種則是采用java8特性來完成的,Projections與Stream還有很多其他的方法,有興趣的小伙伴可以自行GitHub去查看。
QueryDSL官方文檔:http://www.querydsl.com/static/querydsl/latest/reference/html/ch02.html
本章代碼已經上傳碼云:
SpringBoot配套源碼地址:https://gitee.com/hengboy/spring-boot-chapter
SpringCloud配套源碼地址:https://gitee.com/hengboy/spring-cloud-chapter