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

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

当我调用 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 查询。在 Place类中,当 FetchType.LAZY改为 FetchType.EAGER时,查询没有区别(使用附加的 SELECT 进行查询) ,在 FetchType.LAZY改为 FetchType.EAGER时,City类也是如此(使用 JOIN 进行查询)。

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

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

我的目标是在所有情况下都有一个相同的行为(不管是 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);
}
142802 次浏览

I think that Spring Data ignores the FetchMode. I always use the @NamedEntityGraph and @EntityGraph annotations when working with Spring Data

@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

First of all, @Fetch(FetchMode.JOIN) and @ManyToOne(fetch = FetchType.LAZY) are antagonistic because @Fetch(FetchMode.JOIN) is equivalent to the JPA FetchType.EAGER.

Eager fetching is rarely a good choice, and for predictable behavior, you are better off using the query-time JOIN FETCH directive:

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

"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.EAGER" will create join of all table including relevant parent tables directly. (USES 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.

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

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

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.

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

I elaborated on dream83619 answer to make it handle nested Hibernate @Fetch annotations. I used recursive method to find annotations in nested associated classes.

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

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

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

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.

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

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

According to Vlad Mihalcea (see 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.

It seems that JPQL query might override your declared fetching strategy so you'll have to use join fetch in 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).

The fetch mode will only work when selecting the object by id i.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.

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.

Spring Data projections can help here, but at some point you will need a solution like Blaze-Persistence Entity Views which 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

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

The entity manager used by spring data jpa ignores the fetch mode.

Use @EntityGraph annotation over Repository methods,

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

Here user and hashtags are the properties in the LIPost entity.

The query build by spring data JPA uses left outer join to get the related entity(user and hashtags) data.

In this case, no need to use the annotation @NamedEntityGraph over the entity class.

Documentation