在 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
复制代码
在 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();
复制代码
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));
}}
复制代码
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();
复制代码
QQuietTeam quietTeam = QQuietTeam.quietTeam;
QQuietTeamUser quietTeamUser = QQuietTeam.quietTeamUser;
jpaQueryFactory
.selectFrom(quietTeam)
.leftJoin(quietTeamUser)
.on(quietTeam.id.eq(quietTeamUser.teamId))
.where(where)
.distinct()
.fetch();
复制代码
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
复制代码
/**
* @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());
}
复制代码
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),本文就不实时更新这个链接了,后续需要最新的文档可以到官网查询哈。
页面更新:2024-05-14
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号