如何在Spring Data JPA中使用投影和规范?

38

我无法同时使用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) 抛出以下异常 -

Caused by: java.util.NoSuchElementException: null at java.util.ArrayList$Itr.next(ArrayList.java:854) ~[na:1.8.0_102] at java.util.Collections$UnmodifiableCollection$1.next(Collections.java:1042) ~[na:1.8.0_102] at org.springframework.data.jpa.repository.query.CriteriaQueryParameterBinder.bind(CriteriaQueryParameterBinder.java:63) ~[spring-data-jpa-1.10.6.RELEASE.jar:na] at org.springframework.data.jpa.repository.query.ParameterBinder.bind(ParameterBinder.java:100) ~[spring-data-jpa-1.10.6.RELEASE.jar:na] at org.springframework.data.jpa.repository.query.ParameterBinder.bindAndPrepare(ParameterBinder.java:160) ~[spring-data-jpa-1.10.6.RELEASE.jar:na] at org.springframework.data.jpa.repository.query.ParameterBinder.bindAndPrepare(ParameterBinder.java:151) ~[spring-data-jpa-1.10.6.RELEASE.jar:na] at org.springframework.data.jpa.repository.query.PartTreeJpaQuery$QueryPreparer.invokeBinding(PartTreeJpaQuery.java:218) ~[spring-data-jpa-1.10.6.RELEASE.jar:na] at org.springframework.data.jpa.repository.query.PartTreeJpaQuery$QueryPreparer.createQuery(PartTreeJpaQuery.java:142) ~[spring-data-jpa-1.10.6.RELEASE.jar:na] at org.springframework.data.jpa.repository.query.PartTreeJpaQuery.doCreateQuery(PartTreeJpaQuery.java:78) ~[spring-data-jpa-1.10.6.RELEASE.jar:na] at org.springframework.data.jpa.repository.query.AbstractJpaQuery.createQuery(AbstractJpaQuery.java:190) ~[spring-data-jpa-1.10.6.RELEASE.jar:na] at org.springframework.data.jpa.repository.query.JpaQueryExecution$CollectionExecution.doExecute(JpaQueryExecution.java:118) ~[spring-data-jpa-1.10.6.RELEASE.jar:na] at org.springframework.data.jpa.repository.query.JpaQueryExecution.execute(JpaQueryExecution.java:82) ~[spring-data-jpa-1.10.6.RELEASE.jar:na] at org.springframework.data.jpa.repository.query.AbstractJpaQuery.doExecute(AbstractJpaQuery.java:116) ~[spring-data-jpa-1.10.6.RELEASE.jar:na] at org.springframework.data.jpa.repository.query.AbstractJpaQuery.execute(AbstractJpaQuery.java:106) ~[spring-data-jpa-1.10.6.RELEASE.jar:na] at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:482) ~[spring-data-commons-1.12.6.RELEASE.jar:na] at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:460) ~[spring-data-commons-1.12.6.RELEASE.jar:na] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.5.RELEASE.jar:4.3.5.RELEASE] at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:61) ~[spring-data-commons-1.12.6.RELEASE.jar:na] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.5.RELEASE.jar:4.3.5.RELEASE] at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99) ~[spring-tx-4.3.5.RELEASE.jar:4.3.5.RELEASE] at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:282) ~[spring-tx-4.3.5.RELEASE.jar:4.3.5.RELEASE] at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) ~[spring-tx-4.3.5

如何实现这个目标?有什么想法吗?

11个回答

32

目前还不支持混合使用投影和约束条件。有一个bug跟踪此问题。


1
现在有什么关于如何克服这个问题的建议吗? - Viny Machado
漏洞已于2022年4月20日关闭。 - kozla13

17

我发现了这个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);

数据库级别投影的更新

下面的注释已经过时。

这种解决方案很不幸,因为它从DB中选择了所有内容,然后只是将其“映射”到投影中。

此库的最新版本支持数据库级别的投影 链接到提交

最新的Spring Boot版本更新

这个拉请求没有合并之前,为了支持最新的Spring Boot版本,可以直接将修复的库源代码从这里https://github.com/v-ladynev/specification-with-projection-embeded复制粘贴到项目中。


不过,我认为使用REST API会更快,因为你只需要发送较少的JSON数据。我是对的吗? - Jakub Słowikowski
@JakubSłowikowski 不行。最好不要在REST API的范围内使用实体类。 - v.ladynev
嗨 @v.ladynev,非常感谢您适配Spring Boot 2.7,真是救了我一命。 我注意到使用EntityGraph的方法已被标记为过时,并且您建议使用注解代替。我尝试过,但无法使其正常工作。您能提供一个使用Specification + Projections + EntityGraph的用例示例吗?我非常感谢您的帮助。 - undefined
@dmunozfer 我根本不使用静态实体图。我使用动态实体图代替。https://stackoverflow.com/a/71177276/3405171 我必须创建一个自定义的JpaRepositoryFactory来使用specification-with-projectionspring-data-jpa-entity-graph存储库。但是仍然无法在一个存储库中同时使用规范和动态实体图。我只是不需要它。可能是有可能的。 - undefined

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
}




2
+1 针对使用单独的轻量级实体作为投影手段的可能性提出的(经常被忽视的)建议。 - Giovanni
问题已于2022年4月20日关闭。 - kozla13
这不起作用...规范仍然被视为查询参数...至少对我来说是这样 - Jsef bch

4

从Spring Data 3.0开始,支持混合使用投影和规约。我们可以使用JpaSpecificationExecutor.findBy method

Specification<Country> specification = CountrySpecification.predicateName("Austria");
List<CountryProjection> result = countryRepository.findBy(specification, q -> q
        .project("name")                    // query hint (not required)
        .as(CountryProjection .class)       // projection result class
        .all()                              
);

请务必检查 FetchableFluentQuery 类,了解计数、分页、排序和其他选项


3
生成��SQL仍然查询所有列。 - John Zhang
这应该是被接受的答案,非常感谢你指出这个简单而优雅的解决方案! - undefined
代码可以运行,但正如约翰所提到的,查询包括了选择子句中的所有列,这并不是使用投影的目的。 - undefined

1

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

为了做到这一点,您可以查看此文档:

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

这里有一个示例:

Spring Data JPA自定义存储库

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

问候, C


1
我们尝试了一下Pramoth解决方案(自2022年以来一直没有更新)。但它无法处理OneToMany关联,因为它会返回每个元组的一个实体。
最后他们在Spring Boot 3中添加了这个功能(链接:https://github.com/spring-projects/spring-data-jpa/issues/2274)。
由于我们仍在使用Spring Boot 2,所以我只是复制了合适的FetchableFluentQueryBySpecification类和必要的导入(EntityGraphFactory、FluentQuerySupport)。将jakarta的导入改为javax的导入。
然后创建了一个CustomSimpleJpaRepository,其中包含从SimpleJpaRepository复制的方法:
public <S extends T, R> R findBy(Specification<T> spec, Function<FetchableFluentQuery<S>, R> queryFunction) {

        Assert.notNull(spec, "Specification must not be null!");
        Assert.notNull(queryFunction, "Query function must not be null!");

        Function<Sort, TypedQuery<T>> finder = sort -> {
            return getQuery(spec, getDomainClass(), sort);
        };

        FetchableFluentQuery<R> fluentQuery = new FetchableFluentQueryBySpecification<T, R>(spec, getDomainClass(),
                Sort.unsorted(), null, finder, this::count, this::exists, this.em);

        return queryFunction.apply((FetchableFluentQuery<S>) fluentQuery);
    }

然后启用自定义类
@Configuration
@EnableJpaRepositories(
    repositoryBaseClass = CustomSimpleJpaRepository.class
)

最后,您可以在存储库上使用findBy方法,其中V是您的projectionClass引用。
Page<V> entities = repository.findBy(
    specification,
    q -> q.as(projectionClass)
        .page(
            PageRequest
                .of(page, size)
                .withSort(sort))
);

0

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


1
请尝试详细说明您的解决方案,并添加代码以突出关键字。 - Agilanbu
您可以查看以下内容:https://thoughts-on-java.org/dto-projections/ - Loui5

0

根据您的需求复杂程度,您可能需要实现自定义存储库: 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允许的任何方式实现查询。


0
这是我在项目中使用的代码片段。(代码是Kotlin编写的,但可以轻松改写为Java)
    val specification = Specification { root: Root<User>, query: CriteriaQuery<*>, cb: CriteriaBuilder ->
        val themeJoin = root.join<User, Theme>("themes", JoinType.LEFT)
        query.multiselect(root.get<Long>("id").alias("id"),themeJoin.get<String>("title").alias("title"))
        cb.gt(themeJoin.get<Long>("id"),1)
    }

    val builder: CriteriaBuilder = em.criteriaBuilder
    val query = builder.createQuery(Tuple::class.java)
    val root = query.from(User::class.java)
    val predict = specification.toPredicate(root,query,builder)
    query.where(predict)
    return em.createQuery(query).resultList

在用户实体中,有几个一对多和多对一的属性,由于我只查询用户的ID和主题标题,JPA只会生成一个SQL连接用户实体和主题实体的语句。
select u1_0.id,t1_0.title from user u1_0 left join theme t1_0 on u1_0.id=t1_0.user_id where t1_0.id>?

-1

你可以使用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());
}

希望这能帮到你!


9
这会提取所有的实体,而投影的目的是避免这种情况,减少提取不必要字段的时间。如果我需要优化,则不会使用此解决方案。 - Cosmin Constantinescu
@CosminConstantinescu,你能分享一个更好的建议吗? - esdee

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接