分类
persistence

Querydsl 简介

Intro to Querydsl

1. 概述

Querydsl是一个功能强大的数据持久化API,本文将围绕其进行基本的介绍。本文力争达到如下效果:能够将Querydsl添加到项目中;了解如何自动生成查询类以及为何要生成它们;了解如何使用Querydsl编写类型安全的数据库查询语句。

2. 为什么要用Querydsl

对象关系映射框架是企业JAVA的核心。它的出现使得应用面向对象的思想来操作数据库成为了可能。得益于对象关系映射,开发者们能够使加简洁、更加面向对象的代码来操作数据了。

类型安全:能够在编译阶段发现查询语法错误;类型不安全:只有在程序运行时才能哆发现查询语法错误。

但是如果使用代码创建类型安全的查询,还仍然面临如何选择ORM框架的困难。

Hibernate是一款被广泛应用的ORM框架,其提供了一种与SQL语言非常相近的语言HQL(JPQL)。这种方法的明显缺点是无法保证类型安全以及在编译阶段对检查静态查询是否合法。在更复杂的情况下(例如,当需要根据某些条件在运行时构造查询时),构建HQL查询通常涉及字符串的拼接,这种拼接操作通常非常的不安全且容易出错。

由于在拼接过程中可能导致最终的HQL语法错误,而对于编译器而言最终错误的HQL就是一般的字符串,编译器无法确认最终拼接后的HQL字符串,当然也没有检查某个字符串是否为正确的HQL语法的能力。所以我们说HQL这种基于字符串的特性,是类型不安全的。

JPA 2.0对此进行了改进,引入了Criteria Query API。其在处理过程中引用了注解,并做为一种全新的构建查询方式出现。但基于JAVA无法读取某个类中的属性的属性名的特性(比如我们无法获取Student类中的name属性的属性名为'name'),使用 Criteria Query API构造的查询显得十分的臃肿。比如官方给出以下代码,其作用仅仅是为了实现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应运而生,旨在提从一种更流畅、更易读、更易维护的查询方式。

3. Querydsl生成类

从根本上讲Criteria Query之所以无法达到类型安全,是由于我们无法在运行时获取一个类的元数据。比如某实体类中有以下属性:

class Student {
  private String name;

该name字段将对应数据表中的name字段,如若在Criteria Query实现根据name字段进行查询,那么代码如下:

root.get('name')

而上述代码的'name'是个普通的字符串,如果此时我们不小心输入为root.get(“nane”)也同样不会被编译器发现。而明显的由于数据表中并不存在nane字段,所以在运行时将报一个异常。

这并不是我们所期待并希望能够规避的。我们更希望当发生上述拼写错误时编译器能够快速的提示我们,所以我们更期望能够使用类似的Student.name来替换不可靠的"name"字符串。但是由于JAVA的机制,我们无法像Student.name一样来获取Student类中的name属性这个元数据。

接下来,让我们一步步学习Querydsl是如何解决上述问题的。

3.1 添加依赖

使用Querydsl需要添加querydsl-apt以及querydsl-jpa两个依赖,两个依赖在使用时要求版本相统一。为了避免由于输入问题造成的版本不统一的问题,我们在pom.xml中首先定义一个querydsl.version变量,并赋值为 4.3.1(更多版本请查看MAVEN 官方仓库):

<properties>
    <querydsl.version>4.1.3</querydsl.version>
</properties>

然后加入依赖如下:

    <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>

querydsl-apt是一个注释处理器annotation processing tool,简称APT,APT可以在编译之前对源码文件进行检测并处理特定的注解。querydsl-apt会根据扫描特定注解下的实体类,并生成与其对应生成Q类型(以Q打头)Q- type类。例如,如果您的应用程序类中有一个标有@Entity注解的User类,则querydsl-apt将在编译以前在User类的当前位置上为其对应生成QUser类。

<scope>provided</scope>的作用是声明querydsl-apt依赖仅仅在项目编译、测试的时候生效。

querydsl-jpa即为Querydsl,-jpa后缀意在表明该依赖应当与标准的JPA一起使用。

若要使querydsl-apt生效,则需要在pom.xml中添加以下代码:

	<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>

该插件的作用是在执行maven build时自动生成Q类型。outputDirectory属性指定了Q类型Q-type源文件的生成位置 。我们可以此文件夹中查看某个Q类型Q-type。

大多流行的IDE均能够自动实识outputDirectory设置路径中的Q类型文件。如果你使用的编辑器幸运的无法自动识别outputDirectory下生成的Q类型源文件的话,那么首先尝试在IDE的官文文档的找到答案,或者借助一些搜索工具来解决这个问题。

本文中以演示为目的,创建了课程Course班级Klass学生Student三个实体,学生Student实体如下:

/**
 * 学生
 */
@Entity
public class Student {
    @Id
    @GeneratedValue
    private Long id;

    /**
     * 姓名
     */
    private String name;

    /**
     * 学号
     */
    private String no;

    /**
     * 体重
     */
    private Integer weight;

    /**
     * 所在班级
     */
    @ManyToOne
    private Klass klass;

    /**
     * 拥有的课程
     */
    @ManyToMany
    private List<Course> courses = new ArrayList<>();

    // 省略构造函数及setter/getter

生成实体类对应的Q类型Q-type类,仅需要执行:

mvn compile

3.2 查看生成的类

执行完mvn compile便可以在apt-maven-plugin插件的outputDirectory属性设置的路径中找到对应生的Q类型Q-type类了。找开我们设置的target/generated-sources/java路径,将查看到QCourse.java、QKlass.java、QStudent.java三个类文件。

QStudent.java类将做为所有以Student实体为根查询的入口。打开此文件将会看到类拥有@Generated注解,这代表着:该文件是自动生成而非手动编辑的;如果生成该类依赖的文件发生变化,则必须重新运行mvn compile来更新此类。

观察该文件会发现此类除了存在几个QStudent构造函数以外,还存在与Student实体对应的被声明为final类型的属性。属性中还存在一个QStudent类型:

    public static final QStudent student = new QStudent("student");

除一些join的关联查询外,我们可以使用此属性来完成大多数有关Student实体的查询。

继续观察该类我们还发现实体类的每个字段在Q类型中都有一个对应的* Path字段,例如QStudent类中的NumberPath idStringPath nameListPath courses,分别对应Student类中的 Long id, String name, List courses。稍后我们将在查询中使用到它们。

4. 小试牛刀

4.1 一般查询

我们需要一个JPAQueryFactory的实例用于构造查询。该实例可以通过EntityManager获取到。在JPA应用中,可以通过调用 EntityManagerFactory.createEntityManager() 或使用@PersistenceContext注解来获取到EntityManager

    @PersistenceContext
    EntityManager entityManager;

下面,我们开始创建第一个查询:

    /**
     * 查询姓名为X的学生
     * @param name 学生姓名
     * @return
     */
    Student findByName(String name) {
        JPAQueryFactory query = new JPAQueryFactory(entityManager); // ❶
        QStudent qStudent = QStudent.student; // ❷

        return query.selectFrom(qStudent)  // ❸
             .where( // ❹
                 qStudent.name.eq(name))  // ❺
             .fetchOne(); // ❻
    }
  • ❶ 通过EntityManager获取了一个JPAQueryFactory实例。
  • ❷ 直接引用静态变量QStudent.student.
  • ❸ selectFrom开始构造查询,并指定查询的实体为qStudent对应的Student。
  • ❹ where方法用于指定查询条件。
  • ❺ qStudent的name属性的类型为StringPath,该类型上的eq()方法用于构建“等于”条件。本行代码表示:Student实体中的 name字段与传入的name参数相同的查询条件。
  • ❻ 根据查询条件来获取某个数据。如果未找到相应的数据,则返回null,找到唯一符合要求的数据则返回该数据,如果找到了多条符合要求的数据,则抛出NonUniqueResultException

4.2 排序orderBy与分组grouping

可以使用orderBy方法来轻构的完成排序:

query.selectFrom(qStudent)
                .orderBy(qStudent.weight.asc())
                .fetch();

如下所示*Path除拥有eq()方法以外,还拥有asc()、desc()两个用于排序的方法。上述代码将按学生的体重weigth进行升序排列。

此外,还可以很轻松的实现较为复杂的查询:

    /**
     * 按体重group、按人数排序
     *
     * @return
     */
    public List<Tuple> groupByWeightAndOrderById() {
        JPAQueryFactory query = new JPAQueryFactory(entityManager);
        QStudent qStudent = QStudent.student;
        
        NumberPath<Long> count = Expressions.numberPath(Long.class, "c");

        return query.select(qStudent.weight, qStudent.id.count().as(count))
                    .from(qStudent)
                    .groupBy(qStudent.weight)
                    .orderBy(count.desc())
                    .fetch();
    }

上述代码按学生的体重进行分组的同时,还使用了自定义NumberPath来计算了每种体重的人数。

4.3 关联查询Joins与子查询Subqueries

可以使用innerJoin()以及on()方法来快带的完成关联join查询。比询出所有排有某门课程的学生:

    public List<Student> findAllByCourseName(String courseName) {
        JPAQueryFactory query = new JPAQueryFactory(entityManager);
        QStudent qStudent = QStudent.student;
        QCourse qCourse = QCourse.course;

        return query.selectFrom(qStudent)
                    .innerJoin(qStudent.courses, qCourse)
                    .on(qCourse.name.endsWith(courseName))
                    .fetch();
    }

当然,也支持子查询:

    /**
     * 子查询
     */
    public List<Student> findAllByKlassName(String klassName) {
        JPAQueryFactory query = new JPAQueryFactory(entityManager);
        QStudent qStudent = QStudent.student;
        QKlass qKlass = QKlass.klass;

        return query.selectFrom(qStudent)
                    .where(qStudent.klass.id.in(
                            JPAExpressions.select(qKlass.id)
                                          .from(qKlass)
                                          .where(qKlass.name.eq(klassName))
                    )).fetch();
    }

观察如上代码不难得到:使用Querydsl编写的子查询非常的清爽、易读。在使用子查询时,应该调用in()方法,并在该方法中传入JPAExpressions中的相关方法。

4.4 更新、删除数据

JPAQueryFactory不仅仅能够用于数据查询,还可以使用其进行数据的更新或删除:

query.update(qStudent)
     .where(qStudent.name.eq(name))
     .set(qStudent.no, no)
     .execute();

以下代码实现了按学生姓名更新学生学号的功能。其中set()实现了对某个字符赋新值的功能。上述方法中的where()方法是个选填项,如果在上述代码中去除where()方法,也不会发生语法错误,但是功能却变更为:更新数据表中的所以学生的学号。

如果我们想根据学生姓名删除某个学生,则可以这么写:

query.delete(qStudent)
     .where(qStudent.name.eq(name))
     .execute();

同更新数据中的where()方法一样,删除数据时where()方法同样可以移除。但在实际的使用过程中,应该避免这样移除where(),除非你想清空某个数据表中的所有数据。

此时你心里可能在想:Querydsl既然有了查询、更新、删除方法,那么应该还存在插入方法。很遗憾的说,受限于JPA查询规范的限制,底层的javax.persistence.Query.executeUpdate() 并不拥有插入的功能,所以若想要插入数据则仍然需要调用EntityManager。

如果说你对Querydsl情有独钟,数据的插入操作也想使用Querydsl来完成,那么你可以选择使用SQLQueryFactory。该类位于querydsl-sql库中。

5. 总结

本文中我们围绕功能强大、类型安全的Querydsl展开了讨论。Querydsl在执行查询的过程中,首先根据实体生成了Q类型的类,然后在接下来的查询中充分的调用Q类型,从而达到了类型安全type-safe的目的。

在本文的最后给出应用Querydsl进行数据更新、删除,以致于查询数据的方法。其实Querydsl功能远不止如此,希望在以后的文章中能够有机会与大家继续深入讨论。

此外,我们还一如既往的为大家准备了完整的示例代码,希望能对你有所帮助。