Spring Data JPA + JpaSpecificationExecutor + EntityGraph 春季数据JPA + Jpa规范执行器 + 实体图

29

(使用Spring Data JPA) 我有两个实体ParentChild,它们之间存在一个双向的OneToMany/ManyToOne关系。我向父实体添加了一个@NamedEntityGraph,代码如下:

@Entity
@NamedEntityGraph(name = "Parent.Offspring", attributeNodes = @NamedAttributeNodes("children"))
public class Parent{
//blah blah blah

@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
Set<Child> children;

//blah blah blah
}

注意,Parent的子级的获取类型是LAZY。 这是有意为之的。 当我查询单个parent时,我不总是想要急切地加载子级。 通常我可以使用我的命名实体图来按需急切地加载子级,所谓的。 但是.....

有一种特殊情况,我想查询一个或多个parent并及时加载他们的子级。 除此之外,我需要能够以编程方式构建此查询。 Spring Data提供了JpaSpecificationExecutor,它允许构建动态查询,但我无法弄清如何在此特定情况下将其与实体图一起使用以急切地加载子级。 这是否可能? 是否有其他方法可以使用规格说明急切地加载“toMany”实体?


你有没有找到这个问题的答案? - Joep
7个回答

23
解决方法是创建一个自定义的存储库接口,实现以下功能:
@NoRepositoryBean
public interface CustomRepository<T, ID extends Serializable> extends JpaRepository<T, ID>, JpaSpecificationExecutor<T> {

    List<T> findAll(Specification<T> spec, EntityGraphType entityGraphType, String entityGraphName);
    Page<T> findAll(Specification<T> spec, Pageable pageable, EntityGraphType entityGraphType, String entityGraphName);
    List<T> findAll(Specification<T> spec, Sort sort, EntityGraphType entityGraphType, String entityGraphName);
    T findOne(Specification<T> spec, EntityGraphType entityGraphType, String entityGraphName);

}

同时创建一个实现:
@NoRepositoryBean
public class CustomRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> implements CustomRepository<T, ID> {

    private EntityManager em;

    public CustomRepositoryImpl(Class<T> domainClass, EntityManager em) {
        super(domainClass, em);
        this.em = em;
    }

    @Override
    public List<T> findAll(Specification<T> spec, EntityGraph.EntityGraphType entityGraphType, String entityGraphName) {
        TypedQuery<T> query = getQuery(spec, (Sort) null);
        query.setHint(entityGraphType.getKey(), em.getEntityGraph(entityGraphName));
        return query.getResultList();
    }

    @Override
    public Page<T> findAll(Specification<T> spec, Pageable pageable, EntityGraph.EntityGraphType entityGraphType, String entityGraphName) {
        TypedQuery<T> query = getQuery(spec, pageable.getSort());
        query.setHint(entityGraphType.getKey(), em.getEntityGraph(entityGraphName));
        return readPage(query, pageable, spec);
    }

    @Override
    public List<T> findAll(Specification<T> spec, Sort sort, EntityGraph.EntityGraphType entityGraphType, String entityGraphName) {
        TypedQuery<T> query = getQuery(spec, sort);
        query.setHint(entityGraphType.getKey(), em.getEntityGraph(entityGraphName));
        return query.getResultList();
    }

    @Override
    public T findOne(Specification<T> spec, EntityGraph.EntityGraphType entityGraphType, String entityGraphName) {
        TypedQuery<T> query = getQuery(spec, (Sort) null);
        query.setHint(entityGraphType.getKey(), em.getEntityGraph(entityGraphName));
        return query.getSingleResult();
    }
}

并创建一个工厂:

public class CustomRepositoryFactoryBean<R extends JpaRepository<T, I>, T, I extends Serializable> extends JpaRepositoryFactoryBean<R, T, I> {

    protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {
        return new CustomRepositoryFactory(entityManager);
    }

    private static class CustomRepositoryFactory<T, I extends Serializable> extends JpaRepositoryFactory {

        private EntityManager entityManager;

        public CustomRepositoryFactory(EntityManager entityManager) {
            super(entityManager);
            this.entityManager = entityManager;
        }

        protected Object getTargetRepository(RepositoryMetadata metadata) {
            return new CustomRepositoryImpl<T, I>((Class<T>) metadata.getDomainType(), entityManager);
        }

        protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
            // The RepositoryMetadata can be safely ignored, it is used by the JpaRepositoryFactory
            //to check for QueryDslJpaRepository's which is out of scope.
            return CustomRepository.class;
        }
    }

}

将默认的仓库工厂Bean更改为新的Bean,例如在Spring Boot中将其添加到配置中:

@EnableJpaRepositories(
    basePackages = {"your.package"},
    repositoryFactoryBeanClass = CustomRepositoryFactoryBean.class
)

了解有关自定义存储库的更多信息:http://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.custom-behaviour-for-all-repositories


干得好,@Joepie,省了我几个小时的工作。 - Grant Lay
1
readPage(javax.persistence.TypedQuery<T> query, Pageable pageable, @Nullable Specification<T> spec)现在已经过时,请使用readPage(TypedQuery, Class, Pageable, Specification)代替。 - Ortomala Lokni
在最近的Spring Data JPA版本(从2.0开始的版本)中,不要使用(Sort) null,而是使用Sort.unsorted() - Markus Pscheidt

18

我通过覆盖findAll方法并添加注解@EntityGraph来实现了这一点:

public interface BookRepository extends JpaSpecificationExecutor<Book> {
   @Override
   @EntityGraph(attributePaths = {"book.author"})
   List<Book> findAll(Specification<Book> spec);
}

2
非常优雅的解决方案。谢谢 :-) - John Smith
@vteraz,我无法在实体上声明的NamedEntityGraph覆盖JpaRepositoryfindById方法中使其工作。与上面的示例有什么区别?声明一个新的findWithAttributesAll方法将遵守NamedEntityGraph。 - leo
它对我有效,即使使用了@NamedEntityGraph。你能否提供一个你的案例示例? - vteraz
有没有办法使用这个解决方案来加载多个OneToMany关联? - Raj
当我同时拥有“可分页参数”页面时,这似乎对我不起作用。任何大于0的页面大小都会返回奇怪的结果。 - undefined

9

Joepie的响应是没问题的。

但是你不需要创建repositoryFactoryBeanClass,设置repositoryBaseClass。

@EnableJpaRepositories(
    basePackages = {"your.package"},
    repositoryBaseClass = CustomRepositoryImpl.class)

9
该项目Spring Data JPA EntityGraph实现了其他答案中提到的一些方法。
例如,它具有以下附加存储库接口:
  • EntityGraphJpaRepository,相当于标准的JpaRepository
  • EntityGraphJpaSpecificationExecutor,相当于标准的JpaSpecificationExecutor
请查看参考文档以获取一些示例。

3
为了补充Joeppbo的回答,我必须说一下,使用新版本的Spring Data JPA,你需要修改Custom RepositoryImpl的构造函数。现在 documentation中提到:

该类需要有一个超类构造函数,存储特定的存储库工厂实现正在使用。如果存储库基类具有多个构造函数,请覆盖使用EntityInformation加上存储特定基础设施对象(例如EntityManager或模板类)的构造函数。

我使用以下构造函数:

public CustomRepositoryImpl(JpaEntityInformation<T,?> entityInformation, EntityManager em) {
    super(entityInformation, em);
    this.domainClass = entityInformation.getJavaType();
    this.em = em;
}

我还添加了一个私有字段来存储领域类:

private final Class<T> domainClass;

这使我能够摆脱已弃用的方法readPage(javax.persistence.TypedQuery<T> query, Pageable pageable, @Nullable Specification<T> spec),并使用以下方法代替:
@Override
public Page<T> findAll(Specification<T> spec, Pageable pageable, EntityGraph.EntityGraphType entityGraphType, String entityGraphName) {
    TypedQuery<T> query = getQuery(spec, pageable.getSort());
    query.setHint(entityGraphType.getKey(), em.getEntityGraph(entityGraphName));
    return readPage(query, domainClass, pageable, spec);
 }

1
你可以直接使用SimpleJpaRepository的getDomainClass()方法(https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/support/SimpleJpaRepository.html#getDomainClass--) - Kruschenstein

0

看一下 复合规范 API,它可以实现你需要的功能。 这个 类实现了 规范 接口,可以像 示例 中那样组合使用。 对于你的情况,要获取姓氏为“Doe”的父母以及他们在1990年1月1日之前出生的孩子,你需要以下规范:

public final class ParentSpecifications {

    private ParentSpecifications() {
    }

    public static <S extends Path<Parent>> CompositeSpecification<Parent, S> surname(String surname) {
        return CompositeSpecification.<Parent, S, TypeSafePredicateBuilder<Path<Parent>>>of(
                (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("surname"), surname)
        );
    }

    public static <S extends From<?, Parent>> CompositeSpecification<Parent, S> fetchChildren(CompositeSpecification<?, ? super Join<?, Child>> childSpecification) {
        return CompositeSpecification.<Parent, S, TypeSafePredicateBuilder<From<?, Parent>>>of(
                (root, query, criteriaBuilder) -> {
                    query.distinct(true);
                    return childSpecification.asBuilder().toPredicate((Join<Parent, Child>) root.<Parent, Child>fetch("children", JoinType.LEFT), query, criteriaBuilder);
                });
    }
}

public final class ChildSpecifications {

    private ChildSpecifications() {
    }
    
    public static <S extends Path<Child>> CompositeSpecification<Child, S> dateOfBirth(CompositeSpecification<?, ? super Path<LocalDate>> dateOfBirthSpecification) {
        return CompositeSpecification.<Child, S, TypeSafePredicateBuilder<Path<Child>>>of(
                (root, query, criteriaBuilder) -> dateOfBirthSpecification.asBuilder().toPredicate(root.get("dateOfBirth"), query, criteriaBuilder)
        );
    }
}

以及查询:

var parents = parentRepository.findAll(surname("Doe").and(fetchChildren(dateOfBirth(lessThan(LocalDate.of(1990,1,1))))));

0

其实,仓库Bean的工厂并不是必需的。你只需要一个简单的Bean。

你可以在这里查看我的完整示例:https://github.com/netstart/POCs/tree/master/jpa/jpaspecification-whith-entity-graph


import com.specgraph.entitygraph.model.Characteristic;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.data.repository.NoRepositoryBean;

import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;

@NoRepositoryBean
public class CharacteristicsJpaSpecificationRepository extends SimpleJpaRepository<Characteristic, Long> {

   private final EntityManager em;

   public CharacteristicsJpaSpecificationRepository(Class<Characteristic> domainClass, EntityManager em) {
       super(domainClass, em);
       this.em = em;
   }

   public Page<Characteristic> findByTypeUsingSpecification(String type, Pageable pageable) {
       Specification<Characteristic> spec =
           (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("type"), type);

       return findAll(spec, pageable, EntityGraphType.FETCH, "Characteristic.item");
   }

   public Page<Characteristic> findAll(Specification<Characteristic> spec,
                                       Pageable pageable,
                                       EntityGraph.EntityGraphType entityGraphType,
                                       String entityGraphName) {

       TypedQuery<Characteristic> query = getQuery(spec, pageable.getSort());
       query.setHint(entityGraphType.getKey(), em.getEntityGraph(entityGraphName));
       return readPage(query, Characteristic.class, pageable, spec);
   }

}


import com.specgraph.entitygraph.model.Characteristic;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Configuration
public class MyRepositoryConfiguration {

    @PersistenceContext // this will inject em in your class
    private EntityManager em;

    @Bean
    public CharacteristicsJpaSpecificationRepository getCharacteristicsJpaSpecificationRepository() {
        return new CharacteristicsJpaSpecificationRepository(Characteristic.class, em);
    }

}


import com.specgraph.entitygraph.model.Characteristic;
import com.specgraph.entitygraph.repository.specentitygraph.CharacteristicsJpaSpecificationRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.test.context.jdbc.Sql;

import static org.assertj.core.api.Assertions.assertThat;

@DataJpaTest
@Sql(scripts = "/entitygraph-data.sql")
public class CharacteristicsJpaSpecificationTest2 {

    @Autowired
    private CharacteristicsJpaSpecificationRepository characteristicsJpaSpecificationRepository;

    @Test
    public void find() {
        int pageNumber = 1;
        int pageSize = 10;
        PageRequest pageable = PageRequest.of(pageNumber, pageSize);

        Page<Characteristic> page =
            characteristicsJpaSpecificationRepository.findByTypeUsingSpecification("Rigid", pageable);

        assertThat(page.getTotalElements()).isEqualTo(1);
    }

}

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