How does the FetchMode work in Spring Data JPA

133,192

Solution 1

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

Solution 2

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

Solution 3

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

Solution 4

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

Solution 5

"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

Share:
133,192

Related videos on Youtube

SirKometa
Author by

SirKometa

Java enthusiast. Except for the programming I love photography, especially macro and portraits.

Updated on April 30, 2021

Comments

  • SirKometa
    SirKometa about 3 years

    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.findById it does fire three select queries:

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

    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.LAZY changed to FetchType.EAGER in the Place class (query with additional SELECT), the same for the City class when FetchType.LAZY changed to FetchType.EAGER (query with JOIN).

    When I use CityRepository.findById suppressing fires two selects:

    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);
    }
    
  • SirKometa
    SirKometa about 9 years
    I doesn't seem to work for me. I mean it works but... When I annotate repository with '@EntityGraph' it doesn't work itself (usually). For example: ` Place findById(int id);` works but List<Place> findAll(); ends up with the Exception org.springframework.data.mapping.PropertyReferenceException: No property find found for type Place!. It works when I manually add @Query("select p from Place p"). Seems like workaround though.
  • wesker317
    wesker317 about 9 years
    Maybe it dosent work on findAll() since it's an existing method from the JpaRepository interface while your other method "findById" is a custom query method generated at runtime.
  • svlada
    svlada about 9 years
    IS there way to achieve same result with Criteria API and Spring Data Specifications?
  • SirKometa
    SirKometa about 9 years
    I've decided to mark this as the proper answer since it's the best. It is not perfect though. It works in most scenarios but so far I've noticed bugs in spring-data-jpa with more complex EntityGraphs. Thanks :)
  • Vlad Mihalcea
    Vlad Mihalcea about 9 years
    Not the fetch part, which requires JPA fetch profiles.
  • AndiDev
    AndiDev over 8 years
    @Query("select p from ManyToManyEntity p") duplicates entities with many to many relationships that have more than one relation. However adding distinct, @Query("select distinct p from ManyToManyEntity p"), seems to solve this issue.
  • Ondrej Bozek
    Ondrej Bozek almost 8 years
    @EntityGraph is almost ununsable in real scenarios since it cant be specified what kind of Fetch we want to use (JOIN, SUBSELECT, SELECT, BATCH). This in combination with @OneToMany association and makes Hibernate Fetch whole table to memory even if we use query MaxResults.
  • Yan Khonski
    Yan Khonski over 7 years
    Vlad Mihalcea, could you share the link with an example how to do this by using Spring Data JPA criteria (specification)? Please
  • Vlad Mihalcea
    Vlad Mihalcea over 7 years
    I don't have any such example, but you can surely find one in Spring Data JPA tutorials.
  • JJ Zabkar
    JJ Zabkar over 7 years
    Interestingly, this answer got me on the right path to using NamedEntityGraph since I wanted a non-hydrated object graph.
  • Ondrej Bozek
    Ondrej Bozek about 7 years
    Unfortunately this doesn't work for queries generated using repository method name.
  • Homer1980ar
    Homer1980ar almost 7 years
    I'm trying your solution but I have a private metadata variable in one of the methods to copy that is giving trouble. Can you share final code?
  • Eric Huang
    Eric Huang over 6 years
    if using query-time..... will you still need to define @OneToMany ...etc on the entity?
  • Vlad Mihalcea
    Vlad Mihalcea over 6 years
    The mappings are driven by your app needs, not by the presence of a query.
  • antohoho
    antohoho almost 6 years
    recursive Fetch doesn't work. If I have OneToMany it passes java.util.List to the next iteration
  • antohoho
    antohoho almost 6 years
    not tested it well yet, but think should be something like this ((Join) descent).getJavaType() instead of field.getType() when call recursively applyFetchMode
  • Hoang Duc Nguyen
    Hoang Duc Nguyen over 5 years
    @adrhc no you dont understand, FetchMode and FetchType are two different things
  • Adrian
    Adrian over 5 years
    Thanks, I wanted to say that JPQL queries may override the default fetching strategy with select fetch policy.
  • ASharma7
    ASharma7 almost 5 years
    @VladMihalcea May you please look at this issue that I am facing regarding eager fetching while using JPA findBy methods . stackoverflow.com/questions/56248867/…
  • Clint Eastwood
    Clint Eastwood over 4 years
    this answer deserves more upvotes. It's succinct and helped me a lot to understand why I was seeing lots of "magically triggered" queries...thanks a lot!
  • granadaCoder
    granadaCoder almost 4 years
    @NeilStockton FetchMode would be more evident if people would put the imports statements in their code samples. #endJavaRant
  • granadaCoder
    granadaCoder almost 4 years
    could you please add all the import statements? thank you.
  • Frischling
    Frischling over 2 years
    this is the smallest change to make the fetchmode right, good tip! Spring docu is a bit vague on this, I find.
  • royatirek
    royatirek over 2 years
    Over time, I have understood that using @NamedEntityGraph over the entity and using the specification to make a query makes more sense as it keeps related things in one place.
  • Frischling
    Frischling over 2 years
    Good point; you'll define the default behaviour in @NamedEntityGraph, but if you need a specially crafted return for a function, you'd still use @EntityGraph at the function, probably also naming the funtion to highlight this? I'd rather have a different return type/DTO in that case - that's something to think about.