1. Querydsl 的目的

对象关系映射(ORM)框架是企业级 Java 应用的核心。它们弥补了面向对象方法与关系数据库模型之间的差异,让开发者能编写更简洁、更清晰的持久化代码和领域逻辑。

然而,ORM 框架最难的设计之一就是如何构建正确且类型安全的查询 API。最常用的 Java ORM 框架 Hibernate(及其紧密相关的 JPA 标准)提出了基于字符串的查询语言 HQL(JPQL),它与 SQL 非常相似。这种方法的明显缺陷是缺乏类型安全性和静态查询检查。

在更复杂的情况下(例如查询需要根据某些条件在运行时动态构建),构建 HQL 查询通常涉及字符串拼接,这既不安全又容易出错。

JPA 2.0 标准引入了 Criteria Query API 作为改进方案——这是一种利用注解预处理期间生成的元模型类的新型类型安全查询方法。可惜的是,尽管本质上是开创性的,Criteria API 最终变得非常冗长且难以阅读。以下是从 Jakarta EE 教程中摘取的生成简单查询 SELECT p FROM Pet p 的示例:

EntityManager em = ...;
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Pet> cq = cb.createQuery(Pet.class);
Root<Pet> pet = cq.from(Pet.class);
cq.select(pet);
TypedQuery<Pet> q = em.createQuery(cq);
List<Pet> allPets = q.getResultList();

难怪很快出现了更强大的 Querydsl 库,它同样基于生成的元数据类理念,但实现了流畅且可读的 API。

2. Querydsl 类生成

让我们从生成和探索那些构成 Querydsl 流畅 API 基础的神奇元类开始。

2.1. 将 Querydsl 添加到 Maven 构建

在项目中集成 Querydsl 只需在构建文件中添加几个依赖项,并配置一个用于处理 JPA 注解的插件。首先,在 pom.xml<project><properties> 部分提取 Querydsl 库版本到单独的属性(最新版本可查阅 Maven Central):

<properties>
    <querydsl.version>5.0.0</querydsl.version>
</properties>

接下来,在 pom.xml<project><dependencies> 部分添加以下依赖:

<dependencies>

    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-apt</artifactId>
        <version>${querydsl.version}</version>
        <classifier>jakarta</classifier>
        <scope>provided</scope>
    </dependency>

    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-jpa</artifactId>
        <classifier>jakarta</classifier>
        <version>${querydsl.version}</version>
    </dependency>

</dependencies>

querydsl-apt 是一个注解处理工具(APT)——实现了相应的 Java API,允许在源文件进入编译阶段前处理其中的注解。该工具生成所谓的 Q-type 类:这些类与应用中的实体类直接对应,但以字母 Q 作为前缀。例如,如果应用中有一个带有 @Entity 注解的 User 类,生成的 Q-type 将位于 QUser.java 源文件中。

querydsl-aptprovided 作用域表示该 JAR 只在构建时可用,不会打包到应用构件中。

querydsl-jpa 库是 Querydsl 本身,专为与 JPA 应用配合使用而设计。

要配置利用 querydsl-apt 的注解处理插件,在 pom.xml<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>

该插件确保在 Maven 构建的 process 目标期间生成 Q-type。outputDirectory 配置属性指定 Q-type 源文件的生成目录,稍后探索 Q 文件时会用到这个值。

如果 IDE 没有自动将该目录添加到项目源文件夹,请手动添加——查阅你常用 IDE 的文档了解操作方法。

本文使用一个简单的博客服务 JPA 模型,包含 Users 及其 BlogPosts,两者之间是一对多关系:

@Entity
public class User {

    @Id
    @GeneratedValue
    private Long id;

    private String login;

    private Boolean disabled;

    @OneToMany(cascade = CascadeType.PERSIST, mappedBy = "user")
    private Set<BlogPost> blogPosts = new HashSet<>(0);

    // getters and setters

}

@Entity
public class BlogPost {

    @Id
    @GeneratedValue
    private Long id;

    private String title;

    private String body;

    @ManyToOne
    private User user;

    // getters and setters

}

为模型生成 Q-type,只需执行:

mvn compile

2.2. 探索生成的类

现在进入 apt-maven-plugin 的 outputDirectory 属性指定的目录(本例中为 target/generated-sources/java)。你会看到一个直接反映领域模型的包和类结构,只是所有类都以字母 Q 开头(本例中为 QUserQBlogPost)。

打开 QUser.java 文件。这是构建所有以 User 为根实体的查询的入口点。首先会注意到 @Generated 注解,表明该文件是自动生成的,不应手动编辑。如果修改了任何领域模型类,需要再次运行 mvn compile 重新生成对应的 Q-type。

除了几个 QUser 构造函数外,还应关注一个 QUser 类的公共静态 final 实例:

public static final QUser user = new QUser("user");

这是在大多数针对该实体的 Querydsl 查询中使用的实例,除非需要编写更复杂的查询(例如在单个查询中连接同一张表的多个实例)。

最后要注意的是,实体类的每个字段在 Q-type 中都有对应的 *Path 字段,如 QUser 类中的 NumberPath idStringPath loginSetPath blogPosts(注意对应 Set 的字段名使用了复数形式)。这些字段将用于稍后介绍的流畅查询 API。

3. 使用 Querydsl 查询

3.1. 简单查询和过滤

构建查询需要先获取 JPAQueryFactory 实例,这是启动构建过程的首选方式。JPAQueryFactory 只需要一个 EntityManager,在 JPA 应用中通常通过 EntityManagerFactory.createEntityManager() 调用或 @PersistenceContext 注入获得:

EntityManagerFactory emf = 
  Persistence.createEntityManagerFactory("com.baeldung.querydsl.intro");
EntityManager em = entityManagerFactory.createEntityManager();
JPAQueryFactory queryFactory = new JPAQueryFactory(JPQLTemplates.DEFAULT, em);

现在创建第一个查询:

QUser user = QUser.user;

User c = queryFactory.selectFrom(user)
  .where(user.login.eq("David"))
  .fetchOne();

我们定义了局部变量 QUser user 并用 QUser.user 静态实例初始化。这纯粹为了简洁,也可以静态导入 QUser.user 字段。

JPAQueryFactoryselectFrom 方法开始构建查询。传入 QUser 实例后,通过 .where() 方法继续构建查询条件。user.login 是之前见过的 QUser 类的 StringPath 字段引用。StringPath 对象还提供 .eq() 方法,通过指定字段相等条件流畅地继续构建查询。

最后,调用 fetchOne() 方法将数据库中的值提取到持久化上下文。如果找不到对象,该方法返回 null;如果有多个实体满足 .where() 条件,则抛出 NonUniqueResultException

3.2. 排序和分组

现在获取所有用户列表,按登录名升序排序:

List<User> c = queryFactory.selectFrom(user)
  .orderBy(user.login.asc())
  .fetch();

这种语法之所以可行,是因为 *Path 类提供了 .asc().desc() 方法。也可以为 .orderBy() 方法指定多个参数以按多字段排序。

尝试更复杂的操作:假设需要按标题对所有文章分组并统计重复标题数量。这需要使用 .groupBy() 子句,同时希望按结果计数对标题排序:

NumberPath<Long> count = Expressions.numberPath(Long.class, "c");

List<Tuple> userTitleCounts = queryFactory.select(
  blogPost.title, blogPost.id.count().as(count))
  .from(blogPost)
  .groupBy(blogPost.title)
  .orderBy(count.desc())
  .fetch();

我们选择了文章标题和重复计数,按标题分组后按聚合计数排序。注意首先在 .select() 子句中为 count() 字段创建别名,因为需要在 .orderBy() 子句中引用它。

3.3. 复杂查询:连接和子查询

查找所有写过标题为 "Hello World!" 文章的用户。这类查询可以使用内连接。注意为连接表创建了别名 blogPost,以便在 .on() 子句中引用:

QBlogPost blogPost = QBlogPost.blogPost;

List<User> users = queryFactory.selectFrom(user)
  .innerJoin(user.blogPosts, blogPost)
  .on(blogPost.title.eq("Hello World!"))
  .fetch();

现在尝试用子查询实现相同目标:

List<User> users = queryFactory.selectFrom(user)
  .where(user.id.in(
    JPAExpressions.select(blogPost.user.id)
      .from(blogPost)
      .where(blogPost.title.eq("Hello World!"))))
  .fetch();

子查询与主查询非常相似且可读性强,但以 JPAExpressions 工厂方法开头。要连接子查询与主查询,一如既往地引用之前定义和使用的别名。

3.4. 修改数据

JPAQueryFactory 不仅支持构建查询,还能修改和删除记录。修改用户登录名并禁用账户:

queryFactory.update(user)
  .where(user.login.eq("Ash"))
  .set(user.login, "Ash2")
  .set(user.disabled, true)
  .execute();

可以为不同字段添加任意数量的 .set() 子句。.where() 子句不是必需的,因此可以一次性更新所有记录。

删除匹配特定条件的记录,可以使用类似语法:

queryFactory.delete(user)
  .where(user.login.eq("David"))
  .execute();

.where() 子句同样不是必需的,但要小心——省略它会导致删除指定类型的所有实体。

你可能会好奇,为什么 JPAQueryFactory 没有 .insert() 方法?这是 JPA Query 接口的限制。底层的 jakarta.persistence.Query.executeUpdate() 方法只能执行更新和删除语句,不能执行插入语句。要插入数据,只需使用 EntityManager 持久化实体。

如果仍想使用类似的 Querydsl 语法插入数据,应使用 querydsl-sql 库中的 SQLQueryFactory 类。

4. 结论

本文介绍了 Querydsl 提供的强大类型化 API,用于持久化对象操作。

我们学习了如何将 Querydsl 添加到项目并探索生成的 Q-type,还覆盖了典型用例,体验了其简洁性和可读性。

所有示例源代码可在 GitHub 仓库 中找到。

当然,Querydsl 还提供更多功能,包括原生 SQL 操作、非持久化集合、NoSQL 数据库和全文搜索——我们将在后续文章中探索其中部分特性。


原始标题:Intro to Querydsl