在我们实际项目开发中,往往会遇到一种多表关联查询并且仅需要返回多表内的几个字段最后组合成一个集合或者实体。这种情况在传统的查询中我们无法控制查询的字段,只能全部查询出后再做出分离,这种也是我们最不愿意看到的处理方式,这种方式会产生繁琐、复杂、效率低、代码阅读性差等等问题。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