¿Cómo funciona FetchMode en Spring Data JPA?

95

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

Cuando lo llamo PlaceRepository.findById, activa tres consultas seleccionadas:

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

Ese 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 consultas JOIN. No hay diferencia en las consultas cuando se FetchType.LAZYcambia a FetchType.EAGERen la Placeclase (consulta con SELECT adicional), lo mismo para la Cityclase cuando se FetchType.LAZYcambia a FetchType.EAGER(consulta con JOIN).

Cuando uso la CityRepository.findByIdsupresión de incendios dos selecciones:

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

Mi objetivo es tener un comportamiento similar al sam en todas las situaciones (ya sea siempre JOIN o SELECT, aunque se prefiere JOIN).

Definiciones de modelo:

Sitio:

@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<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
fuente
Hava una mirada a 5 formas de inicializar relaciones perezosas: pensamientos-on-java.org/…
Grigory Kislin

Respuestas:

114

Creo que Spring Data ignora FetchMode. Siempre uso las anotaciones @NamedEntityGraphy @EntityGraphcuando trabajo con 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);

}

Consulta la documentación aquí

wesker317
fuente
1
No parece que funcione para mí. Quiero decir que funciona, pero ... Cuando anoto el repositorio con '@EntityGraph', no funciona solo (normalmente). Por ejemplo: `Place findById (int id);` funciona pero List<Place> findAll();termina con la excepción org.springframework.data.mapping.PropertyReferenceException: No property find found for type Place!. Funciona cuando agrego manualmente @Query("select p from Place p"). Sin embargo, parece una solución alternativa.
SirKometa
Tal vez no funcione en findAll () ya que es un método existente de la interfaz JpaRepository mientras que su otro método "findById" es un método de consulta personalizado generado en tiempo de ejecución.
wesker317
Decidí marcar esto como la respuesta adecuada ya que es la mejor. Aunque no es perfecto. Funciona en la mayoría de los escenarios, pero hasta ahora he notado errores en spring-data-jpa con EntityGraphs más complejos. Gracias :)
SirKometa
2
@EntityGraphes casi ununsable en escenarios reales, ya que no puede especificar qué tipo de Fetchque queremos utilizar ( JOIN, SUBSELECT, SELECT, BATCH). Esto en combinación con la @OneToManyasociación hace que Hibernate recupere la tabla completa en la memoria incluso si usamos query MaxResults.
Ondrej Bozek
1
Gracias, quería decir que las consultas JPQL pueden anular la estrategia de recuperación predeterminada con la política de recuperación seleccionada .
adrhc
53

En primer lugar, @Fetch(FetchMode.JOIN)y @ManyToOne(fetch = FetchType.LAZY)son antagónicos, uno instruye a una búsqueda EAGER, 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 que utilice la JOIN FETCHdirectiva de tiempo de consulta :

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);
}
Vlad Mihalcea
fuente
3
¿Hay alguna manera de lograr el mismo resultado con Criteria API y Spring Data Especificaciones?
svlada
2
No es la parte de recuperación, que requiere perfiles de recuperación de JPA.
Vlad Mihalcea
Vlad Mihalcea, ¿podría compartir el enlace con un ejemplo de cómo hacer esto usando los criterios de Spring Data JPA (especificación)? Por favor
Yan Khonski
No tengo ningún ejemplo de este tipo, pero seguramente puede encontrar uno en los tutoriales de Spring Data JPA.
Vlad Mihalcea
si usa query-time ..... ¿todavía necesitará definir @OneToMany ... etc en la entidad?
Eric Huang
19

Spring-jpa crea la consulta usando el administrador de entidades, e Hibernate ignorará el modo de recuperación si la consulta fue construida por el administrador de entidades.

La siguiente es la solución que utilicé:

  1. Implementar un repositorio personalizado que herede de SimpleJpaRepository

  2. Anule el método 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));
    }
    

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

    (Desafortunadamente, necesitamos copiar todo el método 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<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);
            }
        }
    }
    
sueño83619
fuente
Desafortunadamente, esto no funciona para consultas generadas usando el nombre del método de repositorio.
Ondrej Bozek
¿Podría agregar todas las declaraciones de importación? gracias.
granadaCoder
3

" FetchType.LAZY" solo se activará para la mesa principal. Si en su código llama a cualquier otro método que tenga una dependencia de tabla principal, activará la consulta para obtener la información de esa tabla. (DISPARA SELECCIÓN MÚLTIPLE)

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

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

Recuerde, FetchType.LAZYnecesita una fábrica de sesiones de base de datos activa en el lugar de su código donde si elige recuperar la información de la tabla principal.

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

Godwin
fuente
Curiosamente, esta respuesta me puso en el camino correcto para usarlo, NamedEntityGraphya que quería un gráfico de objetos no hidratados.
JJ Zabkar
esta respuesta merece más votos a favor. Es conciso y me ayudó mucho a entender por qué estaba viendo muchas consultas "activadas mágicamente" ... ¡muchas gracias!
Clint Eastwood
3

El modo de recuperación solo funcionará cuando se seleccione el objeto por id, es decir, usando entityManager.find(). Dado que Spring Data siempre creará una consulta, la configuración del modo de recuperación no le servirá de nada. Puede utilizar consultas dedicadas con fetch joins o utilizar gráficos de entidad.

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

Las proyecciones de Spring Data pueden ayudar aquí, pero en algún momento necesitará una solución como Blaze-Persistence Entity Views que lo hace bastante fácil y tiene muchas más funciones en su funda que serán útiles. Simplemente crea una interfaz DTO por entidad donde los captadores 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<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);
}

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

Christian Beikov
fuente
2

Elaboré en dream83619 respuesta para que sea manejar Hibernate anidados @Fetchanotaciones. Utilicé el método recursivo para encontrar anotaciones en clases asociadas anidadas.

Por lo tanto, debe implementar un repositorio personalizado y un getQuery(spec, domainClass, sort)método de anulación . Desafortunadamente, también debe copiar todos los métodos privados referenciados :(.

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

@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;
    }
}
Ondrej Bozek
fuente
Estoy probando su solución, pero tengo una variable de metadatos privada en uno de los métodos para copiar que está dando problemas. ¿Puedes compartir el código final?
Homer1980ar
La recuperación recursiva no funciona. Si tengo OneToMany, pasa java.util.List a la siguiente iteración
antohoho
aún no lo he probado bien, pero creo que debería ser algo como esto ((Join) descent) .getJavaType () en lugar de field.getType () cuando se llama de forma recursiva applyFetchMode
antohoho
2

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

Si está usando JPA sobre Hibernate, no hay forma de establecer el FetchMode usado por Hibernate en JOIN Sin embargo, si está usando JPA sobre Hibernate, no hay forma de establecer el FetchMode usado por Hibernate en JOIN.

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

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);
kafkas
fuente
2

Según 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 mediante las directivas de recuperación de combinación interna o izquierda, se aplica la política de recuperación de selección predeterminada.

Parece que la consulta JPQL podría anular su estrategia de búsqueda declarada, por lo que tendrá que usarla join fetchpara cargar con entusiasmo alguna entidad referenciada o simplemente cargar por id con EntityManager (que obedecerá su estrategia de búsqueda, pero podría no ser una solución para su caso de uso ).

adrhc
fuente