具有任意 AND 子句的动态 spring 数据 jpa 存储库查询

Posted

技术标签:

【中文标题】具有任意 AND 子句的动态 spring 数据 jpa 存储库查询【英文标题】:Dynamic spring data jpa repository query with arbitrary AND clauses 【发布时间】:2015-05-06 14:28:16 【问题描述】:

我正在使用Spring data jpa repositories,需要提供具有不同字段的搜索功能。在搜索之前输入字段是可选的。我有 5 个字段,例如 EmployeeNumberNameMarriedProfessionDateOfBirth。 在这里我只需要查询用户给定的值,其他字段应该被忽略。例如,

Input : EmployeeNumber: ,Name:St,Married: ,Professsion:IT,DateOfBirth: 
Query : Select * from Employee e where Name like 'St%' and Profession like 'IT%';  

Input : EmployeeNumber:10,Name: ,Married: ,Professsion:IT,DateOfBirth:
Query : Select * from Employee e where EmployeeNumber like '10%' and Profession like 'IT%';  

所以在这里我们考虑输入和查询的值。在这种情况下,Spring 数据存在this post 中提到的限制(不可扩展,应编写所有可能的查询) 我正在使用Querydsl,但问题仍然存在,因为null 字段应该被忽略并且几乎所有可能的查询都需要开发。在这个case 31 queries。 如果搜索字段是6,7,8... 怎么办??

使用可选字段实现搜索选项的最佳方法是什么?

【问题讨论】:

【参考方案1】:

您可以使用 Spring-data 开箱即用的规范。并能够使用标准 API 以编程方式构建查询。要支持规范,您可以使用 JpaSpecificationExecutor 接口扩展存储库接口

public interface CustomerRepository extends SimpleJpaRepository<T, ID>, JpaSpecificationExecutor 


附加接口 (JpaSpecificationExecutor ) 带有允许您以多种方式执行规范的方法。

例如 findAll 方法将返回所有符合规范的实体:

List<T> findAll(Specification<T> spec);

规范界面如下:

public interface Specification<T> 
     Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
            CriteriaBuilder builder);

好的,那么典型的用例是什么?规范可以很容易地用于在实体之上构建一组可扩展的谓词,然后可以将其与 JpaRepository 组合并使用,而无需为每个需要的组合声明查询(方法)。这是一个示例:示例 2.15。客户规格

public class CustomerSpecs 
    public static Specification<Customer> isLongTermCustomer() 
        return new Specification<Customer>() 
            public Predicate toPredicate(
                Root<Customer> root, CriteriaQuery<?> query,
                CriteriaBuilder builder) 
                LocalDate date = new LocalDate().minusYears(2);
                return builder.lessThan(root.get('dateField'), date);
            
        ;
    

    public static Specification<Customer> hasSalesOfMoreThan(MontaryAmount value) 
        return new Specification<Customer>() 
            public Predicate toPredicate(
                Root<T> root, CriteriaQuery<?> query,
                CriteriaBuilder builder) 
                // build query here
            
        ;
    

您在业务需求抽象级别上表达了一些标准并创建了可执行的规范。因此,客户可能会使用如下规范:

List customers = customerRepository.findAll(isLongTermCustomer());

您还可以结合规范示例 2.17。组合规格

    MonetaryAmount amount = new MonetaryAmount(200.0, Currencies.DOLLAR);
    List<Customer> customers = customerRepository.findAll(
        where(isLongTermCustomer()).or(hasSalesOfMoreThan(amount)));

如您所见,规范提供了一些粘合代码方法来链接 并结合规格。因此扩展您的数据访问层是 只需创建新的规范实现和 将它们与现有的结合起来。

您可以创建复杂的规范,这是一个示例

public class WorkInProgressSpecification 
    public static Specification<WorkInProgress> findByCriteria(final SearchCriteria searchCriteria) 

        return new Specification<WorkInProgress>() 

            @Override
            public Predicate toPredicate(
                Root<WorkInProgress> root,
                CriteriaQuery<?> query, CriteriaBuilder cb) 

                List<Predicate> predicates = new ArrayList<Predicate>();

                if (searchCriteria.getView() != null && !searchCriteria.getView().isEmpty()) 
                    predicates.add(cb.equal(root.get("viewType"), searchCriteria.getView()));
                
                if (searchCriteria.getFeature() != null && !searchCriteria.getFeature().isEmpty()) 
                    predicates.add(cb.equal(root.get("title"), searchCriteria.getFeature()));
                
                if (searchCriteria.getEpic() != null && !searchCriteria.getEpic().isEmpty()) 
                    predicates.add(cb.equal(root.get("epic"), searchCriteria.getEpic()));
                
                if (searchCriteria.getPerformingGroup() != null && !searchCriteria.getPerformingGroup().isEmpty()) 
                    predicates.add(cb.equal(root.get("performingGroup"), searchCriteria.getPerformingGroup()));
                
                if (searchCriteria.getPlannedStartDate() != null) 
                    System.out.println("searchCriteria.getPlannedStartDate():" + searchCriteria.getPlannedStartDate());
                    predicates.add(cb.greaterThanOrEqualTo(root.<Date>get("plndStartDate"), searchCriteria.getPlannedStartDate()));
                
                if (searchCriteria.getPlannedCompletionDate() != null) 
                    predicates.add(cb.lessThanOrEqualTo(root.<Date>get("plndComplDate"), searchCriteria.getPlannedCompletionDate()));
                
                if (searchCriteria.getTeam() != null && !searchCriteria.getTeam().isEmpty()) 
                    predicates.add(cb.equal(root.get("agileTeam"), searchCriteria.getTeam()));
                

                return cb.and(predicates.toArray(new Predicate[] ));
            
        ;
    

这里是JPA Respositories docs

【讨论】:

我很确定你可以生成一个元模型,这样你就不需要用字符串指定属性了。 你说得对,QueryDSL 值得考虑用于类型安全查询 不仅在 QueryDsl 中,我认为您可以为 Criteria API 中的根创建元模型。根据spring.io/blog/2011/04/26/… 此处的博客文章,他们似乎创建了一个允许root.get(WorkInProgress_.agileTeam) 而不是root.get("agileTeam") 的元模型。毕竟字符串是有问题的。 感谢您宝贵的时间.. 我正在使用 Querydsl,所以@EpicPandaForce 回答最适合我.. 元模型生成信息:***.com/questions/3037593/…【参考方案2】:

请注意,使用 QueryDSL (4.x) 和 querydsl-jpa 的新主要版本可能需要进行更改


在我们的一个项目中,我们使用了QueryDSLQueryDslPredicateExecutor&lt;T&gt;

  public Predicate createPredicate(DataEntity dataEntity) 
    QDataEntity qDataEntity = QDataEntity.dataEntity;
    BooleanBuilder booleanBuilder = new BooleanBuilder();
    if (!StringUtils.isEmpty(dataEntity.getCnsiConsumerNo())) 
      booleanBuilder
        .or(qDataEntity.cnsiConsumerNo.contains(dataEntity.getCnsiConsumerNo()));
    
    if (!StringUtils.isEmpty(dataEntity.getCnsiMeterNo())) 
      booleanBuilder.or(qDataEntity.cnsiMeterNo.contains(dataEntity.getCnsiMeterNo()));
    

    return booleanBuilder.getValue();
  

我们可以在存储库中使用它:

@Repository
public interface DataEntityRepository
  extends DaoRepository<DataEntity, Long> 

DaoRepository 在哪里

@NoRepositoryBean
public interface DaoRepository<T, K extends Serializable>
  extends JpaRepository<T, K>,
  QueryDslPredicateExecutor<T> 

因此,您可以使用存储库谓词方法。

Iterable<DataEntity> results = dataEntityRepository.findAll(dataEntityPredicateCreator.createPredicate(dataEntity));

要获取QClasses,您需要在 pom.xml 中指定QueryDSL APT Maven plugin。

  <build>
    <plugins>
      <plugin>
        <groupId>com.mysema.maven</groupId>
        <artifactId>maven-apt-plugin</artifactId>
        <version>1.0.4</version>
        <executions>
          <execution>
            <phase>generate-sources</phase>
            <goals>
              <goal>process</goal>
            </goals>
            <configuration>
              <outputDirectory>target/generated-sources</outputDirectory>
              <processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
            </configuration>
          </execution>
        </executions>
      </plugin>

依赖是

    <!-- querydsl -->
    <dependency>
        <groupId>com.mysema.querydsl</groupId>
        <artifactId>querydsl-core</artifactId>
        <version>$querydsl.version</version>
    </dependency>
    <dependency>
        <groupId>com.mysema.querydsl</groupId>
        <artifactId>querydsl-apt</artifactId>
        <version>$querydsl.version</version>
    </dependency>
    <dependency>
        <groupId>com.mysema.querydsl</groupId>
        <artifactId>querydsl-jpa</artifactId>
        <version>$querydsl.version</version>
    </dependency>

或者对于 Gradle:

sourceSets 
    generated

sourceSets.generated.java.srcDirs = ['src/main/generated']
configurations 
    querydslapt

dependencies 
    // other deps ....
    compile "com.mysema.querydsl:querydsl-jpa:3.6.3"
    compile "com.mysema.querydsl:querydsl-apt:3.6.3:jpa"

task generateQueryDSL(type: JavaCompile, group: 'build', description: 'Generates the QueryDSL query types') 
    source = sourceSets.main.java
    classpath = configurations.compile + configurations.querydslapt
    options.compilerArgs = [
            "-proc:only",
            "-processor", "com.mysema.query.apt.jpa.JPAAnnotationProcessor"
    ]
    destinationDir = sourceSets.generated.java.srcDirs.iterator().next()


compileJava 
    dependsOn generateQueryDSL
    source generateQueryDSL.destinationDir


compileGeneratedJava 
    dependsOn generateQueryDSL
    classpath += sourceSets.main.runtimeClasspath

【讨论】:

我们此时使用的是3.2.5版本。 这里也有关于这个主题的指南(例如关于测试的其他信息):petrikainulainen.net/programming/spring-framework/… 最初在这里:spring.io/blog/2011/04/26/… 优秀.. 它工作得非常好.. 我不知道BooleanBuilder .. 在一个很好的例子之后,现在知道 Querydsl 可以做什么.. 感谢您宝贵的时间。 . 不客气,很高兴为您提供帮助:D Spring Data with QueryDSL 非常棒。 不错的项目,让jpa使用更简单【参考方案3】:

从 Spring Data JPA 1.10 开始,还有另一个选项是 Query By Example。 除了JpaRepository 之外,您的存储库还应该实现QueryByExampleExecutor 接口,您可以在其中获得以下方法:

<S extends T> Iterable<S> findAll(Example<S> example)

然后你创建Example 来搜索like:

Employee e = new Employee();
e.setEmployeeNumber(getEmployeeNumberSomewherFrom());
e.setName(getNameSomewhereFrom());
e.setMarried(getMarriedSomewhereFrom());
e.setProfession(getProfessionSomewhereFrom());
e.setDateOfBirth(getDateOfBirthSomewhereFrom());

然后:

employeeRepository.findAll(Example.of(e));

如果某些参数为空,它们将不会被带入 WHERE 子句,因此您可以获得动态查询。

要优化字符串属性的匹配,请查看ExampleMatcher's

一个不区分大小写的ExampleMatcher like 例如:

ExampleMatcher matcher = ExampleMatcher.matching().
          withMatcher("profession", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING).ignoreCase());

QBE 示例:https://github.com/spring-projects/spring-data-examples/tree/master/jpa/query-by-example

【讨论】:

Query By Example 看起来非常好并且适用于许多情况,但不幸的是它有一个缺点值得一提。无法使用 Query By Example 定义条件,例如 LESS THAN、GREATER THAN、BETWEEN 等。所以你不能定义某个日期或某个数字应该在某些边界内。它在限制部分的official documentation 中提到:Only supports starts/contains/ends/regex matching for strings and exact matching for other property types QueryByExample 如果有一对多的关系船也不会加入。例如,我有一个实体 POST,我有另一个 POSTComments,其中一个帖子可以有很多 cmets。在这种情况下假设我正在寻找状态为活动状态的 POST,我在 POST 实体上搜索某些字段。它首先获取所有具有条件的帖子,然后对子对象进行选择查询 ..比如 Get PostComments,其中 postid 是这个 ..如果它选择让我们说 100 个帖子,那么它会再进行 100 个选择以获取似乎效率低下的子实体 按示例查询也不允许您搜索单个字段的多个值。【参考方案4】:

游戏有点晚了,但这里的答案过于复杂......如果您更改实体的字段怎么办?如果您想支持对不同实体的搜索怎么办?

你可以只使用这个库:https://github.com/turkraft/spring-filter

它将让您运行搜索查询,例如:

/search?filter= 平均(收视率)> 4.5 brand.name in (“奥迪”、“路虎”)(年> 2018 公里 50000) 和颜色 : '白色' 意外是空的

结合 Spring 的 Pageable,您将能够使用 &amp;page=11&amp;size=20 进行分页

【讨论】:

以上是关于具有任意 AND 子句的动态 spring 数据 jpa 存储库查询的主要内容,如果未能解决你的问题,请参考以下文章

spring-反射动态代理

如何通过 Spring Boot JPA 执行具有 INTERVAL 子句的本机 SQL 查询?

如何从具有 RETURNING 子句的动态 SQL 返回集合

具有动态查询的 Jdbc 消息源的 Spring 集成流

Spring知识点总结5 反射与代理

如何在 Spring Data JPA 中使用 CTE 表达式 WITH 子句