在SpringDataJPA中,如何优雅的实现动态查询和连表查询
前言
在 ORM 框架的选择范围内,一直在讨论两个工具 Spring Data JPA 和 MyBatis,双方的争论各执一词,这里不去争论这些东西,不同的需求、不同的场景采用不同的解决方案是很正常的,孰优孰劣并没有万金油的答案。在这篇文章中我们来切切实实地解决 Spring Data JPA 中连表查询和动态查询实现复杂的问题。目前在网上搜这两个问题的解决方案大多是 JPQL 和 Specification 的方式,JPQL 在动态查询上不好实现,Specification 在实现的时候太麻烦,而且写出来的代码属实无法评价,也可能是我水平不够,见谅~。其他博客也是抄来抄去的,这里就不提这两种解决方案了,感兴趣的可以自行搜一下。好在现在可以搜到一些 QueryDSL 相关的博客了,虽然不多,但至少有人尝试新的解决方案,而不是简单的 CV,拿来就用。简介
JPA 2.0 标准引入了一种新的类型安全的构建查询的方法,可以利用注释在预处理期间生成元模型类,通过生成的元模型类可以构建查询语句。具体可以看 Criteria Query API。
QueryDSL 在编译的时候会自动帮我们生成一些 Criteria Query API 会用到的元模型类,然后我们可以直接用这些模型类构建查询,当然 QueryDSL 不仅仅只有这个作用。解决问题引入依赖
在 Maven 的 pom.xml 文件中引入 QueryDSL,目前使用 Maven 管理项目依赖还是比较多,Gradle 的使用方式这里就不介绍了,Quiet 用的就是 Gradle,需要的话可以看下项目的具体配置,或者私信我也行。 com.querydsl querydsl-apt ${querydsl.version} provided com.querydsl querydsl-jpa ${querydsl.version} com.mysema.maven apt-maven-plugin 1.1.3 process target/generated-sources/java com.querydsl.apt.jpa.JPAAnnotationProcessor 复制代码注入 JPAQueryFactory
在 Spring Boot 项目中可以注入 Bean JPAQueryFactory 方便查询时使用,@Configuration public class JpaAutoConfig { @PersistenceContext private final EntityManager entityManager; public JpaAutoConfig(EntityManager entityManager) { this.entityManager = entityManager; } @Bean public JPAQueryFactory jpaQueryFactory() { return new JPAQueryFactory(entityManager); } } 复制代码生成元模型类
编译项目,在开发的时候可以使用 maven 编译一下项目,或者直接运行项目也可以,这步主要是生成一些查询用到的元模型类。生成的模型类中我们用到类名最多的是 Q${EntityName}(前缀的 Q 好像是可以配置的,有需要修改的话可以自己研究下),EntityName 是我们的实体类的类名,比如@Entity public class User{} 复制代码
那么生成的元模型类的类名就是 QUser。查询
以下内容中的 queryFactory 即上文中注入的 Bean JPAQueryFactory,本文中只列举几种常用的查询方式,更多查询方式的构建可以看下官网文档(文末附有相关链接)。单表查询
简单的单表查询直接使用 Repository 实现即可,动态条件查询在文章后面有构建动态查询条件的方式。QCustomer customer = QCustomer.customer; Customer bob = queryFactory.selectFrom(customer) .where(customer.firstName.eq("Bob")) .fetchOne(); 复制代码or 查询queryFactory.selectFrom(customer) .where(customer.firstName.eq("Bob").or(customer.lastName.eq("Wilson"))); 复制代码查询部分字段QEmployee employee = QEmployee.employee; List result = queryFactory.select(employee.firstName, employee.lastName) .from(employee).fetch(); for (Tuple row : result) { System.out.println("firstName " + row.get(employee.firstName)); System.out.println("lastName " + row.get(employee.lastName)); }} 复制代码查询指定字段并返回指定类型使用 setter 方法构建查询结果QUser user = QUser.user; List dtos = queryFactory.select( Projections.bean(UserDTO.class, user.firstName, user.lastName)).fetch(); 复制代码使用字段填充的方式构建查询结果QUser user = QUser.user; List dtos = queryFactory.select( Projections.fields(UserDTO.class, user.firstName, user.lastName)).fetch(); 复制代码使用类构造方法构建查询结果QUser user = QUser.user; List dtos = queryFactory.select( Projections.constructor(UserDTO.class, user.firstName, user.lastName)).fetch(); 复制代码不同表之间 joinQQuietTeam quietTeam = QQuietTeam.quietTeam; QQuietTeamUser quietTeamUser = QQuietTeam.quietTeamUser; jpaQueryFactory .selectFrom(quietTeam) .leftJoin(quietTeamUser) .on(quietTeam.id.eq(quietTeamUser.teamId)) .where(where) .distinct() .fetch(); 复制代码join 表取别名QCat cat = QCat.cat; QCat mate = new QCat("mate"); QCat kitten = new QCat("kitten"); queryFactory.selectFrom(cat) .innerJoin(cat.mate, mate) .leftJoin(cat.kittens, kitten) .fetch(); 复制代码子查询QDepartment department = QDepartment.department; QDepartment d = new QDepartment("d"); queryFactory.selectFrom(department) .where(department.size.eq( JPAExpressions.select(d.size.max()).from(d))) .fetch(); 复制代码QEmployee employee = QEmployee.employee; QEmployee e = new QEmployee("e"); queryFactory.selectFrom(employee) .where(employee.weeklyhours.gt( JPAExpressions.select(e.weeklyhours.avg()) .from(employee.department.employees, e) .where(e.manager.eq(employee.manager)))) .fetch(); 复制代码分页查询
QueryDSL 的分页查询是内存分页,在 5.0.0 版本已经过期,不建议使用,如果确定数据量不多,影响不大的话可以使用 fetchResults 方法,在文档中推荐了另一个开源项目:Blaze-Persistence引入依赖: com.blazebit blaze-persistence-integration-querydsl-expressions ${blaze-persistence.version} compile com.blazebit blaze-persistence-integration-hibernate-5.6 ${blaze-persistence.version} runtime 复制代码注入 Bean CriteriaBuilderFactory/** * @author lin-mt */ @Configuration(proxyBeanMethods = false) public class JpaConfig { @PersistenceUnit private EntityManagerFactory entityManagerFactory; @Bean @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON) public CriteriaBuilderFactory createCriteriaBuilderFactory() { CriteriaBuilderConfiguration config = Criteria.getDefault(); // do some configuration return config.createCriteriaBuilderFactory(entityManagerFactory); } } 复制代码查询数据: @Override public PagedList pageUser( @NotNull Long deptId, QuietUser params, @NotNull Pageable page) { BooleanBuilder builder = SelectBuilder.booleanBuilder(params).getPredicate(); builder.and(quietDeptUser.deptId.eq(deptId)); return new BlazeJPAQuery(entityManager, criteriaBuilderFactory) .select(quietUser) .from(quietUser) .leftJoin(quietDeptUser) .on(quietUser.id.eq(quietDeptUser.userId)) .where(builder) .orderBy(quietUser.id.desc()) .fetchPage((int) page.getOffset(), page.getPageSize()); } 复制代码结合 JPA 查询
Spring Data JPA 提供了很多的扩展点,QueryDSL 和 Blaze-Persistence 构建的查询条件也是支持这些扩展点。在 JPA 中我们常用的是 org.springframework.data.jpa.repository.JpaRepository 相关的类作为我们 Repository 的父类,Spring Data JPA 对 QueryDSL 也是专门提供了一个接口(这应该能算 QueryDSL 得到了官方认可了吧):org.springframework.data.querydsl.QuerydslPredicateExecutor,那么我们在使用的时候就可以定义一个项目中所有 Repository 共用的父接口:/** * @author lin-mt */ @NoRepositoryBean public interface QuietRepository extends JpaRepository, QuerydslPredicateExecutor {} 复制代码
在分页查询的时候就可以使用方法:org.springframework.data.querydsl.QuerydslPredicateExecutor#findAll(com.querydsl.core.types.Predicate, org.springframework.data.domain.Pageable)
Predicate 参数是构建的查询条件实体信息的父类,下面我们会有动态查询条件构建的例子。动态查询
在项目中我们常常有动态查询的需求,比如前端传了用户名,我们就需要根据用户名进行模糊查询,没有传用户名,就不添加用户名的查询条件,这种需求在 Spring Data JPA 中实现是比较麻烦的,这也是很多项目不选 Spring Data JPA 作为项目 ORM 框架的原因之一,这里就介绍一种比较优雅且可读性较好的方式解决这个问题。
QueryDSL 提供了一种构建查询条件的实体类 com.querydsl.core.BooleanBuilder,这个类实现了接口 com.querydsl.core.types.Predicate,也就是文章上面提到的 Spring Data JPA 提供的 QueryDSL 扩展接口中方法的形参。 QEmployee employee = QEmployee.employee; BooleanBuilder builder = new BooleanBuilder(); for (String name : names) { builder.or(employee.name.equalsIgnoreCase(name)); } if (id != null) { builder.and(employee.id.equals(id)) } queryFactory.selectFrom(employee).where(builder).fetch(); 复制代码
构建动态查询的方式不仅仅只有 BooleanBuilder,所有 Predicate 的子类都可以:
更优雅地构建动态查询
在一些后台管理的项目中,统计需求往往会有很多的动态查询的字段,这时候可能就会出现很多的 if-else 的代码,这种代码可读性就不是很好了,观察一下 Q${EntityName} 的字段,相同类型的字段,它们返回的类型其实都是有共同的父类的,这就很好体现了 Java 的三大特性之一的多态。利用这点我们新建一个构建动态查询的工具类,将动态构建的 if-else 隐藏起来,工具里的方法可以根据自己项目的需要自行增删:/** * 查询条件构造器. * * @author lin-mt */ public abstract class SelectBuilder { @NotNull public static SelectBooleanBuilder booleanBuilder() { return new SelectBooleanBuilder(); } @NotNull public static SelectBooleanBuilder booleanBuilder(BaseEntity entity) { BooleanBuilder builder = null; if (entity != null) { builder = entity.booleanBuilder(); } return new SelectBooleanBuilder(builder); } /** * 获取查询条件 * * @return 查询条件 */ @NotNull public abstract T getPredicate(); } 复制代码/** * 构建 BooleanBuilder. * * @author lin-mt */ public class SelectBooleanBuilder extends SelectBuilder { private final BooleanBuilder builder; public SelectBooleanBuilder() { this.builder = new BooleanBuilder(); } public SelectBooleanBuilder(BooleanBuilder builder) { this.builder = builder == null ? new BooleanBuilder() : builder; } @Override public BooleanBuilder getPredicate() { return builder; } public SelectBooleanBuilder and(@Nullable Predicate right) { builder.and(right); return this; } public SelectBooleanBuilder andAnyOf(Predicate... args) { builder.andAnyOf(args); return this; } public SelectBooleanBuilder andNot(Predicate right) { return and(right.not()); } public SelectBooleanBuilder or(@Nullable Predicate right) { builder.or(right); return this; } public SelectBooleanBuilder orAllOf(Predicate... args) { builder.orAllOf(args); return this; } public SelectBooleanBuilder orNot(Predicate right) { return or(right.not()); } public SelectBooleanBuilder notNullEq(Boolean param, BooleanPath path) { if (param != null) { builder.and(path.eq(param)); } return this; } public > SelectBooleanBuilder notNullEq( T param, NumberPath path) { if (param != null) { builder.and(path.eq(param)); } return this; } public SelectBooleanBuilder isIdEq(Long param, NumberPath path) { if (param != null && param > 0L) { builder.and(path.eq(param)); } return this; } public > SelectBooleanBuilder leZeroIsNull( T param, NumberPath path) { if (param != null && param.longValue() <= 0) { builder.and(path.isNull()); } return this; } public SelectBooleanBuilder notBlankEq(String param, StringPath path) { if (StringUtils.isNoneBlank(param)) { builder.and(path.eq(param)); } return this; } public SelectBooleanBuilder with(@NotNull Consumer consumer) { if (consumer != null) { consumer.accept(this); } return this; } public > SelectBooleanBuilder notNullEq(T param, EnumPath path) { if (param != null) { builder.and(path.eq(param)); } return this; } public SelectBooleanBuilder notBlankContains(String param, StringPath path) { if (StringUtils.isNoneBlank(param)) { builder.and(path.contains(param)); } return this; } public SelectBooleanBuilder notNullEq(Dict dict, QDict qDict) { if (dict != null && StringUtils.isNoneBlank(dict.getKey())) { builder.and(qDict.eq(dict)); } return this; } public SelectBooleanBuilder notNullBefore(LocalDateTime param, DateTimePath path) { if (param != null) { builder.and(path.before(param)); } return this; } public SelectBooleanBuilder notNullAfter(LocalDateTime param, DateTimePath path) { if (param != null) { builder.and(path.after(param)); } return this; } public SelectBooleanBuilder notEmptyIn(Collection<? extends Long> param, NumberPath path) { if (CollectionUtils.isNotEmpty(param)) { builder.and(path.in(param)); } return this; } public SelectBooleanBuilder findInSet(Long param, SetPath> path) { if (param != null) { builder.and(Expressions.booleanTemplate("FIND_IN_SET({0}, {1}) > 0", param, path)); } return this; } } 复制代码使用例子@Override public List listByProjectIdAndName(Long projectId, Set ids, String name, Long limit) { if (Objects.isNull(projectId)) { return Lists.newArrayList(); } BooleanBuilder where = SelectBooleanBuilder.booleanBuilder() .and(docApiGroup.projectId.eq(projectId)) .notEmptyIn(ids, docApiGroup.id) .notBlankContains(name, docApiGroup.name) .getPredicate(); JPAQuery query = jpaQueryFactory.selectFrom(docApiGroup).where(where); if (limit != null && limit > 0) { query.limit(limit); } return query.fetch(); } 复制代码结语
这篇文章主要是介绍一些比较常用的内容,QueryDSL 是基于 SQL 标准实现了 SQL 语句的构建,对于不同类型的数据库(MySQL、Oracle等)具有的特性,就需要自己去构建查询方式了,比如上面的 findInSet 就是 MySQL 特有的函数,不在 SQL 标准中,所以要真正用好的话学习成本确实有点高,我也只是了解一点而已。Blaze-Persistence 也是一个很不错的开源项目,目前我也只是把它当成 QueryDSL 的补充,但其实它也提供了很多查询条件的构建方式,感兴趣的可以自行深入研究哈~最后,再附上相关链接
QueryDSL 官网:querydsl.com/
QueryDSL Github:github.com/querydsl/qu…
Blaze-Persistence 官网:persistence.blazebit.com/index.html
Blaze-Persistence Github:github.com/Blazebit/bl…
QueryDSL 文档:querydsl.com/static/quer…
Blaze-Persistence 文档:persistence.blazebit.com/documentati…
文档的链接带有版本号,目前是最新的(文章发布时间:2023-03-10),本文就不实时更新这个链接了,后续需要最新的文档可以到官网查询哈。
天然氧吧头号解忧馆昨天太近,明天太远,还是今天真实些,早安,朋友们。今天是大晴天,去不了大城市,可以在家乡百里竹海呼吸新鲜空气大自然的氧吧心情也很不错!漫步在竹林好好拥抱大自然。亲们,可以
通渭旅游景点通渭旅游景点通渭位于甘肃中部,地处黄土高原丘陵沟壑区,属于温带大陆性气候。此县城历史文化悠久,在此,给大家盘点一些值得一去的风景。1,通渭温泉通渭温泉位于通渭县城西南,大约8公里处
赞美秋天只会yyds?带你欣赏诗人笔下的秋天,感受中文之美!如今,日新月异的网络用语充斥着我们的生活,当面对这秋韵无限的美景,想要赞美一声,突然发现自己的词汇是如此的匮乏。一句绝美yyds!仿佛就是对秋日美景的所有赞叹。今天想从诗词中最能代
在新西兰南岛东南部的摩拉基海滩上,分布着许多圆形巨石?新西兰是一个大洋洲的岛屿国家,国土主要由北岛和南岛两大岛屿,以及其他一些小岛组成,国土总面积约为27万平方千米,相当于我国两个安徽省的面积。新西兰是一个移民国家,人类活动时间较短,
广州免费看展感受传统粤剧魅力博物馆正门口粤剧艺术博物馆,是广州市区内新建的文化场馆,我以前去过,今天突然兴起又去了一次,想再看一遍粵剧相关知识。粵剧作为广州的传统文化节目,现在年轻人可能会觉得听不懂,而选择去
国内时区着实不少,跨州旅行令人抓狂,澳大利亚人为时差烦恼来源环球时报环球时报驻澳大利亚特派记者陈效卫位于南半球的澳大利亚在10月初开始采用夏令时,澳大利亚虽然面积是世界第六,但时区却有10个之多。每年都会有很多人吐槽时差问题以及人造时间
弥勒西二大山深处的村子里,藏着一段百年米轨,神秘而凄美弥勒有条铁路,在大山里蜿蜒盘旋了百年,一进去就像穿越到了上个世纪。它就是滇越铁路!1探秘滇越铁路,去往大山深处的小河口站滇越铁路,世界三大工程奇迹之一,与苏伊士运河巴拿马运河齐名。
空灵山水,仙人荟萃浙江缙云3日游玩线路,快收好金秋十月,天朗气清,正值秋游赏景好时节。来浙江缙云赏美景,玩项目,品美食13日行程,不同主题的旅游线路让你畅玩这个秋天!三日游推荐线路仙都景区阳冰书房黄龙景区河阳古民居新建横街岩下
采摘露营民宿近郊游选择越来越多来源人民日报乡村特色民宿周边亲子酒店销售火热,房车露营生态采摘成为游玩热点刚刚过去的国庆假期,旅游市场迎来了一波短途近郊出游高峰。文旅部数据中心测算,全国国内旅游出游4。22亿人次
北京最美的悬崖公路,难度系数不输秋名山,老司机们准备好了吗?北京,我们都知道,作为我国一大标杆性城市,其不仅在经济发展方面有着很大的优势,同时北京的旅游景点数量优势极多的,不仅有着很多的自然绝美景区,或者是古建筑寺庙等等,同时还有着很多现代
我为什么看好Q4的港澳游?如果你不看好港澳留言告诉我你的理由自从9月13日从深圳飞邯郸,到9月24日从澳门坐船回深圳,我这趟去了4个地方。最后一站是我熟悉又陌生的澳门,受邀参加第10届澳门国际旅游博览会(M
嗨!我在喜马拉雅山上设计了一座云端木屋随着印度经济的蓬勃发展,印度城市目前正经历着日益增长的人口给城市结构带来的巨大压力。在社会富裕阶层中,前往原始且脆弱的栖息地这种周末度假活动越来越受欢迎。然而,度假木屋(WoodH
伊万卡川普离开白宫后华丽的生活环球旅游和奢侈宴会(全年)伊万卡环球旅行的幸福生活1月6日,Dailymail总结了伊万卡前一年都晒了哪些幸福照。自从她铁了心远离政治,跟华盛顿一刀两断,生活突然美好了很多两年后,这家人似乎比以往任何时候都
川贵喝酒怪相比起茅台五粮液,大多人更偏爱这5款白酒贵州四川的人们,地处美酒之乡,按道理来说,他们应该更喜欢茅台五粮液,但前段时间去这两个地方旅游,发现,他们大多数人更偏爱这5款白酒,咋回事?一大牌太贵了。二会自酿酒,受当地环境的熏
直击出入境政策优化首日赴港游率先走热出入境政策优化首日,香港成为出境游最先走热的目的地。1月8日上午10时36分,随着国泰航空CX334顺利落地首都机场,291名旅客到达北京,该航班也成为了第一班使用T3航站楼E区的
闺蜜行北海涠洲岛超强攻略首先呢这篇游记是完全处于自己这几天亲身感受过后一定要分享给大家省的大家走弯路的攻略。答应我看完!第一天我是坐高铁直接到的北海,到了已经晚上九点了,所以就做好了准备,晚上现在北海住一
奔流东去的苏马荡磁洞沟大峡谷奔流东去磁洞沟文赵青松苏马荡和齐岳山之间有一条大峡谷,候鸟们唤其为磁洞沟大峡谷。可别小看这条峡谷,这条峡谷蜿蜒东去到云阳的新津口直通长江。齐岳山的风和苏马荡的凉皆来源于此。由于地势
嘿,磨黑!来源云南日报去磨黑就像走邻居一般,由南向北既可以沿老磨思公路从农贸市场旁进入,又可以途经宁洱县城过八千方在康佳饭店前方岔路口右转直下,或从北边把边老公路沿河而上。如果你是驱车从元江
齐齐哈尔市梅里斯达斡尔族区第六届冬捕节启幕以达斡尔族渔猎文化展现冰雪魅力一条条鲜活肥美的湖鱼随网摆尾而出宋燕军摄中新网齐齐哈尔1月8日电(贾婷张振翼记者刘锡菊)8日,在齐齐哈尔市梅里斯达斡尔族区梅里斯湖景区内,梅里斯湖第六届冬捕节活动启幕,开幕仪式现场
延庆区旧县冬季旅游欢乐季启动云瀑沟冰瀑进入最佳观赏期迎新年戏冰雪过大年。元旦第一天,冬游赏冰趣旧县过大年延庆区旧县镇冬季旅游欢乐季在延庆区云瀑沟景区正式启动。本次活动以云瀑沟景区开园迎客为背景,联动旧县镇周边的龙庆峡冰雪季万科石京龙
苏州银行股价创近2年多新高,三大因素成跑赢板块的关键密码对苏州银行来说,一个全新的成长期正在开启。作者胜马财经李察编辑欧阳文时隔近三年,苏州银行股价重新站上8元。胜马财经获悉,1月5日,银行股未能复制前一日全线飘红的火热行情,但苏州银行
陈戌源又被曝出丑闻,随队出征期间每晚打麻将,从苏州玩到西亚陈戌源又被曝出丑闻!在其位不谋其政,随队出征期间每晚玩牌打麻将!一直从苏州打到西亚!根据足球报著名记者李璇透露消息,足协主席陈戌源在随队出征40强赛和12强赛期间,无论是在国内还是