How does the FetchMode work in Spring Data JPA
Asked Answered
M

9

115

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);
}
Mammilla answered 13/4, 2015 at 9:48 Comment(1)
Hava a look at 5 ways to initialize lazy relationsships: thoughts-on-java.org/…Burkholder
R
140

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

Rove answered 15/4, 2015 at 13:28 Comment(8)
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.Mammilla
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.Rove
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 :)Mammilla
@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.Pretonic
@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.Expertism
@adrhc no you dont understand, FetchMode and FetchType are two different thingsPennywise
Thanks, I wanted to say that JPQL queries may override the default fetching strategy with select fetch policy.War
@NeilStockton FetchMode would be more evident if people would put the imports statements in their code samples. #endJavaRantRika
B
82

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);
}
Booking answered 16/4, 2015 at 6:37 Comment(7)
IS there way to achieve same result with Criteria API and Spring Data Specifications?Intelligencer
Not the fetch part, which requires JPA fetch profiles.Booking
Vlad Mihalcea, could you share the link with an example how to do this by using Spring Data JPA criteria (specification)? PleaseBoatload
I don't have any such example, but you can surely find one in Spring Data JPA tutorials.Booking
if using query-time..... will you still need to define @OneToMany ...etc on the entity?Blen
The mappings are driven by your app needs, not by the presence of a query.Booking
@VladMihalcea May you please look at this issue that I am facing regarding eager fetching while using JPA findBy methods . #56249367Trantham
G
21

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);
            }
        }
    }
    
Grisby answered 8/10, 2015 at 4:36 Comment(2)
Unfortunately this doesn't work for queries generated using repository method name.Expertism
could you please add all the import statements? thank you.Rika
H
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

Hollerman answered 29/4, 2021 at 13:24 Comment(3)
this is the smallest change to make the fetchmode right, good tip! Spring docu is a bit vague on this, I find.Uniocular
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.Hollerman
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.Uniocular
R
3

"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

Rosenbaum answered 16/4, 2015 at 8:53 Comment(2)
Interestingly, this answer got me on the right path to using NamedEntityGraph since I wanted a non-hydrated object graph.Abaft
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!Eatmon
E
3

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;
    }
}
Expertism answered 3/8, 2016 at 12:18 Comment(3)
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?Forkey
recursive Fetch doesn't work. If I have OneToMany it passes java.util.List to the next iterationGoethite
not tested it well yet, but think should be something like this ((Join) descent).getJavaType() instead of field.getType() when call recursively applyFetchModeGoethite
H
3

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.

Houseman answered 18/7, 2018 at 7:20 Comment(0)
S
2

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);
Sectarianism answered 7/1, 2018 at 19:21 Comment(0)
W
2

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

War answered 9/7, 2018 at 11:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.