Cómo obtener asociaciones FetchType.LAZY con JPA e Hibernate en un controlador Spring

146

Tengo una clase de persona:

@Entity
public class Person {

    @Id
    @GeneratedValue
    private Long id;

    @ManyToMany(fetch = FetchType.LAZY)
    private List<Role> roles;
    // etc
}

Con una relación de muchos a muchos que es vago.

En mi controlador tengo

@Controller
@RequestMapping("/person")
public class PersonController {
    @Autowired
    PersonRepository personRepository;

    @RequestMapping("/get")
    public @ResponseBody Person getPerson() {
        Person person = personRepository.findOne(1L);
        return person;
    }
}

Y el PersonRepository es solo este código, escrito de acuerdo con esta guía

public interface PersonRepository extends JpaRepository<Person, Long> {
}

Sin embargo, en este controlador realmente necesito los datos perezosos. ¿Cómo puedo activar su carga?

Intentar acceder fallará con

no se pudo inicializar perezosamente una colección de roles: no.dusken.momus.model.Person.roles, no se pudo inicializar el proxy: sin sesión

u otras excepciones según lo que intente.

Mi descripción xml , en caso de que sea necesario.

Gracias.

Matsemann
fuente
¿Puedes escribir un método que cree una consulta para buscar un Personobjeto dado algún parámetro? En eso Query, incluya la fetchcláusula y cargue Rolestambién para la persona.
SudoRahul

Respuestas:

206

Tendrá que hacer una llamada explícita a la colección perezosa para inicializarla (la práctica común es llamar .size()para este propósito). En Hibernate hay un método dedicado para esto ( Hibernate.initialize()), pero JPA no tiene equivalente. Por supuesto, tendrá que asegurarse de que se realiza la invocación, cuando la sesión todavía está disponible, así que anote su método de controlador con @Transactional. Una alternativa es crear una capa de Servicio intermedia entre el Controlador y el Repositorio que podría exponer métodos que inicializan colecciones diferidas.

Actualizar:

Tenga en cuenta que la solución anterior es fácil, pero da como resultado dos consultas distintas a la base de datos (una para el usuario y otra para sus funciones). Si desea lograr un mejor rendimiento, agregue el siguiente método a su interfaz de repositorio Spring Data JPA:

public interface PersonRepository extends JpaRepository<Person, Long> {

    @Query("SELECT p FROM Person p JOIN FETCH p.roles WHERE p.id = (:id)")
    public Person findByIdAndFetchRolesEagerly(@Param("id") Long id);

}

Este método utilizará la cláusula fetch join de JPQL para cargar con entusiasmo la asociación de roles en un solo viaje de ida y vuelta a la base de datos y, por lo tanto, mitigará la penalización de rendimiento incurrida por las dos consultas distintas en la solución anterior.

zagyi
fuente
3
Tenga en cuenta que esta es una solución fácil, pero da como resultado dos consultas distintas a la base de datos (una para el usuario y otra para sus funciones). Si desea lograr un mejor rendimiento, intente escribir un método dedicado que busque con entusiasmo al usuario y sus roles asociados en un solo paso usando JPQL o Criteria API como otros sugirieron.
zagyi
Ahora pedí un ejemplo a la respuesta de José, tengo que admitir que no entiendo completamente.
Matsemann
Verifique una posible solución para el método de consulta deseado en mi respuesta actualizada.
zagyi
77
Una cosa interesante a tener en cuenta, si simplemente joinsin el fetch, el conjunto será devuelto con initialized = false; por lo tanto, sigue emitiendo una segunda consulta una vez que se accede al conjunto. fetches clave para asegurarse de que la relación esté completamente cargada y evitar la segunda consulta.
FGreg
Parece que el problema con hacer ambas cosas, buscar y unir es que se ignoran los criterios de predicado de unión y terminas obteniendo todo en la lista o mapa. Si quiere todo, use una búsqueda, si quiere algo específico, luego una unión, pero, como se dijo, la unión estará vacía. Esto anula el propósito de usar la carga .LAZY.
K.Nicholas
37

Aunque esta es una publicación antigua, considere usar @NamedEntityGraph (Javax Persistence) y @EntityGraph (Spring Data JPA). La combinación funciona.

Ejemplo

@Entity
@Table(name = "Employee", schema = "dbo", catalog = "ARCHO")
@NamedEntityGraph(name = "employeeAuthorities",
            attributeNodes = @NamedAttributeNode("employeeGroups"))
public class EmployeeEntity implements Serializable, UserDetails {
// your props
}

y luego el repositorio de primavera como a continuación

@RepositoryRestResource(collectionResourceRel = "Employee", path = "Employee")
public interface IEmployeeRepository extends PagingAndSortingRepository<EmployeeEntity, String>           {

    @EntityGraph(value = "employeeAuthorities", type = EntityGraphType.LOAD)
    EmployeeEntity getByUsername(String userName);

}
rakpan
fuente
1
Tenga en cuenta que @NamedEntityGraphes parte de la API JPA 2.1, que no está implementada en Hibernate antes de la versión 4.3.0.
naXa
2
@EntityGraph(attributePaths = "employeeGroups")puede usarse directamente en un repositorio de datos de Spring para anotar un método sin la necesidad de un @NamedEntityGraphcódigo sin @Entity, fácil de entender cuando abre el repositorio.
Desislav Kamenov el
13

Tienes algunas opciones

  • Escriba un método en el repositorio que devuelva una entidad inicializada como sugirió RJ.

Más trabajo, mejor rendimiento.

  • Use OpenEntityManagerInViewFilter para mantener la sesión abierta para toda la solicitud.

Menos trabajo, generalmente aceptable en entornos web.

  • Use una clase auxiliar para inicializar entidades cuando sea necesario.

Menos trabajo, útil cuando OEMIV no está en opción, por ejemplo en una aplicación Swing, pero también puede ser útil en implementaciones de repositorio para inicializar cualquier entidad de una sola vez.

Para la última opción, escribí una clase de utilidad, JpaUtils para inicializar entidades en alguna definición.

Por ejemplo:

@Transactional
public class RepositoryHelper {

    @PersistenceContext
    private EntityManager em;

    public void intialize(Object entity, int depth) {
        JpaUtils.initialize(em, entity, depth);
    }
}
Jose luis martin
fuente
Como todas mis solicitudes son llamadas REST simples sin representación, etc., la transacción es básicamente mi solicitud completa. Gracias por tu contribución.
Matsemann
¿Cómo hago el primero? Sé cómo escribir una consulta, pero no cómo hacer lo que dices. ¿Podría por favor mostrar un ejemplo? Sería muy útil
Matsemann
Sin embargo, Zagyi proporcionó un ejemplo en su respuesta, gracias por señalarme en la dirección correcta.
Matsemann
¡No sé cómo se llamaría tu clase! solución no completada desperdicia el tiempo de otros
Shady Sherif
Use OpenEntityManagerInViewFilter para mantener la sesión abierta para toda la solicitud.- Mala idea. Haría una solicitud adicional para buscar todas las colecciones para mis entidades.
Yan Khonski
6

Creo que necesita OpenSessionInViewFilter para mantener su sesión abierta durante el renderizado de vistas (pero no es una buena práctica).

Michail Nikolaev
fuente
1
Como no estoy usando JSP ni nada, solo estoy haciendo una REST-api, @Transactional lo hará por mí. Pero será útil en otros momentos. Gracias.
Matsemann
@Matsemann Sé que es tarde ahora ... pero puedes usar OpenSessionInViewFilter incluso en un controlador y la sesión existirá hasta que se compile una respuesta ...
Vishwas Shashidhar
@ Matsemann Gracias! ¡La anotación transaccional me sirvió! FYI: Incluso funciona si solo anotas la superclase de una clase de descanso.
DesesperadoCoder
3

Datos de primavera JpaRepository

Spring Data JpaRepositorydefine los siguientes dos métodos:

  • getOne, que devuelve un proxy de entidad adecuado para establecer un@ManyToOne@OneToOne asociación principal o principal cuando persiste una entidad secundaria .
  • findById, que devuelve la entidad POJO después de ejecutar la instrucción SELECT que carga la entidad desde la tabla asociada

Sin embargo, en su caso, que no llama a cualquiera getOneo findById:

Person person = personRepository.findOne(1L);

Entonces, supongo que el findOnemétodo es un método que definiste en el PersonRepository. Sin embargo, el findOnemétodo no es muy útil en su caso. Como debe buscar la colección Personjunto con is roles, es mejor usar un findOneWithRolesmétodo.

Métodos personalizados de Spring Data

Puede definir una PersonRepositoryCustominterfaz de la siguiente manera:

public interface PersonRepository
    extends JpaRepository<Person, Long>, PersonRepositoryCustom { 

}

public interface PersonRepositoryCustom {
    Person findOneWithRoles(Long id);
}

Y defina su implementación de esta manera:

public class PersonRepositoryImpl implements PersonRepositoryCustom {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public Person findOneWithRoles(Long id)() {
        return entityManager.createQuery("""
            select p 
            from Person p
            left join fetch p.roles
            where p.id = :id 
            """, Person.class)
        .setParameter("id", id)
        .getSingleResult();
    }
}

¡Eso es!

Vlad Mihalcea
fuente
¿Hay alguna razón por la que escribió la consulta usted mismo y no utilizó una solución como EntityGraph en la respuesta @rakpan? ¿No produciría esto el mismo resultado?
Jeroen Vandevelde
La sobrecarga para usar un EntityGraph es mayor que una consulta JPQL. A la larga, es mejor escribir la consulta.
Vlad Mihalcea
¿Puedes dar más detalles sobre la sobrecarga (de dónde viene, es notable, ...)? Porque no entiendo por qué hay una sobrecarga mayor si ambos generan la misma consulta.
Jeroen Vandevelde
1
Porque los planes de EntityGraphs no se almacenan en caché como JPQL. Eso puede ser un éxito significativo en el rendimiento.
Vlad Mihalcea
1
Exactamente. Tendré que escribir un artículo al respecto cuando tenga algo de tiempo.
Vlad Mihalcea
1

Puedes hacer lo mismo así:

@Override
public FaqQuestions getFaqQuestionById(Long questionId) {
    session = sessionFactory.openSession();
    tx = session.beginTransaction();
    FaqQuestions faqQuestions = null;
    try {
        faqQuestions = (FaqQuestions) session.get(FaqQuestions.class,
                questionId);
        Hibernate.initialize(faqQuestions.getFaqAnswers());

        tx.commit();
        faqQuestions.getFaqAnswers().size();
    } finally {
        session.close();
    }
    return faqQuestions;
}

Simplemente use faqQuestions.getFaqAnswers (). Size () en su controlador y obtendrá el tamaño de la lista perezosamente inicializada, sin obtener la lista en sí.

neel4soft
fuente