简介:Querydsl 与 JPA 标准
我使用 JPA Criteria 很多年了。主要优点是它的类型安全方法(与 JPQL 相比),但代码相当复杂且并不总是直截了当。最近,我遇到了一个Querydsl 框架,我立即看到了它的好处。对我来说主要的好处是:
类型安全的方法。
具有 SQL 风格的 Fluent API——代码看起来很像 SQL 查询。
不限于 JPA 技术——还有用于其他技术的模块。
本系列的目的是阐明 Querydsl 框架并将其与 JPA Criteria 进行比较。主要目标是展示一些标准案例中的差异。这个系列计划有这些部分:
简介(本文)
构建查询
预测
采集模块
Lucene模块
在本文中,您将学习:
什么是 Querydsl
如何为 Spring Data JPA 和 Querydsl 设置项目
如何在 Spring Data JPA 中使用不同类型的查询
什么是 Querydsl?
Querydsl 是一个以多种技术提供类型安全查询的框架。本系列的主要重点是 JPA 技术。域名站点使用此定义:
Querydsl 诞生于以类型安全的方式维护 HQL 查询的需要。HQL 查询的增量构造需要字符串连接并导致代码难以阅读。通过纯字符串对域类型和属性的不安全引用是基于字符串的 HQL 构造的另一个问题。
第五版中的 Querydsl 包含以下模块:
JPA — SQL 持久化的流行选择,侧重于 CRUD 和对象加载的简单查询。
SQL — 另一种 SQL 抽象,侧重于 SQL 操作和 SQL 标准的完整表达能力。
MongoDB — 通过 Morphia 或 Spring Data 为 MongoDB 提供 ODM 支持,这是许多人选择的 NoSQL。
JDO — JDO 支持对象、SQL 和 NoSQL 存储抽象。
Lucene — 通过 Lucene 的全文索引,最流行的 Java 全文索引。
集合——查询 Java Beans 和 POJO 的 Java 集合。
项目设置
关于使用Querydsl 的文章(包括官网)有很多。我建议检查这些链接:
查询 DSL
Spring Data JPA + QueryDSL:两全其美
高级 Spring Data JPA 规范和 QueryDSL
本文以 JPA Criteria 和 Querydsl 用法的快速演示开始本系列。
注意:本系列中的所有示例都使用 Lombok 来简化和减少代码。
Maven 配置
春季数据 JPA
为了开始使用 JPA 标准,spring-boot-starter-data-jpa必须添加依赖项,域名如下所示。可以在Maven 中央存储库中找到最新的可用版本。但是,无需定义确切的版本。我们可以使用 Spring Boot 定义的版本(在我们的例子中)。
<dependency> <groupId>域名</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency>
查询DSL
除此之外,我们还需要将依赖项和querydsl-jpa插件添加到我们的 Maven 项目 ( ) 中以启用 Querydsl 功能。我们不需要关心 Spring Boot 定义的版本(请参阅属性)。最新版本可以在Maven 中央存储库中找到。querydsl-aptapt-maven-域名${域名ion}
<dependency> <groupId>域名ydsl</groupId> <artifactId>querydsl-apt</artifactId> <version>${域名ion}</version> <scope>provided</scope> </dependency> <dependency> <groupId>域名ydsl</groupId> <artifactId>querydsl-jpa</artifactId> <version>${域名ion}</version> </dependency> ... <plugin> <groupId>域名n</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>域名.域名nnotationProcessor</processor> </configuration> </execution> </executions> </plugin>
注意:这些工件仅适用于 JPA Criteria 和 Querydsl。我们可能需要更多依赖项(例如,对于 Spring Data JPA、Lombok、Liquibase 等),但本文的目的不是提供完整的设置。
数据库
在本文以及可能的整个系列中,我们使用一个简单的国家/地区域。
注意:我们不需要复杂的域模型来演示 Querydsl 功能。
国家域名
Country域包含和表。他们的关系如下图所示。countrycity
初始化
所有数据库更改都通过Liquibase实现。我们通过添加以下更改日志从数据库表定义 (DDL) 开始:
databaseChangeLog: - changeSet: id: DDL author: 域名lka changes: - createTable: tableName: country columns: - column: name: id type: int autoIncrement: true constraints: primaryKey: true nullable: false - column: name: name type: varchar(255) constraints: nullable: false - createTable: tableName: city columns: - column: name: id type: int autoIncrement: true constraints: primaryKey: true nullable: false - column: name: name type: varchar(255) constraints: nullable: false - column: name: state type: varchar(255) constraints: nullable: true - column: name: country_id type: varchar(255) constraints: nullable: false addForeignKeyConstraint: - baseColumnNames: country_id - baseTableName: country - constraintName: city_fk1 - deferrable: false - initiallyDeferred: false - onDelete: RESTRICT - onUpdate: RESTRICT - referencedColumnNames: id - referencedTableName: country - validate: true
接下来,我们需要用这个变更日志填充一些数据(DML):
\0
databaseChangeLog: - changeSet: id: Init data author: 域名lka changes: - insert: tableName: country columns: - column: name: name value: Australia - insert: tableName: city columns: - column: name: name value: Brisbane - column: name: state value: Queensland - column: name: country_id value: "1" - insert: tableName: city columns: - column: name: name value: Melbourne - column: name: state value: Victoria - column: name: country_id value: "1"
注意:可以在此处找到开始使用 Liquibase 的快速教程。
JPA 配置
映射为CountryJPACity实体。
如上所述,我们使用Spring Data JPA 依赖项。有了这个,我们需要从国家域为我们的数据库表创建实体和存储库。
注意:Country表用于Querydsl的演示,City表用于JPA Criteria。
实体映射
为表创建一个Country实体COUNTRY:
@Entity @Data @Builder @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode public class Country implements Serializable { @Id @GeneratedValue(strategy = 域名TITY) private Long id; @Column(nullable = false) private String name; @OneToMany(mappedBy = "country", cascade = { 域名IST, 域名E }) @域名ude private List<City> cities; }
因此,为表添加一个City实体CITY为:
@Entity @Data @Builder @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode public class City implements Serializable { @Id @GeneratedValue(strategy = 域名TITY) private Long id; @Column(nullable = false) private String name; @Column(nullable = true) private String state; @ManyToOne @JoinColumn(name = "COUNTRY_ID") @NonNull private Country country; }
资料库
我们项目设置中的最后一部分是创建Spring Data JPA存储库。有了它们,我们可以通过非常简单的实现来访问和管理我们的数据。
注意:您可以在我之前的文章《Spring Data Elasticsearch 4.1 介绍》中找到有关 Spring Data 使用的更多详细信息。
命名查询
我们的第一个存储库由接口表示CountryRepository,它JpaRepository使用这些类型扩展了接口:
Country— 存储库管理的 JPA 实体
Long— 主键的类型
现在我们可以添加一些查询方法:
getByName— 一种获取与确切名称匹配的实体的一个实例的方法
findByNameLikeIgnoreCase— 一种查找与属性的不区分大小写like表达式匹配的所有实体的方法name。
存储库CountryRepository为实体定义Country为:
public interface CountryRepository extends JpaRepository<Country, Long> { Country getByName(String name); List<Country> findByNameLikeIgnoreCase(String name); }
同样,CityRepository存储库为实体定义City为:
public interface CityRepository extends JpaRepository<City, Long> { City getByName(String name); City findByNameAndCountryNameAllIgnoringCase(@NonNull String name, @NonNull String country); }
注意:我们可以通过定义正确的名称模式来简单地添加更多方法(请参阅 Spring 文档)。
到目前为止,我们使用的方法既不使用 JPA Criteria 也不使用 Querydsl。让我们看看使用这些技术的高级查询。
动态查询
很多时候,命名查询(即上述静态定义的查询)是不够的,因为查询必须使用输入参数。实现这一点的最简单方法是使用所用技术的特定接口扩展我们的存储库类,并将所需的查询功能添加为方法default。
JPA规范
JPA CriteriaSpecification为此目的使用了一个接口。我们必须遵循以下步骤:
扩展JpaSpecificationExecutor接口(第 1 行)。这使几种方法能够接受Specification接口作为参数。或者,您可以使用Pageable或Sort参数。
添加一个specWithState带有查询约束的方法(第 7-9 行)。在我们的例子中,约束的目标是找到属性中具有空值的城市域名e。
最后,我们需要使用接口findAll提供的方法(第 4 行)JpaSpecificationExecutor。它在findAllWithState方法中进行了演示(第 3-5 行)。
public interface CityRepository extends JpaRepository<City, Long>, JpaSpecificationExecutor<City> { default List<City> findAllWithState() { return findAll(specWithState()); } default Specification<City> specWithState() { return (cityRoot, q, cb) -> 域名(域名ll(域名(state))); } }
注意:这个查询也可以用命名查询来编写。
该specWithState方法的实现包含来自接口的方法的Lambda 表达式(第 8 行)。有了这个,我们可以以一种简单的方式查询数据,因为我们有,和类的可用实例。toPredicateSpecificationRootCriteriaQueryCriteriaBuilder
Querydsl 谓词
Querydsl 使用类似的方法,但使用Predicate返回类型代替。我们必须遵循以下步骤:
扩展QuerydslPredicateExecutor接口(第 1 行)。这使几种方法能够接受Predicate接口作为参数。您还可以选择使用其他参数(例如Pageable或Sort)。
添加一个predicateWithoutCities带有查询约束的方法(第 7-10 行)。目标是定义一个约束以找到没有任何城市的国家。
最后,我们还需要使用接口findAll提供的方法(第 4 行)QuerydslPredicateExecutor。用法在findAllWithoutCities方法中进行了演示(第 3-5 行)。
public interface CountryRepository extends JpaRepository<Country, Long>, QuerydslPredicateExecutor<Country> { default Iterable<Country> findAllWithoutCities() { return findAll(predicateWithoutCities()); } default Predicate predicateWithoutCities() { return new BooleanBuilder() .and(域名pty()); } }
(第 7-10 行)的实现predicateWithoutCities基于BooleanBuilder实现Predicate接口的类。在这里,我们创建了(第 8 行)的实例并对(第 9 行)BooleanBuilder应用了约束。域名try
自定义查询
当上面提到的所有选项都不够时,我们需要创建一个自定义查询。这种方法对于非常复杂的查询(例如,投影、分组、同一实体的多个连接、子查询等)很有用。官方文档第 4.6.1 章介绍了编写自定义查询的推荐方法。
JPA 标准
首先,我们需要创建一个新接口,其中定义了CityCustomRepository所需的方法(在我们的例子中)。findAllCitiesBy
public interface CityCustomRepository { List<City> findAllCitiesBy(@NonNull String name, @NonNull String state, @NonNull String countryName); }
接下来,我们需要实现这个方法。为此,我们创建了一个CityCustomRepositoryImpl实现该findAllCitiesBy方法的类。
@Repository @RequiredArgsConstructor public class CityCustomRepositoryImpl implements CityCustomRepository { @PersistenceContext private final EntityManager em; public List<City> findAllCitiesBy(@NonNull String cityName, @NonNull String cityState, @NonNull String countryName) { var cb = 域名riteriaBuilder(); var query = 域名teQuery(域名s); Root<City> cityRoot = 域名(域名s); List<Predicate> predicates = new ArrayList<>(); 域名(域名(域名("name"), cityName)); 域名(域名(域名("state"), cityState)); 域名(域名l(域名("country").get("name"), 域名ral(countryName))); 域名e(域名ray(new Predicate[0])); return 域名teQuery(query).getResultList(); } }
正如您在课堂上看到的CityCustomRepositoryImpl,我们需要遵循以下步骤:
将 的实例注入EntityManager到变量中em(第 6 行)。
CriteriaBuilder从中检索一个实例em(第 9 行)。
CriteriaQuery<City>创建类型为 from的查询cb(第 10 行)。
Root<City>从查询中获取 a (第 11 行)。
将所需的约束定义到predicates变量中(第 12-16 行)。
添加predicates到query(第 18 行)。
通过调用(第 19 行)触发查询getResultList。注意:还有其他方法可用于不同目的(例如getSingleResult、getFirstResult等)。
最后,我们需要CityRepository使用新创建的CityCustomRepository接口来扩展。
public interface CityRepository extends CityCustomRepository, JpaRepository<City, Long> { ... }
查询dsl
同样,我们需要CountryCustomRepository用所需的方法(findAllCitiesBy本例中的方法)创建一个新接口。
public interface CountryCustomRepository { List<Country> findAllCountriesBy(@NonNull String cityName, @NonNull String cityState); }
findAllCountriesBy我们在类中创建方法的实现CountryCustomRepositoryImpl。
@Repository @RequiredArgsConstructor public class CountryCustomRepositoryImpl implements CountryCustomRepository { @PersistenceContext private final EntityManager em; public List<Country> findAllCountriesBy(@NonNull String cityName, @NonNull String cityState) { return new JPAQuery<Country>(em) .select(域名try) .from(city) .where(域名(cityName) .and(域名(cityState))) .fetch(); } }
正如您在课堂上看到的CountryCustomRepositoryImpl,我们需要遵循以下步骤:
将 的实例注入EntityManager到变量中em(第 6 行)。
从(第 9 行)创建一个JpaQuery<City>查询em。
使用流畅的 API 方法定义类似 SQL 的查询约束(第 10-13 行)。
最后通过调用(第 14 行)触发查询fetch。注意:还有其他方法可用于不同目的(例如fetchOne、fetchFirst等)。
最后,我们需要CountryRepository使用新创建的CountryCustomRepository接口来扩展。
public interface CountryRepository extends CountryCustomRepository, JpaRepository<Country, Long> { ... }
结论
本文介绍了 Querydsl 框架是什么以及如何将其添加到我们的项目中。我们首先通过使用 Liquibase 库创建一个包含两个表和一些数据的数据库来进行设置。接下来,我们解释了使用 Spring Data JPA 创建查询的几个选项。CITY使用 JPA Criteria 演示了表的自定义查询,COUNTRY使用 Querydsl 演示了表的自定义查询。这部分是最重要的,因为它将是下一篇文章的主题。