Quiet 项目简介:/post/717122…

上一篇:

完成在 Markdown 编辑器中图片的上传和缩放

前言

在 ORM 结构的选择范围内,一直在评论两个工具 Spring Data JPAMyBatis,两边的争论各执一词,这儿不去争论这些东西,不同的需求、不同的场景选用不同的处理方案是很正常的,孰优孰劣并没有万金油的答案。在这篇文章中咱们来切实在实地处理 Spring Data JPA 中连表查询和动态查询完成杂乱的问题。现在在网上搜这两个问题的处理方案大多是 JPQLSpecification 的办法,JPQL 在动态查询上欠好完成,Specification 在完成的时分太费事,并且写出来的代码属实无法评价,也可能是我水平不够,见谅~。其他博客也是抄来抄去的,这儿就不提这两种处理方案了,感兴趣的能够自行搜一下。好在现在能够搜到一些 QueryDSL 相关的博客了,尽管不多,但至少有人测验新的处理方案,而不是简略的 CV,拿来就用。

简介

JPA 2.0 规范引进了一种新的类型安全的构建查询的办法,能够运用注释在预处理期间生成元模型类,通过生成的元模型类能够构建查询句子。详细能够看 Criteria Query API。

QueryDSL 在编译的时分会自动帮咱们生成一些 Criteria Query API 会用到的元模型类,然后咱们能够直接用这些模型类构建查询,当然 QueryDSL 不仅仅只要这个效果。

处理问题

引进依赖

Mavenpom.xml 文件中引进 QueryDSL,现在运用 Maven 办理项目依赖还是比较多,Gradle 的运用办法这儿就不介绍了,Quiet 用的便是 Gradle,需求的话能够看下项目的详细装备,或许私信我也行。

 <dependency>
  <groupId>com.querydsl</groupId>
  <artifactId>querydsl-apt</artifactId>
  <version>${querydsl.version}</version>
  <scope>provided</scope>
</dependency>
<dependency>
  <groupId>com.querydsl</groupId>
  <artifactId>querydsl-jpa</artifactId>
  <version>${querydsl.version}</version>
</dependency> 
<project>
  <build>
  <plugins>
    <plugin>
      <groupId> com.mysema.maven </groupId>
      <artifactId> apt-maven-plugin </artifactId>
      <version> 1.1.3 </version>
      <executions>
        <execution>
          <goals>
            <goal> process </goal>
          </goals>
          <configuration>
            <outputDirectory> target/generated-sources/java </outputDirectory>
            <processor> com.querydsl.apt.jpa.JPAAnnotationProcessor </processor>
          </configuration>
        </execution>
      </executions>
    </plugin>
  </plugins>
  </build>
</project> 

注入 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<Tuple> 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<UserDTO> dtos = queryFactory.select(
    Projections.bean(UserDTO.class, user.firstName, user.lastName)).fetch();
  • 运用字段填充的办法构建查询成果
QUser user = QUser.user;
List<UserDTO> dtos = queryFactory.select(
    Projections.fields(UserDTO.class, user.firstName, user.lastName)).fetch();
  • 运用类构造办法构建查询成果
QUser user = QUser.user;
List<UserDTO> dtos = queryFactory.select(
    Projections.constructor(UserDTO.class, user.firstName, user.lastName)).fetch();

不同表之间 join

QQuietTeam 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

  1. 引进依赖:
<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-querydsl-expressions</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-hibernate-5.6</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>runtime</scope>
</dependency>
  1. 注入 Bean CriteriaBuilderFactory
/**
 * @author <a href="mailto:lin-mt@outlook.com">lin-mt</a>
 */
@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);
  }
}
  1. 查询数据:
  @Override
  public PagedList<QuietUser> pageUser(
      @NotNull Long deptId, QuietUser params, @NotNull Pageable page) {
    BooleanBuilder builder = SelectBuilder.booleanBuilder(params).getPredicate();
    builder.and(quietDeptUser.deptId.eq(deptId));
    return new BlazeJPAQuery<QuietUser>(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 供给了许多的扩展点,QueryDSLBlaze-Persistence 构建的查询条件也是支撑这些扩展点。在 JPA 中咱们常用的是 org.springframework.data.jpa.repository.JpaRepository 相关的类作为咱们 Repository 的父类,Spring Data JPAQueryDSL 也是专门供给了一个接口(这应该能算 QueryDSL 得到了官方认可了吧):org.springframework.data.querydsl.QuerydslPredicateExecutor,那么咱们在运用的时分就能够定义一个项目中所有 Repository 共用的父接口:

/**
 * @author <a href="mailto:lin-mt@outlook.com">lin-mt</a>
 */
@NoRepositoryBean
public interface QuietRepository<T> extends JpaRepository<T, Long>, QuerydslPredicateExecutor<T> {}

在分页查询的时分就能够运用办法: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 的子类都能够:

在 Spring Data JPA 中,优雅实现动态查询和连表查询

更高雅地构建动态查询

在一些后台办理的项目中,统计需求往往会有许多的动态查询的字段,这时分可能就会出现许多的 if-else 的代码,这种代码可读性就不是很好了,调查一下 Q${EntityName} 的字段,相同类型的字段,它们回来的类型其实都是有一起的父类的,这就很好表现了 Java 的三大特性之一的多态。运用这点咱们新建一个构建动态查询的工具类,将动态构建的 if-else 躲藏起来,工具里的办法能够依据自己项目的需求自行增删:

/**
 * 查询条件构造器.
 *
 * @author <a href="mailto:lin-mt@outlook.com">lin-mt</a>
 */
public abstract class SelectBuilder<T extends Predicate> {
  @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 <a href="mailto:lin-mt@outlook.com">lin-mt</a>
 */
public class SelectBooleanBuilder extends SelectBuilder<BooleanBuilder> {
  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 <T extends Number & Comparable<?>> SelectBooleanBuilder notNullEq(
      T param, NumberPath<T> path) {
    if (param != null) {
      builder.and(path.eq(param));
    }
    return this;
  }
  public SelectBooleanBuilder isIdEq(Long param, NumberPath<Long> path) {
    if (param != null && param > 0L) {
      builder.and(path.eq(param));
    }
    return this;
  }
  public <T extends Number & Comparable<?>> SelectBooleanBuilder leZeroIsNull(
      T param, NumberPath<T> 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<SelectBooleanBuilder> consumer) {
    if (consumer != null) {
      consumer.accept(this);
    }
    return this;
  }
  public <T extends Enum<T>> SelectBooleanBuilder notNullEq(T param, EnumPath<T> 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<LocalDateTime> path) {
    if (param != null) {
      builder.and(path.before(param));
    }
    return this;
  }
  public SelectBooleanBuilder notNullAfter(LocalDateTime param, DateTimePath<LocalDateTime> path) {
    if (param != null) {
      builder.and(path.after(param));
    }
    return this;
  }
  public SelectBooleanBuilder notEmptyIn(Collection<? extends Long> param, NumberPath<Long> path) {
    if (CollectionUtils.isNotEmpty(param)) {
      builder.and(path.in(param));
    }
    return this;
  }
  public SelectBooleanBuilder findInSet(Long param, SetPath<Long, NumberPath<Long>> path) {
    if (param != null) {
      builder.and(Expressions.booleanTemplate("FIND_IN_SET({0}, {1}) > 0", param, path));
    }
    return this;
  }
}

运用比方

@Override
public List<DocApiGroup> listByProjectIdAndName(Long projectId, Set<Long> 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<DocApiGroup> 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-02-01),本文就不实时更新这个链接了,后续需求最新的文档能够到官网查询哈。

下一篇

如安在 Spring Data JPA 中完美处理自定义枚举内容与数据库类型的自动转换