第四章:使用QueryDSL与SpringDataJPA实现多表关联查询

对于业务逻辑复制的系统来说都存在多表关联查询的情况,查询的返回对象内容也是根据具体业务来处理的,我们本章主要是针对多表关联根据条件查询后返回单表对象,在下一章我们就会针对多表查询返回自定义的对象实体。

本章目标

基于SpringBoot框架平台完成SpringDataJPA与QueryDSL多表关联查询返回单表对象实例,查询时完全采用QueryDSL语法进行编写。

构建项目

我们使用idea工具先来创建一个SpringBoot项目,添加的依赖跟第三章:使用QueryDSL与SpringDataJPA完成Update&Delete一致。为了方便分离文章源码,我们创建完成后把第三章的application.yml配置文件以及pom.xml依赖内容复制到本章项目中(配置内容请参考第三章)。

创建数据表

我们先来根据一个简单的业务逻辑来创建两张一对多关系的表,下面我们先来创建商品类型信息表,代码如下:

-- ----------------------------
-- 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;

接下来我们再来创建一个商品基本信息表,表结构如下代码所示:

-- ----------------------------
-- 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;

创建实体

我们对应上面两张表的结构创建两个实体并添加对应的SpringDataJPA注解配置,商品类型实体如下所示:

package com.yuqiyu.querydsl.sample.chapter4.bean;

import lombok.Data;

import javax.persistence.*;
import java.io.Serializable;

/**
 * ========================
 * Created with IntelliJ IDEA.
 * User:恒宇少年
 * Date:2017/7/9
 * Time:15:04
 * 码云: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;
}

商品基本信息实体如下所示:

package com.yuqiyu.querydsl.sample.chapter4.bean;

import lombok.Data;

import javax.persistence.*;
import java.io.Serializable;

/**
 * ========================
 * Created with IntelliJ IDEA.
 * User:恒宇少年
 * Date:2017/7/9
 * Time:15:08
 * 码云:http://git.oschina.net/jnyqy
 * ========================
 */
@Entity
@Table(name = "good_infos")
@Data
public class GoodInfoBean
    implements Serializable
{
    //主键
    @Id
    @GeneratedValue
    @Column(name = "tg_id")
    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;
}

我在商品表内并没有使用类型的实体作为表之间的关联而是只用的具体类型编号,有的时候也是根据你的需求来配置的,如果你每个商品读取基本信息时都需要获取商品的类型,那么这里配置@OneToOne还是比较省事,不需要你再操作多余的查询。

构建QueryDSL查询实体

下面我们使用maven compile命令来自动生成QueryDSL的查询实体,我们在执行命令的时候会自动去pom.xml配置文件内查找JPAAnnotationProcessor插件,如果你的实体配置了@Entity注解,那么就会自动生成查询实体并将生成的实体放置到target/generated-sources/java内。
我们找到idea工具的Maven Projects窗口,如下图1所示:

图1

我们双击对应的命令就可以执行构建项目了,构建完成的查询实体如下图2所示:


图2

如上图2所示,QueryDSL在生成时会完全根据实体的包来对应创建。

创建控制器

下面我们来创建一个控制器,我们在控制器内直接编写QueryDSL查询代码,这里就不去根据MVC模式进行编程了,在正式环境下还请大家按照MVC模式来编码。
控制器代码如下所示:

package com.yuqiyu.querydsl.sample.chapter4.controller;

import com.querydsl.jpa.impl.JPAQueryFactory;
import com.yuqiyu.querydsl.sample.chapter4.bean.GoodInfoBean;
import com.yuqiyu.querydsl.sample.chapter4.bean.QGoodInfoBean;
import com.yuqiyu.querydsl.sample.chapter4.bean.QGoodTypeBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.PostConstruct;
import javax.persistence.EntityManager;
import java.util.List;

/**
 * ========================
 * Created with IntelliJ IDEA.
 * User:恒宇少年
 * Date:2017/7/9
 * Time:15:24
 * 码云:http://git.oschina.net/jnyqy
 * ========================
 */
@RestController
public class GoodController
{

    @Autowired
    private EntityManager entityManager;

    //查询工厂实体
    private JPAQueryFactory queryFactory;

    //实例化控制器完成后执行该方法实例化JPAQueryFactory
    @PostConstruct
    public void initFactory()
    {
        System.out.println("开始实例化JPAQueryFactory");
        queryFactory = new JPAQueryFactory(entityManager);
    }

    @RequestMapping(value = "/selectByType")
    public List<GoodInfoBean> selectByType
            (
                    @RequestParam(value = "typeId") Long typeId //类型编号
            )
    {
        //商品查询实体
        QGoodInfoBean _Q_good = QGoodInfoBean.goodInfoBean;
        //商品类型查询实体
        QGoodTypeBean _Q_good_type = QGoodTypeBean.goodTypeBean;
        return
                queryFactory
                .select(_Q_good)
                .from(_Q_good,_Q_good_type)
                .where(
                        //为两个实体关联查询
                        _Q_good.typeId.eq(_Q_good_type.id)
                        .and(
                                //查询指定typeid的商品
                                _Q_good_type.id.eq(typeId)
                        )
                )
                //根据排序字段倒序
                .orderBy(_Q_good.order.desc())
                //执行查询
                .fetch();
    }
}

可以看到上面的代码,我们查询了两张表,仅返回了商品信息内的字段(select(_Q_good)),我们在where条件内进行了这两张表的关联,根据传递的类型编号作为关联商品类型主键(相当于left join),最后根据排序字段进行倒序。

运行测试

下面我们来运行项目,控制台日志输出内容如下所示:

.....

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.5.4.RELEASE)

.....
开始实例化JPAQueryFactory
2017-07-09 15:40:38.454  INFO 11776 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@1184ab05: startup date [Sun Jul 09 15:40:36 CST 2017]; root of context hierarchy
2017-07-09 15:40:38.495  INFO 11776 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/selectByType]}" onto public java.util.List<com.yuqiyu.querydsl.sample.chapter4.bean.GoodInfoBean> com.yuqiyu.querydsl.sample.chapter4.controller.GoodController.selectByType(java.lang.Long)
2017-07-09 15:40:38.498  INFO 11776 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2017-07-09 15:40:38.498  INFO 11776 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2017-07-09 15:40:38.515  INFO 11776 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-07-09 15:40:38.515  INFO 11776 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-07-09 15:40:38.536  INFO 11776 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-07-09 15:40:38.721  INFO 11776 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2017-07-09 15:40:38.723  INFO 11776 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Bean with name 'dataSource' has been autodetected for JMX exposure
2017-07-09 15:40:38.726  INFO 11776 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Located MBean 'dataSource': registering with JMX server as MBean [com.alibaba.druid.pool:name=dataSource,type=DruidDataSource]
2017-07-09 15:40:38.765  INFO 11776 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2017-07-09 15:40:38.769  INFO 11776 --- [           main] c.y.q.s.chapter4.Chapter4Application     : Started Chapter4Application in 3.027 seconds (JVM running for 3.683)

可以看到我们在项目启动的时候JPAQueryFactory查询工厂对象就被实例了,接下来我们直接使用JPAQueryFactory实例对象就Ok了。下面我们来访问 : http://127.0.0.1:8080/selectByType?typeId=1
界面输出内容如下图3所示:

图3

下面我们来看下控制台输出的SQL,如下所示:

Hibernate: 
    select
        goodinfobe0_.tg_id as tg_id1_0_,
        goodinfobe0_.tg_order as tg_order2_0_,
        goodinfobe0_.tg_price as tg_price3_0_,
        goodinfobe0_.tg_title as tg_title4_0_,
        goodinfobe0_.tg_type_id as tg_type_5_0_,
        goodinfobe0_.tg_unit as tg_unit6_0_ 
    from
        good_infos goodinfobe0_ cross 
    join
        good_types goodtypebe1_ 
    where
        goodinfobe0_.tg_type_id=goodtypebe1_.tgt_id 
        and goodtypebe1_.tgt_id=? 
    order by
        goodinfobe0_.tg_order desc

QueryDSL自动生成的SQL采用了Cross Join 获取两张表的《笛卡尔集》然后根据select内配置的实体进行返回字段,我们使用 where goodinfobe0_.tg_type_id=goodtypebe1_.tgt_id 代替了on goodinfobe0_.tg_type_id=goodtypebe1_.tgt_id实现了相同的效果。

总结

本章的内容比较简单,我们使用QueryDSL完成了两个实体关联查询并返回单实体实例的方法,QueryDSL内也有LeftJoin、InnerJoin等关联查询不过都是基于具体实体类型来完成的,本章就不做解释了,用起来比较繁琐复杂它们遵循的是HQL语法。

本章代码已经上传到码云:
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

推荐阅读更多精彩内容