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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/29602386/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-11 08:17:15  来源:igfitidea点击:

How does the FetchMode work in Spring Data JPA

javaspringhibernatejpaspring-data-jpa

提问by SirKometa

I do have a relation between three model object in my project (model and repository snippets in the end of the post.

我的项目中的三个模型对象之间确实存在关系(文章末尾的模型和存储库片段。

When I call PlaceRepository.findByIdit does fire three select queries:

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

("sql")

(“sql”)

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

That's rather unusual behavior (for me). As far as I can tell after reading Hibernate documentation it should always use JOIN queries. There is no difference in the queries when FetchType.LAZYchanged to FetchType.EAGERin the Placeclass (query with additional SELECT), the same for the Cityclass when FetchType.LAZYchanged to FetchType.EAGER(query with JOIN).

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

When I use CityRepository.findByIdsuppressing fires two selects:

当我使用CityRepository.findById抑制火灾时,有两个选择:

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

My goal is to have a the sam behavior in all situations (either always JOIN or SELECT, JOIN preferred though).

我的目标是在所有情况下都具有相同的行为(总是加入或选择,但首选加入)。

Model definitions:

模型定义:

Place:

地方:

@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
}

City:

城市:

@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
}

Repositories:

存储库:

PlaceRepository

位置存储库

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

UserRepository:

用户库:

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

CityRepository:

城市仓库:

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

采纳答案by wesker317

I think that Spring Data ignores the FetchMode. I always use the @NamedEntityGraphand @EntityGraphannotations when working with Spring Data

我认为 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);

}

Check the documentation here

检查文档here

回答by Vlad Mihalcea

First of all, @Fetch(FetchMode.JOIN)and @ManyToOne(fetch = FetchType.LAZY)are antagonistic, one instructing an EAGER fetching, while the other suggesting a LAZY fetch.

首先,@Fetch(FetchMode.JOIN)并且@ManyToOne(fetch = FetchType.LAZY)是对立的,一个指示 EAGER 提取,而另一个指示 LAZY 提取。

Eager fetching is rarely a good choiceand for a predictable behavior, you are better off using the query-time JOIN FETCHdirective:

Eager fetch很少是一个好的选择,对于可预测的行为,最好使用查询时间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);
}

回答by Godwin

"FetchType.LAZY" will only fire for primary table. If in your code you call any other method that has a parent table dependency then it will fire query to get that table information. (FIRES MULTIPLE SELECT)

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

"FetchType.EAGER" will create join of all table including relevant parent tables directly. (USES JOIN)

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

When to Use: Suppose you compulsorily need to use dependant parent table informartion then choose FetchType.EAGER. If you only need information for certain records then use FetchType.LAZY.

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

Remember, FetchType.LAZYneeds an active db session factory at the place in your code where if you choose to retrieve parent table information.

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

E.g. for 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

额外参考

回答by dream83619

Spring-jpa creates the query using the entity manager, and Hibernate will ignore the fetch mode if the query was built by the entity manager.

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

The following is the work around that I used:

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

  1. Implement a custom repository which inherits from SimpleJpaRepository

  2. Override the method getQuery(Specification<T> 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));
    }
    

    In the middle of the method, add applyFetchMode(root);to apply the fetch mode, to make Hibernate create the query with the correct join.

    (Unfortunately we need to copy the whole method and related private methods from the base class because there was no other extension point.)

  3. Implement 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);
            }
        }
    }
    
  1. 实现从 SimpleJpaRepository 继承的自定义存储库

  2. 覆盖方法getQuery(Specification<T> 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);应用获取模式,使 Hibernate 使用正确的连接创建查询。

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

  3. 实施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);
            }
        }
    }
    

回答by Ondrej Bozek

I elaborated on dream83619answer to make it handle nested Hibernate @Fetchannotations. I used recursive method to find annotations in nested associated classes.

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

So you have to implement custom repositoryand override getQuery(spec, domainClass, sort)method. Unfortunately you also have to copy all referenced private methods :(.

所以你必须实现自定义存储库和覆盖getQuery(spec, domainClass, sort)方法。不幸的是,您还必须复制所有引用的私有方法:(。

Here is the code, copied private methods are omitted.
EDIT:Added remaining private methods.

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

@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;
    }
}

回答by kafkas

http://jdpgrailsdev.github.io/blog/2014/09/09/spring_data_hibernate_join.html
from this link:

http://jdpgrailsdev.github.io/blog/2014/09/09/spring_data_hibernate_join.html
来自此链接:

if you are using JPA on top of Hibernate, there is no way to set the FetchMode used by Hibernate to JOINHowever, if you are using JPA on top of Hibernate, there is no way to set the FetchMode used by Hibernate to JOIN.

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

The Spring Data JPA library provides a Domain Driven Design Specifications API that allows you to control the behavior of the generated query.

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);

回答by adrhc

According to Vlad Mihalcea (see https://vladmihalcea.com/hibernate-facts-the-importance-of-fetch-strategy/):

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

JPQL queries may override the default fetching strategy. If we don't explicitly declare what we want to fetch using inner or left join fetch directives, the default select fetch policy is applied.

JPQL 查询可能会覆盖默认的获取策略。如果我们没有使用内连接或左连接获取指令明确声明我们想要获取的内容,则会应用默认的选择获取策略。

It seems that JPQL query might override your declared fetching strategy so you'll have to use join fetchin order to eagerly load some referenced entity or simply load by id with EntityManager (which will obey your fetching strategy but might not be a solution for your use case).

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

回答by Christian Beikov

The fetch mode will only work when selecting the object by idi.e. using entityManager.find(). Since Spring Data will always create a query, the fetch mode configuration will have no use to you. You can either use dedicated queries with fetch joins or use entity graphs.

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

When you want best performance, you should select only the subset of the data you really need. To do this, it is generally recommended to use a DTO approach to avoid unnecessary data to be fetched, but that usually results in quite a lot of error prone boilerplate code, since you need define a dedicated query that constructs your DTO model via a JPQL constructor expression.

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

Spring Data projections can help here, but at some point you will need a solution like Blaze-Persistence Entity Viewswhich makes this pretty easy and has a lot more features in it's sleeve that will come in handy! You just create a DTO interface per entity where the getters represent the subset of data you need. A solution to your problem could look like this

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);
}

Disclaimer, I'm the author of Blaze-Persistence, so I might be biased.

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