How does the FetchMode work in Spring Data JPA

JavaSpringHibernateJpaSpring Data-Jpa

Java Problem Overview


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

Java Solutions


Solution 1 - Java

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 - Java

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 - Java

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

  1. 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 - Java

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 - Java

"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

Solution 6 - Java

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.

Solution 7 - Java

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

Solution 8 - Java

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

Solution 9 - Java

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionSirKometaView Question on Stackoverflow
Solution 1 - Javawesker317View Answer on Stackoverflow
Solution 2 - JavaVlad MihalceaView Answer on Stackoverflow
Solution 3 - Javadream83619View Answer on Stackoverflow
Solution 4 - JavaroyatirekView Answer on Stackoverflow
Solution 5 - JavaGodwinView Answer on Stackoverflow
Solution 6 - JavaChristian BeikovView Answer on Stackoverflow
Solution 7 - JavaOndrej BozekView Answer on Stackoverflow
Solution 8 - JavakafkasView Answer on Stackoverflow
Solution 9 - JavaAdrianView Answer on Stackoverflow