在SpringDataJPA中,如何优雅的实现动态查询和连表
前言
在ORM框架的选择范围内,一直在讨论两个工具SpringDataJPA和MyBatis,双方的争论各执一词,这里不去争论这些东西,不同的需求、不同的场景采用不同的解决方案是很正常的,孰优孰劣并没有万金油的答案。在这篇文章中我们来切切实实地解决SpringDataJPA中连表查询和动态查询实现复杂的问题。目前在网上搜这两个问题的解决方案大多是JPQL和Specification的方式,JPQL在动态查询上不好实现,Specification在实现的时候太麻烦,而且写出来的代码属实无法评价,也可能是我水平不够,见谅~。其他博客也是抄来抄去的,这里就不提这两种解决方案了,感兴趣的可以自行搜一下。好在现在可以搜到一些QueryDSL相关的博客了,虽然不多,但至少有人尝试新的解决方案,而不是简单的CV,拿来就用。简介
JPA2。0标准引入了一种新的类型安全的构建查询的方法,可以利用注释在预处理期间生成元模型类,通过生成的元模型类可以构建查询语句。具体可以看CriteriaQueryAPI。
QueryDSL在编译的时候会自动帮我们生成一些CriteriaQueryAPI会用到的元模型类,然后我们可以直接用这些模型类构建查询,当然QueryDSL不仅仅只有这个作用。解决问题引入依赖
在Maven的pom。xml文件中引入QueryDSL,目前使用Maven管理项目依赖还是比较多,Gradle的使用方式这里就不介绍了,Quiet用的就是Gradle,需要的话可以看下项目的具体配置,或者私信我也行。dependencygroupIdcom。querydslgroupIdquerydslaptartifactIdversion{querydsl。version}versionscopeprovidedscopedependencydependencygroupIdcom。querydslgroupIdquerydsljpaartifactIdversion{querydsl。version}versiondependencyprojectbuildpluginsplugingroupIdcom。mysema。mavengroupIdaptmavenpluginartifactIdversion1。1。3versionexecutionsexecutiongoalsgoalprocessgoalgoalsconfigurationoutputDirectorytargetgeneratedsourcesjavaoutputDirectoryprocessorcom。querydsl。apt。jpa。JPAAnnotationProcessorprocessorconfigurationexecutionexecutionspluginpluginsbuildproject复制代码注入JPAQueryFactory
在SpringBoot项目中可以注入BeanJPAQueryFactory方便查询时使用,ConfigurationpublicclassJpaAutoConfig{PersistenceContextprivatefinalEntityManagerentityManager;publicJpaAutoConfig(EntityManagerentityManager){this。entityManagerentityManager;}BeanpublicJPAQueryFactoryjpaQueryFactory(){returnnewJPAQueryFactory(entityManager);}}复制代码生成元模型类
编译项目,在开发的时候可以使用maven编译一下项目,或者直接运行项目也可以,这步主要是生成一些查询用到的元模型类。生成的模型类中我们用到类名最多的是Q{EntityName}(前缀的Q好像是可以配置的,有需要修改的话可以自己研究下),EntityName是我们的实体类的类名,比如EntitypublicclassUser{}复制代码
那么生成的元模型类的类名就是QUser。查询
以下内容中的queryFactory即上文中注入的BeanJPAQueryFactory,本文中只列举几种常用的查询方式,更多查询方式的构建可以看下官网文档(文末附有相关链接)。单表查询
简单的单表查询直接使用Repository实现即可,动态条件查询在文章后面有构建动态查询条件的方式。QCustomercustomerQCustomer。customer;CustomerbobqueryFactory。selectFrom(customer)。where(customer。firstName。eq(Bob))。fetchOne();复制代码or查询queryFactory。selectFrom(customer)。where(customer。firstName。eq(Bob)。or(customer。lastName。eq(Wilson)));复制代码查询部分字段QEmployeeemployeeQEmployee。employee;ListTupleresultqueryFactory。select(employee。firstName,employee。lastName)。from(employee)。fetch();for(Tuplerow:result){System。out。println(firstNamerow。get(employee。firstName));System。out。println(lastNamerow。get(employee。lastName));}}复制代码查询指定字段并返回指定类型使用setter方法构建查询结果QUseruserQUser。user;ListUserDTOdtosqueryFactory。select(Projections。bean(UserDTO。class,user。firstName,user。lastName))。fetch();复制代码使用字段填充的方式构建查询结果QUseruserQUser。user;ListUserDTOdtosqueryFactory。select(Projections。fields(UserDTO。class,user。firstName,user。lastName))。fetch();复制代码使用类构造方法构建查询结果QUseruserQUser。user;ListUserDTOdtosqueryFactory。select(Projections。constructor(UserDTO。class,user。firstName,user。lastName))。fetch();复制代码不同表之间joinQQuietTeamquietTeamQQuietTeam。quietTeam;QQuietTeamUserquietTeamUserQQuietTeam。quietTeamUser;jpaQueryFactory。selectFrom(quietTeam)。leftJoin(quietTeamUser)。on(quietTeam。id。eq(quietTeamUser。teamId))。where(where)。distinct()。fetch();复制代码join表取别名QCatcatQCat。cat;QCatmatenewQCat(mate);QCatkittennewQCat(kitten);queryFactory。selectFrom(cat)。innerJoin(cat。mate,mate)。leftJoin(cat。kittens,kitten)。fetch();复制代码子查询QDepartmentdepartmentQDepartment。department;QDepartmentdnewQDepartment(d);queryFactory。selectFrom(department)。where(department。size。eq(JPAExpressions。select(d。size。max())。from(d)))。fetch();复制代码QEmployeeemployeeQEmployee。employee;QEmployeeenewQEmployee(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方法,在文档中推荐了另一个开源项目:BlazePersistence引入依赖:dependencygroupIdcom。blazebitgroupIdblazepersistenceintegrationquerydslexpressionsartifactIdversion{blazepersistence。version}versionscopecompilescopedependencydependencygroupIdcom。blazebitgroupIdblazepersistenceintegrationhibernate5。6artifactIdversion{blazepersistence。version}versionscoperuntimescopedependency复制代码注入BeanCriteriaBuilderFactoryauthorlinmtConfiguration(proxyBeanMethodsfalse)publicclassJpaConfig{PersistenceUnitprivateEntityManagerFactoryentityManagerFactory;BeanScope(ConfigurableBeanFactory。SCOPESINGLETON)publicCriteriaBuilderFactorycreateCriteriaBuilderFactory(){CriteriaBuilderConfigurationconfigCriteria。getDefault();dosomeconfigurationreturnconfig。createCriteriaBuilderFactory(entityManagerFactory);}}复制代码查询数据:OverridepublicPagedListQuietUserpageUser(NotNullLongdeptId,QuietUserparams,NotNullPageablepage){BooleanBuilderbuilderSelectBuilder。booleanBuilder(params)。getPredicate();builder。and(quietDeptUser。deptId。eq(deptId));returnnewBlazeJPAQueryQuietUser(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查询
SpringDataJPA提供了很多的扩展点,QueryDSL和BlazePersistence构建的查询条件也是支持这些扩展点。在JPA中我们常用的是org。springframework。data。jpa。repository。JpaRepository相关的类作为我们Repository的父类,SpringDataJPA对QueryDSL也是专门提供了一个接口(这应该能算QueryDSL得到了官方认可了吧):org。springframework。data。querydsl。QuerydslPredicateExecutor,那么我们在使用的时候就可以定义一个项目中所有Repository共用的父接口:authorlinmtNoRepositoryBeanpublicinterfaceQuietRepositoryTextendsJpaRepositoryT,Long,QuerydslPredicateExecutorT{}复制代码
在分页查询的时候就可以使用方法:org。springframework。data。querydsl。QuerydslPredicateExecutorfindAll(com。querydsl。core。types。Predicate,org。springframework。data。domain。Pageable)
Predicate参数是构建的查询条件实体信息的父类,下面我们会有动态查询条件构建的例子。动态查询
在项目中我们常常有动态查询的需求,比如前端传了用户名,我们就需要根据用户名进行模糊查询,没有传用户名,就不添加用户名的查询条件,这种需求在SpringDataJPA中实现是比较麻烦的,这也是很多项目不选SpringDataJPA作为项目ORM框架的原因之一,这里就介绍一种比较优雅且可读性较好的方式解决这个问题。
QueryDSL提供了一种构建查询条件的实体类com。querydsl。core。BooleanBuilder,这个类实现了接口com。querydsl。core。types。Predicate,也就是文章上面提到的SpringDataJPA提供的QueryDSL扩展接口中方法的形参。QEmployeeemployeeQEmployee。employee;BooleanBuilderbuildernewBooleanBuilder();for(Stringname:names){builder。or(employee。name。equalsIgnoreCase(name));}if(id!null){builder。and(employee。id。equals(id))}queryFactory。selectFrom(employee)。where(builder)。fetch();复制代码
构建动态查询的方式不仅仅只有BooleanBuilder,所有Predicate的子类都可以:
更优雅地构建动态查询
在一些后台管理的项目中,统计需求往往会有很多的动态查询的字段,这时候可能就会出现很多的ifelse的代码,这种代码可读性就不是很好了,观察一下Q{EntityName}的字段,相同类型的字段,它们返回的类型其实都是有共同的父类的,这就很好体现了Java的三大特性之一的多态。利用这点我们新建一个构建动态查询的工具类,将动态构建的ifelse隐藏起来,工具里的方法可以根据自己项目的需要自行增删:查询条件构造器。authorlinmtpublicabstractclassSelectBuilderTextendsPredicate{NotNullpublicstaticSelectBooleanBuilderbooleanBuilder(){returnnewSelectBooleanBuilder();}NotNullpublicstaticSelectBooleanBuilderbooleanBuilder(BaseEntityentity){BooleanBuilderbuildernull;if(entity!null){builderentity。booleanBuilder();}returnnewSelectBooleanBuilder(builder);}获取查询条件return查询条件NotNullpublicabstractTgetPredicate();}复制代码构建BooleanBuilder。authorlinmtpublicclassSelectBooleanBuilderextendsSelectBuilderBooleanBuilder{privatefinalBooleanBuilderbuilder;publicSelectBooleanBuilder(){this。buildernewBooleanBuilder();}publicSelectBooleanBuilder(BooleanBuilderbuilder){this。builderbuildernull?newBooleanBuilder():builder;}OverridepublicBooleanBuildergetPredicate(){returnbuilder;}publicSelectBooleanBuilderand(NullablePredicateright){builder。and(right);returnthis;}publicSelectBooleanBuilderandAnyOf(Predicate。。。args){builder。andAnyOf(args);returnthis;}publicSelectBooleanBuilderandNot(Predicateright){returnand(right。not());}publicSelectBooleanBuilderor(NullablePredicateright){builder。or(right);returnthis;}publicSelectBooleanBuilderorAllOf(Predicate。。。args){builder。orAllOf(args);returnthis;}publicSelectBooleanBuilderorNot(Predicateright){returnor(right。not());}publicSelectBooleanBuildernotNullEq(Booleanparam,BooleanPathpath){if(param!null){builder。and(path。eq(param));}returnthis;}publicTextendsNumberComparablelt;?SelectBooleanBuildernotNullEq(Tparam,NumberPathTpath){if(param!null){builder。and(path。eq(param));}returnthis;}publicSelectBooleanBuilderisIdEq(Longparam,NumberPathLongpath){if(param!nullparam0L){builder。and(path。eq(param));}returnthis;}publicTextendsNumberComparablelt;?SelectBooleanBuilderleZeroIsNull(Tparam,NumberPathTpath){if(param!nullparam。longValue()0){builder。and(path。isNull());}returnthis;}publicSelectBooleanBuildernotBlankEq(Stringparam,StringPathpath){if(StringUtils。isNoneBlank(param)){builder。and(path。eq(param));}returnthis;}publicSelectBooleanBuilderwith(NotNullConsumerSelectBooleanBuilderconsumer){if(consumer!null){consumer。accept(this);}returnthis;}publicTextendsEnumTSelectBooleanBuildernotNullEq(Tparam,EnumPathTpath){if(param!null){builder。and(path。eq(param));}returnthis;}publicSelectBooleanBuildernotBlankContains(Stringparam,StringPathpath){if(StringUtils。isNoneBlank(param)){builder。and(path。contains(param));}returnthis;}publicSelectBooleanBuildernotNullEq(Dictdict,QDictqDict){if(dict!nullStringUtils。isNoneBlank(dict。getKey())){builder。and(qDict。eq(dict));}returnthis;}publicSelectBooleanBuildernotNullBefore(LocalDateTimeparam,DateTimePathLocalDateTimepath){if(param!null){builder。and(path。before(param));}returnthis;}publicSelectBooleanBuildernotNullAfter(LocalDateTimeparam,DateTimePathLocalDateTimepath){if(param!null){builder。and(path。after(param));}returnthis;}publicSelectBooleanBuildernotEmptyIn(Collectionlt;?extendsLongparam,NumberPathLongpath){if(CollectionUtils。isNotEmpty(param)){builder。and(path。in(param));}returnthis;}publicSelectBooleanBuilderfindInSet(Longparam,SetPathLong,NumberPathLongpath){if(param!null){builder。and(Expressions。booleanTemplate(FINDINSET({0},{1})0,param,path));}returnthis;}}复制代码使用例子OverridepublicListDocApiGrouplistByProjectIdAndName(LongprojectId,SetLongids,Stringname,Longlimit){if(Objects。isNull(projectId)){returnLists。newArrayList();}BooleanBuilderwhereSelectBooleanBuilder。booleanBuilder()。and(docApiGroup。projectId。eq(projectId))。notEmptyIn(ids,docApiGroup。id)。notBlankContains(name,docApiGroup。name)。getPredicate();JPAQueryDocApiGroupqueryjpaQueryFactory。selectFrom(docApiGroup)。where(where);if(limit!nulllimit0){query。limit(limit);}returnquery。fetch();}复制代码结语
这篇文章主要是介绍一些比较常用的内容,QueryDSL是基于SQL标准实现了SQL语句的构建,对于不同类型的数据库(MySQL、Oracle等)具有的特性,就需要自己去构建查询方式了,比如上面的findInSet就是MySQL特有的函数,不在SQL标准中,所以要真正用好的话学习成本确实有点高,我也只是了解一点而已。BlazePersistence也是一个很不错的开源项目,目前我也只是把它当成QueryDSL的补充,但其实它也提供了很多查询条件的构建方式,感兴趣的可以自行深入研究哈~最后,再附上相关链接
QueryDSL官网:querydsl。com
QueryDSLGithub:github。comquerydslqu
BlazePersistence官网:persistence。blazebit。comindex。html
BlazePersistenceGithub:github。comBlazebitbl
QueryDSL文档:querydsl。comstaticquer
BlazePersistence文档:persistence。blazebit。comdocumentati
文档的链接带有版本号,目前是最新的(文章发布时间:20230310),本文就不实时更新这个链接了,后续需要最新的文档可以到官网查询哈。