FetchMode如何在Spring Data JPA中工作


91

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

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

(“ 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

(对我而言)这是非常不正常的行为。据我阅读Hibernate文档后所知,它应该始终使用JOIN查询。在类中FetchType.LAZY更改为 查询时(带有附加SELECT 的查询)没有任何区别,而在类更改为 (使用JOIN查询时)则没有变化。FetchType.EAGERPlaceCityFetchType.LAZYFetchType.EAGER

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

  1. SELECT * FROM city c where id = arg
  2. 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);
}

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

:华亿达看看5种方法来初始化懒relationsships thoughts-on-java.org/...
格里戈里Kislin

Answers:


109

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

}

此处查看文档


1
我似乎没有为我工作。我的意思是它可以工作,但是...当我用'@EntityGraph'注释存储库时,它本身(通常)不起作用。例如:`Place findById(int id);`可以工作,但 List<Place> findAll();以Exception结尾org.springframework.data.mapping.PropertyReferenceException: No property find found for type Place!。当我手动添加时它起作用@Query("select p from Place p")。好像解决方法。
SirKometa 2015年

也许它对findAll()起作用,因为它是JpaRepository接口中的现有方法,而其他方法“ findById”是在运行时生成的自定义查询方法。
wesker317

因为这是最好的,所以我决定将其标记为正确的答案。虽然这并不完美。它可以在大多数情况下使用,但到目前为止,我已经注意到spring-data-jpa中的错误,其中包含更为复杂的EntityGraphs。谢谢:)
SirKometa

2
@EntityGraph在现实情况下几乎ununsable因为不能指定它是什么样的Fetch,我们想用(JOINSUBSELECTSELECTBATCH)。这与@OneToMany关联结合在一起,即使我们使用query,也可以使Hibernate将整个表提取到内存中MaxResults
Ondrej Bozek '16

1
谢谢,我想说JPQL查询可以使用选择提取策略覆盖默认的提取策略。
adrhc

51

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

渴望获取很少是一个好的选择,对于可预期的行为,最好使用query-time 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);
}

3
是否可以通过Criteria API和Spring Data Specification达到相同的结果?
svlada 2015年

2
不是提取部分,这需要JPA提取配置文件。
Vlad Mihalcea 2015年

Vlad Mihalcea,您能否与一个示例共享链接,该示例如何使用Spring Data JPA标准(规范)来做到这一点?请
Yan Khonski

我没有任何这样的示例,但是您肯定可以在Spring Data JPA教程中找到一个示例。
弗拉德·米哈尔切亚

如果使用查询时间.....您是否仍需要在实体上定义@OneToMany ... etc?
Eric Huang

19

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

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

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

不幸的是,这不适用于使用存储库方法名称生成的查询。
Ondrej Bozek

您能否添加所有导入声明?谢谢。
granadaCoder

3

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

附加参考


有趣的是,NamedEntityGraph由于我想要一个非水合的对象图,这个答案使我走上了正确的道路。
JJ Zabkar '16

这个答案值得更多的赞扬。简洁明了,并帮助我很多理解了为什么我看到很多“神奇触发”的查询……非常感谢!
克林特·伊斯特伍德

3

提取模式仅在按ID选择对象即使用)时有效entityManager.find()。由于Spring Data将始终创建查询,因此获取模式配置将对您毫无用处。您可以将专用查询用于访存联接,也可以使用实体图。

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

Spring Data投影可以在这里提供帮助,但是在某些时候,您将需要诸如Blaze-Persistence Entity Views之类的解决方案,该解决方案非常简单,并且在其袖子中具有更多功能,将派上用场!您只需为每个实体创建一个DTO接口,其中的吸气剂表示您需要的数据子集。解决问题的方法可能如下所示

@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的作者,所以我可能会有偏见。


2

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

因此,您必须实现自定义存储库和重写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;
    }
}

我正在尝试您的解决方案,但是在复制方法之一中有一个私有元数据变量会给您带来麻烦。您可以共享最终代码吗?
Homer1980ar

递归提取不起作用。如果我有OneToMany,它将java.util.List传递给下一个迭代
antohoho,

尚未对其进行很好的测试,但是当递归调用applyFetchMode时,认为应该是这样的((Join)descent).getJavaType()而不是field.getType()
antohoho

2


通过此链接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,该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);

2

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

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

似乎JPQL查询可能会覆盖您声明的获取策略,因此您必须使用join fetch以便急切加载某些引用的实体或只是通过EntityManager的id加载(这将服从您的获取策略,但可能不是您的用例的解决方案)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.