FetchMode如何在Spring Data JPA中工作?

我在我的项目中有三个模型对象之间的关系(模型和存储库代码片尾)

当我打电话PlaceRepository.findById它会触发三个select查询:

( “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更改为Place类中的FetchType.EAGER (具有附加SELECT的查询)时,查询在查询中没有区别,而当FetchType.LAZY更改为FetchType.EAGER (JOIN查询)时, City类也相同。

当我使用CityRepository.findById抑制两个select:

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

我的目标是在所有情况下都具有山姆行为(或者总是JOIN或SELECT,尽pipeJOIN首选)。

模型定义:

地点:

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

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

这里检查文档

首先, @Fetch(FetchMode.JOIN)@ManyToOne(fetch = FetchType.LAZY)是敌对的,一个指示EAGER抓取,另一个指示LAZY抓取。

提前取指不是一个好的select ,对于可预测的行为,最好使用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); } 

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

以下是我使用的工作:

  1. 实现一个从SimpleJpaRepositoryinheritance的自定义仓库

  2. 重写getQuery(Specification<T> spec, Sort sort)方法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用正确的连接创build查询。

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

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

FetchType.LAZY ”只会触发主表。 如果在你的代码中调用任何其他具有父表依赖项的方法,那么它将触发查询来获取表信息。 (FIRES MULTIPLE SELECT)

FetchType.EAGER ”将直接创build包含相关父表的所有表的连接。 (使用连接)

何时使用:假设您强制需要使用从属父表信息,然后selectFetchType.EAGER 。 如果您只需要某些logging的信息,则使用FetchType.LAZY

请记住,如果您select检索父表信息, 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 

额外的参考

我详细阐述了dream83619的答案,以便处理嵌套的Hibernate @Fetch注解。 我使用recursion方法来查找嵌套关联类中的注释。

所以你必须实现自定义存储库并重写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; } }