Spring Data JPA 中的 FetchMode 是如何工作的

Posted

技术标签:

【中文标题】Spring Data JPA 中的 FetchMode 是如何工作的【英文标题】:How does the FetchMode work in Spring Data JPA 【发布时间】:2015-06-18 14:09:36 【问题描述】:

我的项目中的三个模型对象(模型和存储库 sn-ps 在帖子末尾)之间确实存在关系。

当我调用 PlaceRepository.findById 时,它会触发三个选择查询:

("sql")

    SELECT * FROM place p where id = arg SELECT * FROM user u where u.id = place.user.id SELECT * FROM city c LEFT OUTER JOIN state s on c.woj_id = s.id where c.id = place.city.id

这是相当不寻常的行为(对我来说)。据我在阅读 Hibernate 文档后所知,它应该始终使用 JOIN 查询。 Place 类中FetchType.LAZY 更改为FetchType.EAGER 时(使用附加SELECT 查询),查询没有区别,FetchType.LAZY 更改为FetchType.EAGERCity 类相同(使用JOIN 查询)。

当我使用CityRepository.findById 抑制时会触发两个选择:

    SELECT * FROM city c where id = arg SELECT * FROM state s where id = city.state.id

我的目标是在所有情况下都有一个 sam 行为(总是 JOIN 或 SELECT,但首选 JOIN)。

模型定义:

地点:

@Entity
@Table(name = "place")
public class Place extends Identified 

    @Fetch(FetchMode.JOIN)
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_user_author")
    private User author;

    @Fetch(FetchMode.JOIN)
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "area_city_id")
    private City city;
    //getters and setters

城市:

@Entity
@Table(name = "area_city")
public class City extends Identified 

    @Fetch(FetchMode.JOIN)
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "area_woj_id")
    private State state;
    //getters and setters

存储库:

PlaceRepository

public interface PlaceRepository extends JpaRepository<Place, Long>, PlaceRepositoryCustom 
    Place findById(int id);

用户存储库:

public interface UserRepository extends JpaRepository<User, Long> 
        List<User> findAll();
    User findById(int id);

城市存储库:

public interface CityRepository extends JpaRepository<City, Long>, CityRepositoryCustom     
    City findById(int id);

【问题讨论】:

看看 5 种初始化惰性关系的方法:thoughts-on-java.org/… 【参考方案1】:

我认为 Spring Data 忽略了 FetchMode。在使用 Spring Data 时,我总是使用 @NamedEntityGraph@EntityGraph 注释

@Entity
@NamedEntityGraph(name = "GroupInfo.detail",
  attributeNodes = @NamedAttributeNode("members"))
public class GroupInfo 

  // default fetch mode is lazy.
  @ManyToMany
  List<GroupMember> members = new ArrayList<GroupMember>();

  …


@Repository
public interface GroupRepository extends CrudRepository<GroupInfo, String> 

  @EntityGraph(value = "GroupInfo.detail", type = EntityGraphType.LOAD)
  GroupInfo getByGroupName(String name);


查看文档here

【讨论】:

我似乎不适合我。我的意思是它可以工作,但是......当我用'@EntityGraph'注释存储库时它本身不起作用(通常)。例如:` Place findById(int id);` 有效,但List&lt;Place&gt; findAll(); 以异常org.springframework.data.mapping.PropertyReferenceException: No property find found for type Place! 结束。当我手动添加@Query("select p from Place p") 时它可以工作。不过似乎是解决方法。 也许它对 findAll() 不起作用,因为它是 JpaRepository 接口中的现有方法,而您的其他方法“findById”是在运行时生成的自定义查询方法。 我决定将此标记为正确答案,因为它是最好的。虽然它并不完美。它适用于大多数情况,但到目前为止,我注意到 spring-data-jpa 中的错误具有更复杂的 EntityGraphs。谢谢:) @EntityGraph 在实际场景中几乎无法使用,因为它无法指定我们想要使用哪种FetchJOINSUBSELECTSELECTBATCH)。这与@OneToMany 关联相结合,即使我们使用查询MaxResults,也会使Hibernate 将整个表提取到内存中。 谢谢,我想说JPQL 查询可能override 使用select fetch 策略的默认获取策略。【参考方案2】:

首先,@Fetch(FetchMode.JOIN)@ManyToOne(fetch = FetchType.LAZY) 是对立的,因为@Fetch(FetchMode.JOIN) 相当于JPA FetchType.EAGER

Eager fetching 很少是一个好的选择,对于可预测的行为,最好使用查询时 JOIN FETCH 指令:

public interface PlaceRepository extends JpaRepository<Place, Long>, PlaceRepositoryCustom 

    @Query(value = "SELECT p FROM Place p LEFT JOIN FETCH p.author LEFT JOIN FETCH p.city c LEFT JOIN FETCH c.state where p.id = :id")
    Place findById(@Param("id") int id);


public interface CityRepository extends JpaRepository<City, Long>, CityRepositoryCustom  
    @Query(value = "SELECT c FROM City c LEFT JOIN FETCH c.state where c.id = :id")   
    City findById(@Param("id") int id);

【讨论】:

有没有办法通过 Criteria API 和 Spring Data Specifications 获得相同的结果? 不是获取部分,它需要 JPA 获取配置文件。 Vlad Mihalcea,您能否通过使用 Spring Data JPA 标准(规范)的示例分享链接?请 我没有这样的例子,但你肯定可以在 Spring Data JPA 教程中找到一个。 如果使用查询时间.....你还需要在实体上定义@OneToMany ...等吗?【参考方案3】:

Spring-jpa 使用实体管理器创建查询,如果查询是由实体管理器构建的,Hibernate 将忽略 fetch 模式。

以下是我使用的解决方法:

    Implement a custom repository which inherits from SimpleJpaRepository

    重写方法getQuery(Specification&lt;T&gt; spec, Sort sort)

    @Override
    protected TypedQuery<T> getQuery(Specification<T> spec, Sort sort)  
        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
        CriteriaQuery<T> query = builder.createQuery(getDomainClass());
    
        Root<T> root = applySpecificationToCriteria(spec, query);
        query.select(root);
    
        applyFetchMode(root);
    
        if (sort != null) 
            query.orderBy(toOrders(sort, root, builder));
        
    
        return applyRepositoryMethodMetadata(entityManager.createQuery(query));
    
    

    在方法中间,添加 applyFetchMode(root); 以应用 fetch 模式,以使 Hibernate 创建具有正确连接的查询。

    (不幸的是,我们需要从基类中复制整个方法和相关的私有方法,因为没有其他扩展点。)

    实现applyFetchMode

    private void applyFetchMode(Root<T> root) 
        for (Field field : getDomainClass().getDeclaredFields()) 
    
            Fetch fetch = field.getAnnotation(Fetch.class);
    
            if (fetch != null && fetch.value() == FetchMode.JOIN) 
                root.fetch(field.getName(), JoinType.LEFT);
            
        
    
    

【讨论】:

不幸的是,这不适用于使用存储库方法名称生成的查询。 能否请您添加所有导入语句?谢谢。【参考方案4】:

"FetchType.LAZY" 只会针对主表触发。如果在您的代码中调用任何其他具有父表依赖项的方法,那么它将触发查询以获取该表信息。 (触发多选)

"FetchType.EAGER" 将直接创建所有表的连接,包括相关的父表。 (使用JOIN

何时使用: 假设你强制需要使用依赖父表信息然后选择FetchType.EAGER。 如果您只需要某些记录的信息,请使用FetchType.LAZY

请记住,FetchType.LAZY 需要在您的代码中选择检索父表信息的位置有一个活动的数据库会话工厂。

例如对于LAZY

.. Place fetched from db from your dao loayer
.. only place table information retrieved
.. some code
.. getCity() method called... Here db request will be fired to get city table info

Additional reference

【讨论】:

有趣的是,这个答案让我走上了使用NamedEntityGraph 的正确道路,因为我想要一个非水合对象图。 这个答案值得更多的支持。它很简洁,帮助我理解了为什么我会看到很多“神奇触发”的查询......非常感谢!【参考方案5】:

获取模式仅在按 id 选择对象时起作用,即使用entityManager.find()。由于 Spring Data 将始终创建查询,因此获取模式配置对您没有用处。您可以使用带有 fetch 连接的专用查询或使用实体图。

当您想要获得最佳性能时,您应该只选择您真正需要的数据子集。为此,通常建议使用 DTO 方法来避免获取不必要的数据,但这通常会导致大量容易出错的样板代码,因为您需要定义一个专用查询来通过 JPQL 构造您的 DTO 模型构造函数表达式。

Spring Data 预测可以在这里提供帮助,但在某些时候,您将需要像 Blaze-Persistence Entity Views 这样的解决方案,它使这变得非常简单,并且它的袖子里有更多的功能会派上用场!您只需为每个实体创建一个 DTO 接口,其中 getter 表示您需要的数据子集。您的问题的解决方案可能如下所示

@EntityView(Identified.class)
public interface IdentifiedView 
    @IdMapping
    Integer getId();


@EntityView(Identified.class)
public interface UserView extends IdentifiedView 
    String getName();


@EntityView(Identified.class)
public interface StateView extends IdentifiedView 
    String getName();


@EntityView(Place.class)
public interface PlaceView extends IdentifiedView 
    UserView getAuthor();
    CityView getCity();


@EntityView(City.class)
public interface CityView extends IdentifiedView 
    StateView getState();


public interface PlaceRepository extends JpaRepository<Place, Long>, PlaceRepositoryCustom 
    PlaceView findById(int id);


public interface UserRepository extends JpaRepository<User, Long> 
    List<UserView> findAllByOrderByIdAsc();
    UserView findById(int id);


public interface CityRepository extends JpaRepository<City, Long>, CityRepositoryCustom     
    CityView findById(int id);

免责声明,我是 Blaze-Persistence 的作者,所以我可能会有偏见。

【讨论】:

【参考方案6】:

spring data jpa使用的实体管理器忽略了fetch模式。

在 Repository 方法上使用 @EntityGraph 注释,

@EntityGraph(attributePaths =  "user", "hashtags")
Page<LIPost> findByVoteTypeIn(Set<VoteType> listOfVotetype, Pageable paging);

这里的用户和主题标签是 LIPost 实体中的属性。

spring data JPA构建的查询使用left outer join获取相关实体(用户和hashtags)数据。

在这种情况下,不需要在实体类上使用注解@NamedEntityGraph。

Documentation

【讨论】:

这是使 fetchmode 正确的最小更改,好技巧!我发现 Spring 文档对此有点模糊。 随着时间的推移,我明白在实体上使用@NamedEntityGraph 并使用规范进行查询更有意义,因为它将相关的东西保存在一个地方。 好点;您将在@NamedEntityGraph 中定义默认行为,但如果您需要一个特制的函数返回,您仍然会在函数中使用@EntityGraph,可能还命名该函数以突出显示这一点?在这种情况下,我宁愿使用不同的返回类型/DTO - 这是需要考虑的事情。【参考方案7】:

我详细说明了dream83619 答案,以使其处理嵌套的Hibernate @Fetch 注释。我使用递归方法在嵌套关联类中查找注释。

So you have to implement custom repository 并覆盖 getQuery(spec, domainClass, sort) 方法。 不幸的是,您还必须复制所有引用的私有方法:(。

这里是代码,复制的私有方法被省略了。编辑:添加了剩余的私有方法。

@NoRepositoryBean
public class EntityGraphRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> 

    private final EntityManager em;
    protected JpaEntityInformation<T, ?> entityInformation;

    public EntityGraphRepositoryImpl(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) 
        super(entityInformation, entityManager);
        this.em = entityManager;
        this.entityInformation = entityInformation;
    

    @Override
    protected <S extends T> TypedQuery<S> getQuery(Specification<S> spec, Class<S> domainClass, Sort sort) 
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<S> query = builder.createQuery(domainClass);

        Root<S> root = applySpecificationToCriteria(spec, domainClass, query);

        query.select(root);
        applyFetchMode(root);

        if (sort != null) 
            query.orderBy(toOrders(sort, root, builder));
        

        return applyRepositoryMethodMetadata(em.createQuery(query));
    

    private Map<String, Join<?, ?>> joinCache;

    private void applyFetchMode(Root<? extends T> root) 
        joinCache = new HashMap<>();
        applyFetchMode(root, getDomainClass(), "");
    

    private void applyFetchMode(FetchParent<?, ?> root, Class<?> clazz, String path) 
        for (Field field : clazz.getDeclaredFields()) 
            Fetch fetch = field.getAnnotation(Fetch.class);

            if (fetch != null && fetch.value() == FetchMode.JOIN) 
                FetchParent<?, ?> descent = root.fetch(field.getName(), JoinType.LEFT);
                String fieldPath = path + "." + field.getName();
                joinCache.put(path, (Join) descent);

                applyFetchMode(descent, field.getType(), fieldPath);
            
        
    

    /**
     * Applies the given @link Specification to the given @link CriteriaQuery.
     *
     * @param spec can be @literal null.
     * @param domainClass must not be @literal null.
     * @param query must not be @literal null.
     * @return
     */
    private <S, U extends T> Root<U> applySpecificationToCriteria(Specification<U> spec, Class<U> domainClass,
        CriteriaQuery<S> query) 

        Assert.notNull(query);
        Assert.notNull(domainClass);
        Root<U> root = query.from(domainClass);

        if (spec == null) 
            return root;
        

        CriteriaBuilder builder = em.getCriteriaBuilder();
        Predicate predicate = spec.toPredicate(root, query, builder);

        if (predicate != null) 
            query.where(predicate);
        

        return root;
    

    private <S> TypedQuery<S> applyRepositoryMethodMetadata(TypedQuery<S> query) 
        if (getRepositoryMethodMetadata() == null) 
            return query;
        

        LockModeType type = getRepositoryMethodMetadata().getLockModeType();
        TypedQuery<S> toReturn = type == null ? query : query.setLockMode(type);

        applyQueryHints(toReturn);

        return toReturn;
    

    private void applyQueryHints(Query query) 
        for (Map.Entry<String, Object> hint : getQueryHints().entrySet()) 
            query.setHint(hint.getKey(), hint.getValue());
        
    

    public Class<T> getEntityType() 
        return entityInformation.getJavaType();
    

    public EntityManager getEm() 
        return em;
    

【讨论】:

我正在尝试您的解决方案,但我在其中一种复制方法中有一个私有元数据变量,这给您带来了麻烦。可以分享最终代码吗? 递归提取不起作用。如果我有 OneToMany,它会将 java.util.List 传递给下一次迭代 还没有很好地测试过,但是认为递归调用applyFetchMode时应该是这样的 ((Join) descent).getJavaType() 而不是 field.getType()【参考方案8】:

http://jdpgrailsdev.github.io/blog/2014/09/09/spring_data_hibernate_join.html 从这个链接:

如果您在 Hibernate 之上使用 JPA,则无法将 Hibernate 使用的 FetchMode 设置为 JOIN 但是,如果您在 Hibernate 之上使用 JPA,则无法将 Hibernate 使用的 FetchMode 设置为 JOIN .

Spring Data JPA 库提供了一个领域驱动设计规范 API,允许您控制生成的查询的行为。

final long userId = 1;

final Specification<User> spec = new Specification<User>() 
   @Override
    public Predicate toPredicate(final Root<User> root, final 
     CriteriaQuery<?> query, final CriteriaBuilder cb) 
    query.distinct(true);
    root.fetch("permissions", JoinType.LEFT);
    return cb.equal(root.get("id"), userId);
 
;

List<User> users = userRepository.findAll(spec);

【讨论】:

【参考方案9】:

根据 Vlad Mihalcea(见 https://vladmihalcea.com/hibernate-facts-the-importance-of-fetch-strategy/):

JPQL 查询可能会覆盖默认的获取策略。如果我们不 使用内连接或左连接显式声明我们想要获取的内容 fetch 指令,应用默认的 select fetch 策略。

似乎 JPQL 查询可能会覆盖您声明的获取策略,因此您必须使用 join fetch 以便急切地加载一些引用的实体或简单地使用 EntityManager 按 id 加载(这将遵循您的获取策略,但可能不会适合您的用例的解决方案)。

【讨论】:

以上是关于Spring Data JPA 中的 FetchMode 是如何工作的的主要内容,如果未能解决你的问题,请参考以下文章

spring-data-jpa

Spring Data JPA在Spring Boot中的应用

spring-data-jpa中的查询方法

没有 JPQL 查询的 Spring-data-jpa 中的 CURRENT_DATE

spring data jpa中的存储是啥?

什么是 Spring Data JPA 中的默认 ORM 提供程序