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")
SELECT * FROM place p where id = arg
SELECT * FROM user u where u.id = place.user.id
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.LAZY
cambia a FetchType.EAGER
en la Place
clase (consulta con SELECT adicional), lo mismo para la City
clase cuando se FetchType.LAZY
cambia a FetchType.EAGER
(consulta con JOIN).
Cuando uso la CityRepository.findById
supresión de incendios dos selecciones:
SELECT * FROM city c where id = arg
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);
}
Respuestas:
Creo que Spring Data ignora 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<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í
fuente
List<Place> findAll();
termina con la excepciónorg.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.@EntityGraph
es casi ununsable en escenarios reales, ya que no puede especificar qué tipo deFetch
que queremos utilizar (JOIN
,SUBSELECT
,SELECT
,BATCH
). Esto en combinación con la@OneToMany
asociación hace que Hibernate recupere la tabla completa en la memoria incluso si usamos queryMaxResults
.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 FETCH
directiva 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); }
fuente
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é:
Implementar un repositorio personalizado que herede de SimpleJpaRepository
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).
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); } } }
fuente
"
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. (USOSJOIN
)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, utiliceFetchType.LAZY
.Recuerde,
FetchType.LAZY
necesita 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
:Referencia adicional
fuente
NamedEntityGraph
ya que quería un gráfico de objetos no hidratados.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.
fuente
Elaboré en dream83619 respuesta para que sea manejar Hibernate anidados
@Fetch
anotaciones. 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; } }
fuente
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);
fuente
Según Vlad Mihalcea (ver https://vladmihalcea.com/hibernate-facts-the-importance-of-fetch-strategy/ ):
Parece que la consulta JPQL podría anular su estrategia de búsqueda declarada, por lo que tendrá que usarla
join fetch
para 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 ).fuente