第四章:使用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所示:
我们双击对应的命令就可以执行构建项目了,构建完成的查询实体如下图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所示:
下面我们来看下控制台输出的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
SpringBoot相关系列文章请访问:目录:SpringBoot学习目录
QueryDSL相关系列文章请访问:QueryDSL通用查询框架学习目录
SpringDataJPA相关系列文章请访问:目录:SpringDataJPA学习目录
感谢阅读!