【问题标题】:Dynamic spring data jpa repository query with arbitrary AND clauses具有任意 AND 子句的动态 spring 数据 jpa 存储库查询
【发布时间】: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... 怎么办??

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

【问题讨论】:

    标签: java spring spring-data spring-data-jpa querydsl


    【解决方案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 回答最适合我..
    • 元模型生成信息:stackoverflow.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 进行分页

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-05-25
      • 2015-10-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-04-16
      相关资源
      最近更新 更多