使用QueryDSL与SpringDataJPA实现查询返回自定义对象

恒宇少年2018-10-29 18:58:48

在我们实际项目开发中,往往会遇到一种多表关联查询并且仅需要返回多表内的几个字段最后组合成一个集合或者实体。这种情况在传统的查询中我们无法控制查询的字段,只能全部查询出后再做出分离,这种也是我们最不愿意看到的处理方式,这种方式会产生繁琐、复杂、效率低、代码阅读性差等等问题。QueryDSL为我们提供了一个返回自定义对象的工具类型,而Java8新特性Collection中stream方法也能够完成返回自定义对象的逻辑,下面我们就来看下这两种方式如何编写?

本章目标

基于SpringBoot平台完成SpringDataJPA与QueryDSL整合查询返回自定义对象的两种方式。

构建项目

我们先来使用idea工具创建一个SpringBoot项目,预先添加相对应的依赖,pom.xml配置文件内容如下所示:

  1. <?xml version="1.0" encoding="UTF-8"?>

  2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  3.    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  4.    <modelVersion>4.0.0</modelVersion>

  5.    <groupId>com.yuqiyu.querydsl.sample</groupId>

  6.    <artifactId>chapter5</artifactId>

  7.    <version>0.0.1-SNAPSHOT</version>

  8.    <packaging>war</packaging>

  9.    <name>chapter5</name>

  10.    <description>Demo project for Spring Boot</description>

  11.    <parent>

  12.        <groupId>org.springframework.boot</groupId>

  13.        <artifactId>spring-boot-starter-parent</artifactId>

  14.        <version>1.5.4.RELEASE</version>

  15.        <relativePath/> <!-- lookup parent from repository -->

  16.    </parent>

  17.    <properties>

  18.        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

  19.        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

  20.        <java.version>1.8</java.version>

  21.    </properties>

  22.    <dependencies>

  23.        <dependency>

  24.            <groupId>org.springframework.boot</groupId>

  25.            <artifactId>spring-boot-starter-data-jpa</artifactId>

  26.        </dependency>

  27.        <dependency>

  28.            <groupId>org.springframework.boot</groupId>

  29.            <artifactId>spring-boot-starter-web</artifactId>

  30.        </dependency>

  31.        <dependency>

  32.            <groupId>mysql</groupId>

  33.            <artifactId>mysql-connector-java</artifactId>

  34.            <scope>runtime</scope>

  35.        </dependency>

  36.        <!--阿里巴巴数据库连接池,专为监控而生 -->

  37.        <dependency>

  38.            <groupId>com.alibaba</groupId>

  39.            <artifactId>druid</artifactId>

  40.            <version>1.0.26</version>

  41.        </dependency>

  42.        <!-- 阿里巴巴fastjson,解析json视图 -->

  43.        <dependency>

  44.            <groupId>com.alibaba</groupId>

  45.            <artifactId>fastjson</artifactId>

  46.            <version>1.2.15</version>

  47.        </dependency>

  48.        <dependency>

  49.            <groupId>org.springframework.boot</groupId>

  50.            <artifactId>spring-boot-starter-tomcat</artifactId>

  51.            <!--<scope>provided</scope>-->

  52.        </dependency>

  53.        <dependency>

  54.            <groupId>org.springframework.boot</groupId>

  55.            <artifactId>spring-boot-starter-test</artifactId>

  56.            <scope>test</scope>

  57.        </dependency>

  58.        <!--queryDSL-->

  59.        <dependency>

  60.            <groupId>com.querydsl</groupId>

  61.            <artifactId>querydsl-jpa</artifactId>

  62.            <version>${querydsl.version}</version>

  63.        </dependency>

  64.        <dependency>

  65.            <groupId>com.querydsl</groupId>

  66.            <artifactId>querydsl-apt</artifactId>

  67.            <version>${querydsl.version}</version>

  68.            <scope>provided</scope>

  69.        </dependency>

  70.        <dependency>

  71.            <groupId>org.projectlombok</groupId>

  72.            <artifactId>lombok</artifactId>

  73.            <version>1.16.16</version>

  74.        </dependency>

  75.        <dependency>

  76.            <groupId>javax.inject</groupId>

  77.            <artifactId>javax.inject</artifactId>

  78.            <version>1</version>

  79.        </dependency>

  80.    </dependencies>

  81.    <build>

  82.        <plugins>

  83.            <plugin>

  84.                <groupId>org.springframework.boot</groupId>

  85.                <artifactId>spring-boot-maven-plugin</artifactId>

  86.            </plugin>

  87.            <!--添加QueryDSL插件支持-->

  88.            <plugin>

  89.                <groupId>com.mysema.maven</groupId>

  90.                <artifactId>apt-maven-plugin</artifactId>

  91.                <version>1.1.3</version>

  92.                <executions>

  93.                    <execution>

  94.                        <goals>

  95.                            <goal>process</goal>

  96.                        </goals>

  97.                        <configuration>

  98.                            <outputDirectory>target/generated-sources/java</outputDirectory>

  99.                            <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>

  100.                        </configuration>

  101.                    </execution>

  102.                </executions>

  103.            </plugin>

  104.        </plugins>

  105.    </build>

  106. </project>

上面内的QueryDSL这里就不多做讲解了,如有疑问请查看第一章:Maven环境下如何配置QueryDSL环境。 下面我们需要创建两张表来完成本章的内容。

创建表结构

跟上一章一样,我们还是使用商品信息表、商品类型表来完成编码。

商品信息表

  1. -- ----------------------------

  2. -- Table structure for good_infos

  3. -- ----------------------------

  4. DROP TABLE IF EXISTS `good_infos`;

  5. CREATE TABLE `good_infos` (

  6.  `tg_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键自增',

  7.  `tg_title` varchar(50) CHARACTER SET utf8 DEFAULT NULL COMMENT '商品标题',

  8.  `tg_price` decimal(8,2) DEFAULT NULL COMMENT '商品单价',

  9.  `tg_unit` varchar(20) CHARACTER SET utf8 DEFAULT NULL COMMENT '单位',

  10.  `tg_order` varchar(255) DEFAULT NULL COMMENT '排序',

  11.  `tg_type_id` int(11) DEFAULT NULL COMMENT '类型外键编号',

  12.  PRIMARY KEY (`tg_id`),

  13.  KEY `tg_type_id` (`tg_type_id`)

  14. ) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=latin1;

  15. -- ----------------------------

  16. -- Records of good_infos

  17. -- ----------------------------

  18. INSERT INTO `good_infos` VALUES ('1', '金针菇', '5.50', '斤', '1', '3');

  19. INSERT INTO `good_infos` VALUES ('2', '油菜', '12.60', '斤', '2', '1');

商品类型信息表

  1. -- ----------------------------

  2. -- Table structure for good_types

  3. -- ----------------------------

  4. DROP TABLE IF EXISTS `good_types`;

  5. CREATE TABLE `good_types` (

  6.  `tgt_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键自增',

  7.  `tgt_name` varchar(30) CHARACTER SET utf8 DEFAULT NULL COMMENT '类型名称',

  8.  `tgt_is_show` char(1) DEFAULT NULL COMMENT '是否显示',

  9.  `tgt_order` int(2) DEFAULT NULL COMMENT '类型排序',

  10.  PRIMARY KEY (`tgt_id`)

  11. ) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=latin1;

  12. -- ----------------------------

  13. -- Records of good_types

  14. -- ----------------------------

  15. INSERT INTO `good_types` VALUES ('1', '绿色蔬菜', '1', '1');

  16. INSERT INTO `good_types` VALUES ('2', '根茎类', '1', '2');

  17. INSERT INTO `good_types` VALUES ('3', '菌类', '1', '3');

创建实体

我们对应表结构创建实体并且添加对应的SpringDataJPA注解。

商品实体

  1. package com.yuqiyu.querydsl.sample.chapter5.bean;

  2. import lombok.Data;

  3. import javax.persistence.*;

  4. import java.io.Serializable;

  5. /**

  6. * 商品基本信息实体

  7. * ========================

  8. * Created with IntelliJ IDEA.

  9. * User:恒宇少年

  10. * Date:2017/7/10

  11. * Time:22:39

  12. * 码云:http://git.oschina.net/jnyqy

  13. * ========================

  14. */

  15. @Entity

  16. @Table(name = "good_infos")

  17. @Data

  18. public class GoodInfoBean

  19.    implements Serializable

  20. {

  21.    //主键

  22.    @Id

  23.    @Column(name = "tg_id")

  24.    @GeneratedValue

  25.    private Long id;

  26.    //标题

  27.    @Column(name = "tg_title")

  28.    private String title;

  29.    //价格

  30.    @Column(name = "tg_price")

  31.    private double price;

  32.    //单位

  33.    @Column(name = "tg_unit")

  34.    private String unit;

  35.    //排序

  36.    @Column(name = "tg_order")

  37.    private int order;

  38.    //类型编号

  39.    @Column(name = "tg_type_id")

  40.    private Long typeId;

  41. }

商品类型实体

  1. package com.yuqiyu.querydsl.sample.chapter5.bean;

  2. import lombok.Data;

  3. import javax.persistence.*;

  4. import java.io.Serializable;

  5. /**

  6. * 商品类别实体

  7. * ========================

  8. * Created with IntelliJ IDEA.

  9. * User:恒宇少年

  10. * Date:2017/7/10

  11. * Time:22:39

  12. * 码云:http://git.oschina.net/jnyqy

  13. * ========================

  14. */

  15. @Entity

  16. @Table(name = "good_types")

  17. @Data

  18. public class GoodTypeBean

  19.    implements Serializable

  20. {

  21.    //类型编号

  22.    @Id

  23.    @GeneratedValue

  24.    @Column(name = "tgt_id")

  25.    private Long id;

  26.    //类型名称

  27.    @Column(name = "tgt_name")

  28.    private String name;

  29.    //是否显示

  30.    @Column(name = "tgt_is_show")

  31.    private int isShow;

  32.    //排序

  33.    @Column(name = "tgt_order")

  34.    private int order;

  35. }

上面实体内的注解@Entity标识该实体被SpringDataJPA所管理,@Table标识该实体对应的数据库内的表信息,@Data该注解则是lombok内的合并注解,根据idea工具的插件自动添加getter/setter、toString、全参构造函数等。

创建DTO

我们创建一个查询返回的自定义对象,对象内的字段包含了商品实体、商品类型实体内的部分内容,DTO代码如下所示:

  1. package com.yuqiyu.querydsl.sample.chapter5.dto;

  2. import lombok.Data;

  3. import java.io.Serializable;

  4. /**

  5. * 商品dto

  6. * ========================

  7. * Created with IntelliJ IDEA.

  8. * User:恒宇少年

  9. * Date:2017/7/10

  10. * Time:22:39

  11. * 码云:http://git.oschina.net/jnyqy

  12. * ========================

  13. */

  14. @Data

  15. public class GoodDTO

  16.    implements Serializable

  17. {

  18.    //主键

  19.    private Long id;

  20.    //标题

  21.    private String title;

  22.    //单位

  23.    private String unit;

  24.    //价格

  25.    private double price;

  26.    //类型名称

  27.    private String typeName;

  28.    //类型编号

  29.    private Long typeId;

  30. }

要注意我们的自定义返回的对象仅仅只是一个实体,并不对应数据库内的表,所以这里不需要配置@Entity、@Table等JPA注解,仅把@Data注解配置上就可以了,接下来我们编译下项目让QueryDSL插件自动生成查询实体。

生成查询实体

idea工具为maven project自动添加了对应的功能,我们打开右侧的Maven Projects,如下图1所示:

QueryDSL配置JPA插件仅会根据@Entity进行生成查询实体

创建控制器

我们来创建一个测试的控制器读取商品表内的所有商品,在编写具体的查询方法之前我们需要实例化EntityManager对象以及JPAQueryFactory对象,并且通过实例化控制器时就去实例化JPAQueryFactory对象。控制器代码如下所示:

  1. package com.yuqiyu.querydsl.sample.chapter5.controller;

  2. import com.querydsl.core.types.Projections;

  3. import com.querydsl.jpa.impl.JPAQueryFactory;

  4. import com.yuqiyu.querydsl.sample.chapter5.bean.QGoodInfoBean;

  5. import com.yuqiyu.querydsl.sample.chapter5.bean.QGoodTypeBean;

  6. import com.yuqiyu.querydsl.sample.chapter5.dto.GoodDTO;

  7. import org.springframework.beans.factory.annotation.Autowired;

  8. import org.springframework.web.bind.annotation.RequestMapping;

  9. import org.springframework.web.bind.annotation.RestController;

  10. import javax.annotation.PostConstruct;

  11. import javax.persistence.EntityManager;

  12. import java.util.List;

  13. import java.util.stream.Collectors;

  14. /**

  15. * 多表查询返回商品dto控制器

  16. * ========================

  17. * Created with IntelliJ IDEA.

  18. * User:恒宇少年

  19. * Date:2017/7/10

  20. * Time:23:04

  21. * 码云:http://git.oschina.net/jnyqy

  22. * ========================

  23. */

  24. @RestController

  25. public class GoodController

  26. {

  27.    //实体管理

  28.    @Autowired

  29.    private EntityManager entityManager;

  30.    //查询工厂

  31.    private JPAQueryFactory queryFactory;

  32.    //初始化查询工厂

  33.    @PostConstruct

  34.    public void init()

  35.    {

  36.        queryFactory = new JPAQueryFactory(entityManager);

  37.    }

  38. }

可以看到我们配置的是一个@RestController该控制器返回的数据都是Json字符串,这也是RestController所遵循的规则。

QueryDSL & Projections

下面我们开始编写完全基于QueryDSL形式的返回自定义对象方法,代码如下所示:

  1. /**

  2.     * 根据QueryDSL查询

  3.     * @return

  4.     */

  5.    @RequestMapping(value = "/selectWithQueryDSL")

  6.    public List<GoodDTO> selectWithQueryDSL()

  7.    {

  8.        //商品基本信息

  9.        QGoodInfoBean _Q_good = QGoodInfoBean.goodInfoBean;

  10.        //商品类型

  11.        QGoodTypeBean _Q_good_type = QGoodTypeBean.goodTypeBean;

  12.        return queryFactory

  13.                .select(

  14.                        Projections.bean(

  15.                                GoodDTO.class,//返回自定义实体的类型

  16.                                _Q_good.id,

  17.                                _Q_good.price,

  18.                                _Q_good.title,

  19.                                _Q_good.unit,

  20.                                _Q_good_type.name.as("typeName"),//使用别名对应dto内的typeName

  21.                                _Q_good_type.id.as("typeId")//使用别名对应dto内的typeId

  22.                         )

  23.                )

  24.                .from(_Q_good,_Q_good_type)//构建两表笛卡尔集

  25.                .where(_Q_good.typeId.eq(_Q_good_type.id))//关联两表

  26.                .orderBy(_Q_good.order.desc())//倒序

  27.                .fetch();

  28.    }

我们可以看到上面selectWithQueryDSL()查询方法,里面出现了一个新的类型Projections,这个类型是QueryDSL内置针对处理自定义返回结果集的解决方案,里面包含了构造函数、实体、字段等处理方法,我们今天主要讲解下实体。

JPAQueryFactory工厂select方法可以将Projections方法返回的QBean作为参数,我们通过Projections的bean方法来构建返回的结果集映射到实体内,有点像Mybatis内的ResultMap的形式,不过内部处理机制肯定是有着巨大差别的!bean方法第一个参数需要传递一个实体的泛型类型作为返回集合内的单个对象类型,如果QueryDSL查询实体内的字段与DTO实体的字段名字不一样时,我们就可以采用as方法来处理,为查询的结果集指定的字段添加别名,这样就会自动映射到DTO实体内。

运行测试

下面我们来运行下项目,访问地址:http://127.0.0.1:8080/selectWithQueryDSL查看界面输出的效果如下代码块所示:

  1. [

  2.    {

  3.        "id": 2,

  4.        "title": "油菜",

  5.        "unit": "斤",

  6.        "price": 12.6,

  7.        "typeName": "绿色蔬菜",

  8.        "typeId": 1

  9.    },

  10.    {

  11.        "id": 1,

  12.        "title": "金针菇",

  13.        "unit": "斤",

  14.        "price": 5.5,

  15.        "typeName": "菌类",

  16.        "typeId": 3

  17.    }

  18. ]

我们可以看到输出的Json数组字符串就是我们DTO内的所有字段反序列后的效果,DTO实体内对应的typeName、typeId都已经查询出并且赋值。 下面我们来查看控制台输出自动生成的SQL,如下代码块所示:

  1. Hibernate:

  2.    select

  3.        goodinfobe0_.tg_id as col_0_0_,

  4.        goodinfobe0_.tg_price as col_1_0_,

  5.        goodinfobe0_.tg_title as col_2_0_,

  6.        goodinfobe0_.tg_unit as col_3_0_,

  7.        goodtypebe1_.tgt_name as col_4_0_,

  8.        goodtypebe1_.tgt_id as col_5_0_

  9.    from

  10.        good_infos goodinfobe0_ cross

  11.    join

  12.        good_types goodtypebe1_

  13.    where

  14.        goodinfobe0_.tg_type_id=goodtypebe1_.tgt_id

  15.    order by

  16.        goodinfobe0_.tg_order desc

生成的SQL是cross join形式关联查询,关联 形式通过where goodinfobe0.tgtypeid=goodtypebe1.tgtid 代替了on goodinfobe0.tgtypeid=goodtypebe1.tgtid,最终查询结果集返回数据这两种方式一致。

QueryDSL & Collection

下面我们采用java8新特性返回自定义结果集,我们查询仍然采用QueryDSL形式,方法代码如下所示:

  1. /**

  2.     * 使用java8新特性Collection内stream方法转换dto

  3.     * @return

  4.     */

  5.    @RequestMapping(value = "/selectWithStream")

  6.    public List<GoodDTO> selectWithStream()

  7.    {

  8.        //商品基本信息

  9.        QGoodInfoBean _Q_good = QGoodInfoBean.goodInfoBean;

  10.        //商品类型

  11.        QGoodTypeBean _Q_good_type = QGoodTypeBean.goodTypeBean;

  12.        return queryFactory

  13.                .select(

  14.                        _Q_good.id,

  15.                        _Q_good.price,

  16.                        _Q_good.title,

  17.                        _Q_good.unit,

  18.                        _Q_good_type.name,

  19.                        _Q_good_type.id

  20.                )

  21.                .from(_Q_good,_Q_good_type)//构建两表笛卡尔集

  22.                .where(_Q_good.typeId.eq(_Q_good_type.id))//关联两表

  23.                .orderBy(_Q_good.order.desc())//倒序

  24.                .fetch()

  25.                .stream()

  26.                //转换集合内的数据

  27.                .map(tuple -> {

  28.                    //创建商品dto

  29.                    GoodDTO dto = new GoodDTO();

  30.                    //设置商品编号

  31.                    dto.setId(tuple.get(_Q_good.id));

  32.                    //设置商品价格

  33.                    dto.setPrice(tuple.get(_Q_good.price));

  34.                    //设置商品标题

  35.                    dto.setTitle(tuple.get(_Q_good.title));

  36.                    //设置单位

  37.                    dto.setUnit(tuple.get(_Q_good.unit));

  38.                    //设置类型编号

  39.                    dto.setTypeId(tuple.get(_Q_good_type.id));

  40.                    //设置类型名称

  41.                    dto.setTypeName(tuple.get(_Q_good_type.name));

  42.                    //返回本次构建的dto

  43.                    return dto;

  44.                })

  45.                //返回集合并且转换为List<GoodDTO>

  46.                .collect(Collectors.toList());

  47.    }

从方法开始到fetch()结束完全跟QueryDSL没有任何区别,采用了最原始的方式进行返回结果集,但是从fetch()获取到结果集后我们处理的方式就有所改变了,fetch()方法返回的类型是泛型List(List),List继承了Collection,完全存在使用Collection内非私有方法的权限,通过调用stream方法可以将集合转换成Stream泛型对象,该对象的map方法可以操作集合内单个对象的转换,具体的转换代码可以根据业务逻辑进行编写。 在map方法内有个lambda表达式参数tuple,我们通过tuple对象get方法就可以获取对应select方法内的查询字段。

tuple只能获取select内存在的字段,如果select内为一个实体对象,tuple无法获取指定字段的值。

运行测试

下面我们重启下项目,访问地址:127.0.0.1:8080/selectWithStream,界面输出的内容如下代码块所示:

  1. [

  2.    {

  3.        "id": 2,

  4.        "title": "油菜",

  5.        "unit": "斤",

  6.        "price": 12.6,

  7.        "typeName": "绿色蔬菜",

  8.        "typeId": 1

  9.    },

  10.    {

  11.        "id": 1,

  12.        "title": "金针菇",

  13.        "unit": "斤",

  14.        "price": 5.5,

  15.        "typeName": "菌类",

  16.        "typeId": 3

  17.    }

  18. ]

可以看到返回的数据跟上面方法是一致的,当然你们也能猜到自动生成的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

同系列文章

SpringBoot与QueryDSL初整合


QueryDSL与SpringDataJPA实现单表普通条件查询


使用QueryDSL与SpringDataJPA完成Update&Delete


QueryDSL与SpringDataJPA实现多表关联查询

推荐阅读

SpringBoot平台使用Lombok来优雅的编码


在SpringBoot内如何使用ApplicationEvent&Listener完成业务解耦?


SpringBoot自定义专属业务的Starter


SpringBoot使用MapStruct自动映射DTO


SpringBoot架构使用Profile完成打包环境分离


SpringBoot2.0新特性 - 你get到WebMvcConfigurer两种配置方式了吗?


SpringBoot2.0新特性 - 岂止至今最简单redis缓存集成


SpringBoot2.0新特性 - Quartz自动化配置集成


编码规范 - 养成良好的Java编码习惯


基于SpringBoot 设计业务逻辑异常统一处理


基于SpringBoot & Quartz分布式单节点持久化

加入知识星球,恒宇少年带你走以后的技术道路!!!


Copyright © 温县电话机虚拟社区@2017