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

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

图1

我们双击compile命令执行,执行完成后会在我们pom.xml配置文件内配置生成目录内生成对应实体的QueryDSL查询实体。生成的查询实体如下图2所示:

图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

作者个人 博客
使用开源框架 ApiBoot 助你成为Api接口服务架构师

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容