Cómo funciona el FetchMode en Spring Data JPA

Tengo una relación entre tres objetos modelo en mi proyecto (modelo y fragmentos de repository al final de la publicación.

Cuando llamo a PlaceRepository.findById se PlaceRepository.findById tres consultas de selección:

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

Es un comportamiento bastante inusual (para mí). Por lo que puedo decir después de leer la documentación de Hibernate, siempre debería usar las consultas JOIN. No hay diferencia en las consultas cuando FetchType.LAZY cambió a FetchType.EAGER en la clase Place (consulta con SELECT adicional), lo mismo para la clase City cuando FetchType.LAZY cambió a FetchType.EAGER (consulta con JOIN).

Cuando uso CityRepository.findById incendios, CityRepository.findById dos:

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

Mi objective es tener un comportamiento sam en todas las situaciones (ya sea siempre UNIRSE o SELECCIONAR, sin embargo SIEMPRE preferido).

Definiciones de modelo:

Lugar:

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

Ciudad:

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

Repositorios:

PlaceRepository

 public interface PlaceRepository extends JpaRepository, PlaceRepositoryCustom { Place findById(int id); } 

UserRepository:

 public interface UserRepository extends JpaRepository { List findAll(); User findById(int id); } 

CityRepository:

 public interface CityRepository extends JpaRepository, CityRepositoryCustom { City findById(int id); } 

Creo que Spring Data ignora el FetchMode. Siempre uso las anotaciones @NamedEntityGraph y @EntityGraph cuando trabajo con Spring Data

 @Entity @NamedEntityGraph(name = "GroupInfo.detail", attributeNodes = @NamedAttributeNode("members")) public class GroupInfo { // default fetch mode is lazy. @ManyToMany List members = new ArrayList(); … } @Repository public interface GroupRepository extends CrudRepository { @EntityGraph(value = "GroupInfo.detail", type = EntityGraphType.LOAD) GroupInfo getByGroupName(String name); } 

Verifique la documentación aquí

En primer lugar, @Fetch(FetchMode.JOIN) y @ManyToOne(fetch = FetchType.LAZY) son antagónicos, uno instruye a un EAGER buscando, mientras que el otro sugiere una búsqueda LAZY.

La búsqueda ansiosa rara vez es una buena opción y, para un comportamiento predecible, es mejor utilizar la directiva JOIN FETCH tiempo de consulta:

 public interface PlaceRepository extends JpaRepository, 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, 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 crea la consulta utilizando el administrador de entidades e Hibernate ignorará el modo de búsqueda si la consulta fue creada por el administrador de entidades.

El siguiente es el trabajo que utilicé:

  1. Implementar un repository personalizado que hereda de SimpleJpaRepository

  2. Reemplace el método getQuery(Specification spec, Sort sort) :

     @Override protected TypedQuery getQuery(Specification spec, Sort sort) { CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery query = builder.createQuery(getDomainClass()); Root root = applySpecificationToCriteria(spec, query); query.select(root); applyFetchMode(root); if (sort != null) { query.orderBy(toOrders(sort, root, builder)); } return applyRepositoryMethodMetadata(entityManager.createQuery(query)); } 

    En el medio del método, agregue applyFetchMode(root); para aplicar el modo de búsqueda, para que Hibernate cree la consulta con la combinación correcta.

    (Desafortunadamente tenemos que copiar el método completo y los métodos privados relacionados de la clase base porque no había otro punto de extensión).

  3. Implementar applyFetchMode :

     private void applyFetchMode(Root 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 ” solo se activará para la tabla principal. Si en su código llama a cualquier otro método que tenga una dependencia de tabla padre, activará la consulta para obtener esa información de la tabla. (FUEGOS SELECCIÓN MÚLTIPLE)

FetchType.EAGER ” creará una combinación de todas las tablas, incluidas las tablas principales relevantes directamente. (LOS USOS SE JOIN )

Cuándo usar: suponga que obligatoriamente necesita utilizar la información de la tabla padre dependiente y luego seleccione FetchType.EAGER . Si solo necesita información para ciertos registros, utilice FetchType.LAZY .

Recuerde, FetchType.LAZY necesita una fábrica de sesiones de db activa en el lugar de su código, donde si elige recuperar la información de la tabla padre.

Por ejemplo, para 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 

Referencia adicional

Desarrollé la respuesta dream83619 para hacer que maneje las anotaciones Hibernate @Fetch anidadas. Usé el método recursivo para encontrar anotaciones en clases asociadas anidadas.

Por lo tanto, debe implementar el repository personalizado y anular el getQuery(spec, domainClass, sort) . Lamentablemente también tiene que copiar todos los métodos privados a los que se hace referencia :(.

Aquí está el código, los métodos privados copiados se omiten.
EDITAR: Agregó los métodos privados restantes.

 @NoRepositoryBean public class EntityGraphRepositoryImpl extends SimpleJpaRepository { private final EntityManager em; protected JpaEntityInformation entityInformation; public EntityGraphRepositoryImpl(JpaEntityInformation entityInformation, EntityManager entityManager) { super(entityInformation, entityManager); this.em = entityManager; this.entityInformation = entityInformation; } @Override protected  TypedQuery getQuery(Specification spec, Class domainClass, Sort sort) { CriteriaBuilder builder = em.getCriteriaBuilder(); CriteriaQuery query = builder.createQuery(domainClass); Root 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> joinCache; private void applyFetchMode(Root 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  Root applySpecificationToCriteria(Specification spec, Class domainClass, CriteriaQuery query) { Assert.notNull(query); Assert.notNull(domainClass); Root 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  TypedQuery applyRepositoryMethodMetadata(TypedQuery query) { if (getRepositoryMethodMetadata() == null) { return query; } LockModeType type = getRepositoryMethodMetadata().getLockModeType(); TypedQuery toReturn = type == null ? query : query.setLockMode(type); applyQueryHints(toReturn); return toReturn; } private void applyQueryHints(Query query) { for (Map.Entry hint : getQueryHints().entrySet()) { query.setHint(hint.getKey(), hint.getValue()); } } public Class getEntityType() { return entityInformation.getJavaType(); } public EntityManager getEm() { return em; } } 

De acuerdo con Vlad Mihalcea (ver https://vladmihalcea.com/hibernate-facts-the-importance-of-fetch-strategy/ ):

Las consultas JPQL pueden anular la estrategia de búsqueda predeterminada. Si no declaramos explícitamente lo que queremos recuperar utilizando las directivas de búsqueda de unión hacia la izquierda o hacia la izquierda, se aplica la política de selección de búsqueda predeterminada.

Parece que la consulta JPQL puede anular tu estrategia de búsqueda declarada, por lo que tendrás que usar la función de join fetch para cargar ansiosamente alguna entidad referenciada o simplemente cargar por id con EntityManager (que obedecerá tu estrategia de búsqueda pero puede no ser una solución para tu caso de uso).

http://jdpgrailsdev.github.io/blog/2014/09/09/spring_data_hibernate_join.html
de este enlace:

Si está utilizando JPA en la parte superior de Hibernate, no hay forma de configurar el modo de captura utilizado por Hibernate para UNIRSE. Sin embargo, si está utilizando JPA encima de Hibernate, no hay forma de configurar el modo de captura utilizado por Hibernate para JOIN.

La biblioteca Spring Data JPA proporciona una API de especificaciones de diseño controladas por el dominio que le permite controlar el comportamiento de la consulta generada.

 final long userId = 1; final Specification spec = new Specification() { @Override public Predicate toPredicate(final Root root, final CriteriaQuery query, final CriteriaBuilder cb) { query.distinct(true); root.fetch("permissions", JoinType.LEFT); return cb.equal(root.get("id"), userId); } }; List users = userRepository.findAll(spec); 

El modo de búsqueda solo funcionará al seleccionar el objeto por id, es decir, utilizando entityManager.find() . Dado que Spring Data siempre creará una consulta, la configuración del modo de búsqueda no te servirá. Puede usar consultas dedicadas con fetch joins o usar gráficos de entidades.

Cuando desee obtener el mejor rendimiento, debe seleccionar solo el subconjunto de los datos que realmente necesita. Para hacer esto, generalmente se recomienda usar un enfoque DTO para evitar que se obtengan datos innecesarios, pero eso generalmente resulta en un código repetitivo propenso a errores, ya que necesita definir una consulta dedicada que construye su modelo DTO a través de un JPQL expresión constructora

Las proyecciones de Spring Data pueden ser útiles aquí, pero en algún momento necesitarás una solución como Blaze-Persistence Entity Views que hace que esto sea bastante fácil y tiene muchas más características en su manga que te serán útiles. Simplemente crea una interfaz DTO por entidad donde los getters representan el subconjunto de datos que necesita. Una solución a su problema podría verse así

 @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, PlaceRepositoryCustom { PlaceView findById(int id); } public interface UserRepository extends JpaRepository { List findAllByOrderByIdAsc(); UserView findById(int id); } public interface CityRepository extends JpaRepository, CityRepositoryCustom { CityView findById(int id); } 

Descargo de responsabilidad, soy el autor de Blaze-Persistence, por lo que podría ser parcial.