java - example - spring data jpa jpql
¿Cómo funciona FetchMode en Spring Data JPA? (8)
Tengo una relación entre tres objetos modelo en mi proyecto (fragmentos de modelo y repositorio al final de la publicación.
Cuando llamo a
PlaceRepository.findById
,
PlaceRepository.findById
tres consultas de selección:
("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 debe usar 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
suprimiendo 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 sam en todas las situaciones (siempre unirme o SELECCIONAR, aunque unirme prefiere).
Definiciones del 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<Place, Long>, PlaceRepositoryCustom {
Place findById(int id);
}
UserRepository:
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findAll();
User findById(int id);
}
Depósito de la ciudad:
public interface CityRepository extends JpaRepository<City, Long>, CityRepositoryCustom {
City findById(int id);
}
"
FetchType.LAZY
" solo se activará para la tabla primaria.
Si en su código llama a cualquier otro método que tenga una dependencia de la tabla principal, se activará la consulta para obtener esa información de la tabla.
(INCENDIOS SELECCIÓN MÚLTIPLE)
"
FetchType.EAGER
" creará una combinación de todas las tablas, incluidas las tablas principales relevantes directamente.
(USOS
JOIN
)
Cuándo usar: Supongamos que necesita usar obligatoriamente la información de la tabla principal dependiente y luego elegir
FetchType.EAGER
.
Si solo necesita información para ciertos registros, use
FetchType.LAZY
.
Recuerde,
FetchType.LAZY
necesita una fábrica de sesión 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
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<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 here
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 recuperación no le servirá de nada.
Puede usar consultas dedicadas con combinaciones de búsqueda o usar gráficos de entidad.
Cuando desee el mejor rendimiento, debe seleccionar solo el subconjunto de los datos que realmente necesita. Para hacer esto, generalmente se recomienda utilizar un enfoque DTO para evitar la obtención de datos innecesarios, pero eso generalmente resulta en 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 cree 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.
En primer lugar,
@Fetch(FetchMode.JOIN)
y
@ManyToOne(fetch = FetchType.LAZY)
son antagónicos, uno indica 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 usar la directiva
JOIN FETCH
:
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);
}
Según Vlad Mihalcea (ver https://vladmihalcea.com/hibernate-facts-the-importance-of-fetch-strategy/ ):
Las consultas JPQL pueden anular la estrategia de recuperación predeterminada. Si no declaramos explícitamente lo que queremos recuperar utilizando las directivas de recuperación de unión internas o izquierdas, se aplica la política predeterminada de recuperación de selección.
Parece que la consulta JPQL podría anular su estrategia de
join fetch
declarada, por lo que tendrá que usar
join fetch
para cargar con entusiasmo alguna entidad referenciada o simplemente cargar por id con EntityManager (que obedecerá su estrategia de recuperación pero podría no ser una solución para su caso de uso).
Spring-jpa crea la consulta utilizando el administrador de entidades, e Hibernate ignorará el modo de recuperación si la consulta fue construida por el administrador de entidades.
El siguiente es el trabajo que utilicé:
-
Implemente un repositorio personalizado que la bruja hereda de SimpleJpaRepository
-
getQuery(Specification<T> spec, Sort sort)
el métodogetQuery(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 búsqueda, 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).
-
Implemente
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); } } }
respuesta
para hacer que maneje las anotaciones anidadas de Hibernate
@Fetch
.
Usé un método recursivo para encontrar anotaciones en clases asociadas anidadas.
Por lo tanto, debe implementar un repositorio personalizado
y anular el
getQuery(spec, domainClass, sort)
.
Lamentablemente, también debe copiar todos los métodos privados a los que se hace referencia :(.
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;
}
}
http://jdpgrailsdev.github.io/blog/2014/09/09/spring_data_hibernate_join.html
desde este enlace:
si está utilizando JPA encima de Hibernate, no hay forma de configurar el FetchMode utilizado por Hibernate en JOIN Sin embargo, si está utilizando JPA encima de Hibernate, no hay forma de configurar el FetchMode utilizado por Hibernate en JOIN.
La biblioteca Spring Data JPA proporciona una API de especificaciones de diseño impulsada 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);