【问题标题】:How to use projections and specifications with spring data jpa?如何在 spring data jpa 中使用预测和规范?
【发布时间】:2017-06-01 05:40:02
【问题描述】:

我无法同时使用 Spring Data JPA 预测和规范。我有以下设置:

实体:

@Entity
public class Country {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(name = "NAME", nullable = false)
    private String name;

    @Column(name = "CODE", nullable = false)
    private String code;

    ---getters & setters---

}

投影界面:

public interface CountryProjection {
    String getName();
}

国家规格:

public class CountrySpecification {
    public static Specification<Country> predicateName(final String name) {
        return new Specification<Country>() {
            @Override
            public Predicate toPredicate(Root<Country> eventRoot, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
                return criteriaBuilder.equal(eventRoot.get(Country_.name), name);
            }
        };
    }
}

存储库:

public interface CountryRepository extends JpaRepository<Country, Long>, JpaSpecificationExecutor<Country> {
    List<CountryProjection> findByName(String name); // works fine
    List<CountryProjection> findAllProjectedBy(); // works fine
    List<CountryProjection> findAllProjectedBy(Specification<Country> specification); //throws Exception as shown below
}

前两个方法 findByName 和 findAllProjectedBy 工作正常。 而第三种方法 findAllProjectedBy(Specification specification) 抛出以下异常 -

原因:java.util.NoSuchElementException: null at java.util.ArrayList$Itr.next(ArrayList.java:854) ~[na:1.8.0_102] 在 java.util.Collections$UnmodifiableCollection$1.next(Collections.java:1042) ~[na:1.8.0_102] 在 org.springframework.data.jpa.repository.query.CriteriaQueryParameterBinder.bind(CriteriaQueryParameterBinder.java:63) ~[spring-data-jpa-1.10.6.RELEASE.jar:na] 在 org.springframework.data.jpa.repository.query.ParameterBinder.bind(ParameterBinder.java:100) ~[spring-data-jpa-1.10.6.RELEASE.jar:na] 在 org.springframework.data.jpa.repository.query.ParameterBinder.bindAndPrepare(ParameterBinder.java:160) ~[spring-data-jpa-1.10.6.RELEASE.jar:na] 在 org.springframework.data.jpa.repository.query.ParameterBinder.bindAndPrepare(ParameterBinder.java:151) ~[spring-data-jpa-1.10.6.RELEASE.jar:na] 在 org.springframework.data.jpa.repository.query.PartTreeJpaQuery$QueryPreparer.invokeBinding(PartTreeJpaQuery.java:218) ~[spring-data-jpa-1.10.6.RELEASE.jar:na] 在 org.springframework.data.jpa.repository.query.PartTreeJpaQuery$QueryPreparer.createQuery(PartTreeJpaQuery.java:142) ~[spring-data-jpa-1.10.6.RELEASE.jar:na] 在 org.springframework.data.jpa.repository.query.PartTreeJpaQuery.doCreateQuery(PartTreeJpaQuery.java:78) ~[spring-data-jpa-1.10.6.RELEASE.jar:na] 在 org.springframework.data.jpa.repository.query.AbstractJpaQuery.createQuery(AbstractJpaQuery.java:190) ~[spring-data-jpa-1.10.6.RELEASE.jar:na] 在 org.springframework.data.jpa.repository.query.JpaQueryExecution$CollectionExecution.doExecute(JpaQueryExecution.java:118) ~[spring-data-jpa-1.10.6.RELEASE.jar:na] 在 org.springframework.data.jpa.repository.query.JpaQueryExecution.execute(JpaQueryExecution.java:82) ~[spring-data-jpa-1.10.6.RELEASE.jar:na] 在 org.springframework.data.jpa.repository.query.AbstractJpaQuery.doExecute(AbstractJpaQuery.java:116) ~[spring-data-jpa-1.10.6.RELEASE.jar:na] 在 org.springframework.data.jpa.repository.query.AbstractJpaQuery.execute(AbstractJpaQuery.java:106) ~[spring-data-jpa-1.10.6.RELEASE.jar:na] 在 org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:482) ~[spring-data-commons-1.12.6.RELEASE.jar:na] 在 org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:460) ~[spring-data-commons-1.12.6.RELEASE.jar:na] 在 org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.5.RELEASE.jar:4.3.5.RELEASE] 在 org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:61) ~[spring-data-commons-1.12.6.RELEASE.jar:na] 在 org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.5.RELEASE.jar:4.3.5.RELEASE] 在 org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99) ~[spring-tx-4.3.5.RELEASE.jar:4.3.5.RELEASE] 在 org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:282) ~[spring-tx-4.3.5.RELEASE.jar:4.3.5.RELEASE] 在 org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) ~[spring-tx-4.3.5.RELEASE.jar:4.3.5.RELEASE] 在 org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.5.RELEASE.jar:4.3.5.RELEASE] 在 org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:136) ~[spring-tx-4.3.5.RELEASE.jar:4.3.5.RELEASE] 在 org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.5.RELEASE.jar:4.3.5.RELEASE] 在 org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:133) ~[spring-data-jpa-1.10.6.RELEASE.jar:na] 在 org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.5.RELEASE.jar:4.3.5.RELEASE] 在 org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92) ~[spring-aop-4.3.5.RELEASE.jar:4.3.5.RELEASE] 在 org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.5.RELEASE.jar:4.3.5.RELEASE] 在 org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213) ~[spring-aop-4.3.5.RELEASE.jar:4.3.5.RELEASE] 在 com.sun.proxy.$Proxy82.findAllProjectedBy(Unknown Source) ~[na:na] at com.mmp.data.jpa.DataJpaApplication.run(DataJpaApplication.java:42) [类/:na]在 org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:800) [spring-boot-1.4.3.RELEASE.jar:1.4.3.RELEASE] ... 11个常用框架 省略

如何做到这一点?有什么想法吗?

【问题讨论】:

    标签: hibernate spring-boot spring-data-jpa jpql


    【解决方案1】:

    尚不支持混合投影和规格的功能。有一个bug 跟踪这个。

    【讨论】:

    • 目前有什么解决这个问题的建议吗?
    【解决方案2】:

    我找到了这个https://github.com/pramoth/specification-with-projection,它似乎可以满足您的需求。我已经将它包含在我自己的项目中,到目前为止没有问题。非常感谢 Pramoth。

    基本上你扩展 JpaSpecificationExecutorWithProjection 而不是 JpaSpecificationExecutor。

    public interface DocumentRepository extends JpaRepository< Country,Long>,JpaSpecificationExecutorWithProjection<Country,Long>
    

    你会得到带有预测和规范的 findall() 方法

    <R> Page<R> findAll(Specification<T> spec, Class<R> projectionClass, Pageable pageable);
    

    【讨论】:

    • 这个解决方案是不幸的,因为它选择了数据库中的所有内容,而不仅仅是“映射”到投影。
    • 这个解决方案违背了使用 Projections 的目的。
    • 它选择了所有的列并开始加载
    • 尽管如此,我认为这在 REST API 中会运行得更快,因为您必须在 JSON 中发送更少的数据。我说的对吗?
    • @JakubSłowikowski 否。最好不要在 REST API 范围内使用实体类。
    【解决方案3】:

    @esdee:目前,我创建了一个自定义存储库实现,在其中创建了一个动态查询,您甚至可以在其中创建本机查询并将其映射到 DTO,而无需使用投影。

    为此,您可以查看此文档:

    https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.custom-implementations

    这里已经是一个例子:

    Spring Data JPA Custom Repository

    请记住,如果您想使其可分页,在您的自定义存储库中,您还必须创建一个计算行数的方法,您希望在 customFindAll(parameters) 中提取该方法。 缺点是我在本机查询中重写了规范。但也许自定义实施也可以与规范一起使用,如果有帮助,请告诉我。

    问候, C

    【讨论】:

      【解决方案4】:

      除非您实现自己的存储库,否则没有解决方案。

      【讨论】:

      【解决方案5】:

      所以这个问题在 spring data github 中仍然存在。 正如@Naso 所说,您可以将另一个依赖项带入您的项目(https://github.com/pramoth/specification-with-projection) 或者没有什么能阻止您创建两个指向同一个表的实体类。例如

      @Entity
      @Table("country")
      public class Country {
        String code;
        String name;
      
      }
      @Entity
      @Table("country")
      public class CountryName {
      
       String name;
      }
      
      public interface CountryRepository extends JpaRepository<CountryName, Long>, JpaSpecificationExecutor<Country> {
      
          List<CountryName> findAllProjectedBy(Specification<Country> specification); //throws Exception as shown below
      }
      
      
      
      

      【讨论】:

      • +1 指出使用单独的轻量级实体作为投影方式的(通常被遗忘的)可能性
      【解决方案6】:

      根据您的需求变得多么复杂,您最终可能不得不实现一个自定义存储库: https://dzone.com/articles/accessing-the-entitymanager-from-spring-data-jpa

      总结上面的文章,你需要为自定义方法实现一个接口(接口的名称必须以Custom结尾):

      public interface ParkrunCourseRepositoryCustom {    
          void refresh(ParkrunCourse parkrunCourse);
      }
      

      然后你需要创建一个实现接口的类(类名必须以Impl结尾):

      import javax.persistence.PersistenceContext;
      import javax.persistence.EntityManager;
      import com.glenware.springboot.form.ParkrunCourse;
      import org.springframework.transaction.annotation.Transactional;
      public class ParkrunCourseRepositoryImpl implements ParkrunCourseRepositoryCustom {
          @PersistenceContext
          private EntityManager em;
          @Override
          @Transactional
          public void refresh(ParkrunCourse parkrunCourse) {
              em.refresh(parkrunCourse);
          }
      }
      

      最后,您必须实现实际存储库的接口:

      public interface ParkrunCourseRepository extends CrudRepository, ParkrunCourseRepositoryCustom {
      }
      

      这将使您可以完全访问EntityManager,从而允许您以 JPA 允许的任何方式实现查询。

      【讨论】:

        【解决方案7】:

        如果使用规范,则不能在CountryRepository中使用。

        CountryRepository cRepository;
        
        cRepository.findAll(Specification<Country> specification);
        

        【讨论】:

          【解决方案8】:

          解决此问题的另一种方法是使用ProxyProjectionFactory。您将让您的存储库获取实际实体,然后沿线(可能在您的服务层中)将结果集映射到投影类型。见下文;

          public interface CountryRepository extends JpaRepository<Country, Long>, JpaSpecificationExecutor<Country> {  
          
          }
          

          然后在你的服务中,你这样做;

          List<CountryProjection> findAllProjectedBy(Specification<Country> countrySpecification) {
              List<Country> countries = this.countryRepository.findAll(countrySpecification);
          
              ProxyProjectionFactory pf= new SpelAwareProxyProjectionFactory();
              return countries.stream().map(c->pf.createProjection(CountryProjection.class, c)).collect(Collectors.toList());
          }
          

          希望这会有所帮助!

          【讨论】:

          • 这将提取所有实体,Projection 的目的是避免这种情况,以减少提取不必要字段的时间。如果需要优化,我不会使用此解决方案
          • @CosminConstantinescu,你能分享一个更好的建议吗?
          猜你喜欢
          • 2016-02-17
          • 2022-11-21
          • 2019-05-25
          • 2016-07-22
          • 1970-01-01
          • 2017-10-28
          • 2017-01-12
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多