Spring Data Specifications入门教程
Spring Data Specifications入门教程
如果您正在寻找一种更好的方法来管理您的查询或想要生成动态和类型安全的查询,那么您可能会在 Spring Data JPA Specifications中找到您的解决方案。
代码示例
本文附有GitHub上的工作代码示例。
什么是Specifications?
Spring Data JPA Specifications 是我们可以使用的另一个工具,可以使用 Spring 或 Spring Boot 执行数据库查询。
Specifications 建立在 Criteria API 之上。
当构建条件查询时,我们需要自己建立和管理Root
,CriteraQuery
以及CriteriaBuilder
对象:
...
EntityManager entityManagr = getEntityManager();
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Product> productQuery = builder.createQuery(Product.class);
Root<Person> personRoot = productQuery.from(Product.class);
...
Specification建立在 Criteria API 之上,以简化开发人员体验。我们只需要实现Specification接口:
interface Specification<T>{
Predicate toPredicate(Root<T> root,
CriteriaQuery<?> query,
CriteriaBuilder criteriaBuilder);
}
使用规范我们可以构建原子谓词(atomic predicates),并结合这些谓词来构建复杂的动态查询。
Specifications 的灵感来自领域驱动设计“规范”模式。
为什么我们需要Specifications?
在 Spring Boot 中执行查询的最常见方法之一是使用如下查询方法:
interface ProductRepository extends JpaRepository<Product, String>,
JpaSpecificationExecutor<Product> {
List<Product> findAllByNameLike(String name);
List<Product> findAllByNameLikeAndPriceLessThanEqual(
String name,
Double price
);
List<Product> findAllByCategoryInAndPriceLessThanEqual(
List<Category> categories,
Double price
);
List<Product> findAllByCategoryInAndPriceBetween(
List<Category> categories,
Double bottom,
Double top
);
List<Product> findAllByNameLikeAndCategoryIn(
String name,
List<Category> categories
);
List<Product> findAllByNameLikeAndCategoryInAndPriceBetween(
String name,
List<Category> categories,
Double bottom,
Double top
);
}
查询方法(query methods)的问题是我们只能指定固定数量的标准。此外,查询方法的数量随着用例的增加而迅速增加。
在某些时候,查询方法中有许多重叠的条件,如果其中任何一种发生变化,我们将不得不对多种查询方法进行更改。
此外,当我们的查询中有很长的字段名称和多个条件时,查询方法的长度可能会显着增加。因此,可能需要一段时间才能理解如此冗长的查询及其目的:
List<Product> findAllByNameLikeAndCategoryInAndPriceBetweenAndManufacturingPlace_State(String name,
List<Category> categories,
Double bottom, Double top,
STATE state);
使用Specifications,我们可以通过创建原子谓词来解决这些问题。通过给这些谓词一个有意义的名字,我们可以清楚地说明它们的意图。我们将在使用规范编写查询部分中了解如何将上述内容转换为更有意义的查询 。
Specifications允许我们以编程方式编写查询。因此,我们可以根据用户输入动态构建查询。我们将在下面的利用Specifications进行动态查询部分中更详细地了解这一点。
初始设置
首先,我们需要在我们的build.gradle文件中添加 Spring Data Jpa 依赖:
...
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
annotationProcessor 'org.hibernate:hibernate-jpamodelgen'
...
我们还添加了hibernate-jpamodelgen注释处理器依赖项,它将生成我们实体的静态元模型类。
生成的元模型
Hibernate JPA 模型生成器生成的类将允许我们以强类型方式编写查询。
例如,让我们看看 JPA 实体Distributor:
@Entity
public class Distributor {
@Id
private String id;
private String name;
@OneToOne
private Address address;
//Getter setter ignored for brevity
}
Distributor实体的元模型类如下所示:
@Generated(value = "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor")
@StaticMetamodel(Distributor.class)
public abstract class Distributor_ {
public static volatile SingularAttribute<Distributor, Address> address;
public static volatile SingularAttribute<Distributor, String> name;
public static volatile SingularAttribute<Distributor, String> id;
public static final String ADDRESS = "address";
public static final String NAME = "name";
public static final String ID = "id";
}
我们现在可以在标准查询中使用Distributor_.name
,而不是直接使用实体的字符串字段名称。这样做的一个主要好处是使用元模型的查询随实体一起变化,并且比字符串查询更容易重构。
使用Specifications编写查询
让我们将上面提到的查询findAllByNameLike()
转换为一个Specification:
List<Product> findAllByNameLike(String name);
该查询方法的等效于Specification:
private Specification<Product> nameLike(String name){
return new Specification<Product>() {
@Override
public Predicate toPredicate(Root<Product> root,
CriteriaQuery<?> query,
CriteriaBuilder criteriaBuilder) {
return criteriaBuilder.like(root.get(Product_.NAME), "%"+name+"%");
}
};
}
使用 Java 8 Lambda,我们可以将上述内容简化为以下内容:
private Specification<Product> nameLike(String name){
return (root, query, criteriaBuilder)
-> criteriaBuilder.like(root.get(Product_.NAME), "%"+name+"%");
}
我们也可以在代码中需要它的地方内联编写它:
...
Specification<Product> nameLike =
(root, query, criteriaBuilder) ->
criteriaBuilder.like(root.get(Product_.NAME), "%"+name+"%");
...
但这违背了我们可重用性的目的,所以除非我们的用例需要,否则让我们避免这种情况。
要执行Specifications,我们需要扩展Spring Data JPA 存储库中的接口JpaSpecificationExecutor
:
interface ProductRepository extends JpaRepository<Product, String>,
JpaSpecificationExecutor<Product> {
}
该JpaSpecificationExecutor接口添加了允许我们执行Specifications 的方法,例如,这些:
List<T> findAll(Specification<T> spec);
Page<T> findAll(Specification<T> spec, Pageable pageable);
List<T> findAll(Specification<T> spec, Sort sort);
最后,要执行我们的查询,我们可以简单地调用:
List<Product> products = productRepository.findAll(namelike("reflectoring"));
我们还可以利用带有Pageable和Sort参数的重载方法findAll(),在这种情况下,我们可以对结果分页或者排序。
该Specification接口还具有公共静态辅助方法and()、or()和where(),这允许我们组合多个Specification。它还提供了not(),允许我们否定一个Specification。
让我们看一个例子:
public List<Product> getPremiumProducts(String name,
List<Category> categories) {
return productRepository.findAll(
where(belongsToCategory(categories))
.and(nameLike(name))
.and(isPremium()));
}
private Specification<Product> belongsToCategory(List<Category> categories){
return (root, query, criteriaBuilder)->
criteriaBuilder.in(root.get(Product_.CATEGORY)).value(categories);
}
private Specification<Product> isPremium() {
return (root, query, criteriaBuilder) ->
criteriaBuilder.and(
criteriaBuilder.equal(
root.get(Product_.MANUFACTURING_PLACE)
.get(Address_.STATE),
STATE.CALIFORNIA),
criteriaBuilder.greaterThanOrEqualTo(
root.get(Product_.PRICE), PREMIUM_PRICE));
}
在这里,我们使用where()
和and()
辅助函数将belongsToCategory()、nameLike()和isPremium()规范(specifications)合二为一。这也很好读,你不觉得吗?另外,请注意isPremium()如何赋予查询更多意义。
目前,isPremium()正在组合两个谓词,但如果我们愿意,我们可以为每个谓词创建单独的规范,然后再次通过and()来组合。现在,我们将保持原样,因为 在isPremium()中使用的谓词在该查询中是特定的,如果将来我们也需要在其他查询中使用它们,那么我们总是可以在不影响isPremium()函数的使用者的情况下拆分它们。
利用Specifications进行动态查询
假设我们想要创建一个 API,允许我们的客户获取所有产品并根据许多属性(例如类别、价格、颜色等)过滤它们。在这里,我们事先不知道哪些属性组合客户将用于过滤产品。
处理此问题的一种方法是为所有可能的组合编写查询方法,但这需要编写大量查询方法。随着我们引入新领域,这个数字会组合增加。
更好的解决方案是直接从客户端获取谓词,并使用规范将它们转换为数据库查询语句。客户端只需向我们提供Filters的列表,我们的后端将负责其余的工作。让我们看看如何做到这一点。
首先,让我们创建一个输入对象来从客户端获取过滤器:
public class Filter {
private String field;
private QueryOperator operator;
private String value;
private List<String> values;//Used in case of IN operator
}
我们将通过 REST API 将此对象公开给我们的客户。
其次,我们需要编写方法可以将Filter转成Specification:
private Specification<Product> createSpecification(Filter input) {
switch (input.getOperator()){
case EQUALS:
return (root, query, criteriaBuilder) ->
criteriaBuilder.equal(root.get(input.getField()),
castToRequiredType(root.get(input.getField()).getJavaType(),
input.getValue()));
case NOT_EQUALS:
return (root, query, criteriaBuilder) ->
criteriaBuilder.notEqual(root.get(input.getField()),
castToRequiredType(root.get(input.getField()).getJavaType(),
input.getValue()));
case GREATER_THAN:
return (root, query, criteriaBuilder) ->
criteriaBuilder.gt(root.get(input.getField()),
(Number) castToRequiredType(
root.get(input.getField()).getJavaType(),
input.getValue()));
case LESS_THAN:
return (root, query, criteriaBuilder) ->
criteriaBuilder.lt(root.get(input.getField()),
(Number) castToRequiredType(
root.get(input.getField()).getJavaType(),
input.getValue()));
case LIKE:
return (root, query, criteriaBuilder) ->
criteriaBuilder.like(root.get(input.getField()),
"%"+input.getValue()+"%");
case IN:
return (root, query, criteriaBuilder) ->
criteriaBuilder.in(root.get(input.getField()))
.value(castToRequiredType(
root.get(input.getField()).getJavaType(),
input.getValues()));
default:
throw new RuntimeException("Operation not supported yet");
}
}
在这里,我们支持多种操作,如EQUALS,LESS_THAN,IN等,我们也可以根据自己的需求添加更多。
现在,正如我们所知,Criteria API 允许我们编写类型安全的查询。因此,我们提供的值的类型必须与我们的字段类型兼容。Filter将值作为String这意味着我们必须将值转换为所需的类型,然后再将其传递给CriteriaBuilder:
private Object castToRequiredType(Class fieldType, String value) {
if(fieldType.isAssignableFrom(Double.class)) {
return Double.valueOf(value);
} else if(fieldType.isAssignableFrom(Integer.class)) {
return Integer.valueOf(value);
} else if(Enum.class.isAssignableFrom(fieldType)) {
return Enum.valueOf(fieldType, value);
}
return null;
}
private Object castToRequiredType(Class fieldType, List<String> value) {
List<Object> lists = new ArrayList<>();
for (String s : value) {
lists.add(castToRequiredType(fieldType, s));
}
return lists;
}
最后,我们添加一个函数,将多个过滤器组合到一个规范中:
private Specification<Product> getSpecificationFromFilters(List<Filter> filter){
Specification<Product> specification =
where(createSpecification(queryInput.remove(0)));
for (Filter input : filter) {
specification = specification.and(createSpecification(input));
}
return specification;
}
现在,让我们尝试使用我们新的动态specifications查询生成器获取属于MOBILE或TV APPLIANCE类别且价格低于 1000的所有产品。
Filter categories = Filter.builder()
.field("category")
.operator(QueryOperator.IN)
.values(List.of(Category.MOBILE.name(),
Category.TV_APPLIANCES.name()))
.build();
Filter lowRange = Filter.builder()
.field("price")
.operator(QueryOperator.LESS_THAN)
.value("1000")
.build();
List<Filter> filters = new ArrayList<>();
filters.add(lowRange);
filters.add(categories);
productRepository.getQueryResult(filters);
上面的代码片段应该适用于大多数过滤器情况,但仍有很大的改进空间。例如允许基于嵌套实体属性 ( manufacturingPlace.state) 的查询或限制我们希望允许过滤器的字段。将此视为一个开放式问题。
什么时候应该使用规范而不是查询方法?
想到的一个问题是,如果我们可以编写任何带有规范的查询,那么我们什么时候更喜欢查询方法?或者我们应该更喜欢它们吗?我相信在某些情况下查询方法可以派上用场。
假设我们的实体只有少数几个字段,它只需要以某种方式查询,那么当我们可以简单地编写查询方法时,为什么还要编写规范呢?
如果未来需要对给定实体进行更多查询,那么我们总是可以重构它以使用规范。此外,如果我们想在查询中使用特定于数据库的功能,例如使用 PostgresSQL 执行 JSON 查询,则规范将无济于事。
结论
规范为我们提供了一种编写可重用查询和流畅 API 的方法,我们可以使用这些 API 组合和构建更复杂的查询。
总而言之,无论我们想要创建可重用谓词还是想要以编程方式生成类型安全查询,Spring JPA 规范都是一个很好的工具。
感谢您的阅读!您可以在GitHub上找到工作代码。
来自:
Getting Started with Spring Data Specifications - Reflectoring