Comment récupérer les associations FetchType.LAZY avec JPA et Hibernate dans un Spring Controller

146

J'ai une classe Person:

@Entity
public class Person {

    @Id
    @GeneratedValue
    private Long id;

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

Avec une relation plusieurs-à-plusieurs qui est paresseuse.

Dans mon contrôleur, j'ai

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

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

Et le PersonRepository n'est que ce code, écrit selon ce guide

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

Cependant, dans ce contrôleur, j'ai réellement besoin des données paresseuses. Comment puis-je déclencher son chargement?

Essayer d'y accéder échouera avec

échec de l'initialisation paresseuse d'une collection de rôles: no.dusken.momus.model.Person.roles, impossible d'initialiser le proxy - pas de session

ou d'autres exceptions selon ce que j'essaye.

Ma description xml , en cas de besoin.

Merci.

Matsemann
la source
Pouvez-vous écrire une méthode qui créera une requête pour récupérer un Personobjet en fonction d'un paramètre? En cela Query, incluez la fetchclause et chargez le Rolestrop pour la personne.
SudoRahul

Réponses:

206

Vous devrez faire un appel explicite sur la collection paresseuse afin de l'initialiser (la pratique courante est d'appeler .size()dans ce but). Dans Hibernate, il existe une méthode dédiée pour this ( Hibernate.initialize()), mais JPA n'a pas d'équivalent. Bien sûr, vous devrez vous assurer que l'invocation est effectuée, lorsque la session est encore disponible, alors annotez votre méthode de contrôleur avec @Transactional. Une alternative consiste à créer une couche de service intermédiaire entre le contrôleur et le référentiel qui pourrait exposer des méthodes qui initialisent des collections différées.

Mettre à jour:

Veuillez noter que la solution ci-dessus est simple, mais entraîne deux requêtes distinctes dans la base de données (une pour l'utilisateur, une autre pour ses rôles). Si vous souhaitez obtenir de meilleures performances, ajoutez la méthode suivante à votre interface de référentiel 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);

}

Cette méthode utilisera la clause fetch join de JPQL pour charger avec empressement l'association de rôles en un seul aller-retour vers la base de données, et atténuera donc la pénalité de performances encourue par les deux requêtes distinctes dans la solution ci-dessus.

Zagyi
la source
3
Veuillez noter que c'est une solution simple, mais qui entraîne deux requêtes distinctes dans la base de données (une pour l'utilisateur, une autre pour ses rôles). Si vous souhaitez obtenir de meilleures performances, essayez d'écrire une méthode dédiée qui récupère avec empressement l'utilisateur et ses rôles associés en une seule étape à l'aide de JPQL ou de l'API Criteria comme d'autres l'ont suggéré.
zagyi
J'ai maintenant demandé un exemple à la réponse de Jose, je dois admettre que je ne comprends pas entièrement.
Matsemann
Veuillez vérifier une solution possible pour la méthode de requête souhaitée dans ma réponse mise à jour.
zagyi
7
Chose intéressante à noter, si vous n'avez simplement joinpas le fetch, l'ensemble sera retourné avec initialized = false; émettant donc toujours une deuxième requête une fois que l'ensemble est accédé. fetchest la clé pour s'assurer que la relation est complètement chargée et éviter la deuxième requête.
FGreg
Il semble que le problème avec les deux et l'extraction et une jointure est que les critères de prédicat de jointure sont ignorés et que vous finissez par obtenir tout dans la liste ou la carte. Si vous voulez tout, utilisez une extraction, si vous voulez quelque chose de spécifique, puis une jointure, mais comme cela a été dit, la jointure sera vide. Cela va à l'encontre de l'objectif du chargement .LAZY.
K.Nicholas
37

Bien qu'il s'agisse d'un ancien article, veuillez envisager d'utiliser @NamedEntityGraph (Javax Persistence) et @EntityGraph (Spring Data JPA). La combinaison fonctionne.

Exemple

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

puis le repo de printemps comme ci-dessous

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

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

}
rakpan
la source
1
Notez que cela @NamedEntityGraphfait partie de l'API JPA 2.1, qui n'est pas implémentée dans Hibernate avant la version 4.3.0.
naXa
2
@EntityGraph(attributePaths = "employeeGroups")peut être utilisé directement dans un référentiel de données Spring pour annoter une méthode sans avoir besoin d'un @NamedEntityGraphsur votre @Entity - moins de code, facile à comprendre lorsque vous ouvrez le référentiel.
Desislav Kamenov
13

Vous avez quelques options

  • Écrivez une méthode sur le référentiel qui retourne une entité initialisée comme RJ l'a suggéré.

Plus de travail, meilleures performances.

  • Utilisez OpenEntityManagerInViewFilter pour garder la session ouverte pour toute la demande.

Moins de travail, généralement acceptable dans les environnements Web.

  • Utilisez une classe d'assistance pour initialiser les entités si nécessaire.

Moins de travail, utile lorsque OEMIV n'est pas en option, par exemple dans une application Swing, mais peut également être utile sur les implémentations de référentiel pour initialiser toute entité en un seul coup.

Pour la dernière option, j'ai écrit une classe utilitaire, JpaUtils pour initialiser les entités à une certaine profondeur.

Par exemple:

@Transactional
public class RepositoryHelper {

    @PersistenceContext
    private EntityManager em;

    public void intialize(Object entity, int depth) {
        JpaUtils.initialize(em, entity, depth);
    }
}
José Luis Martin
la source
Puisque toutes mes demandes sont de simples appels REST sans rendu, etc., la transaction est essentiellement toute ma demande. Merci pour votre contribution.
Matsemann
Comment faire le premier? Je sais comment rédiger une requête, mais pas comment faire ce que vous dites. Pourriez-vous s'il vous plaît montrer un exemple? Serait très utile.
Matsemann
zagyi a fourni un exemple dans sa réponse, merci de m'avoir quand même pointé dans la bonne direction.
Matsemann
Je ne sais pas comment votre classe s'appellerait! solution non terminée perdre le temps des autres
Shady Sherif
Utilisez OpenEntityManagerInViewFilter pour garder la session ouverte pour toute la demande. - Mauvaise idée. Je ferais une demande supplémentaire pour récupérer toutes les collections de mes entités.
Yan Khonski le
6

Je pense que vous avez besoin d' OpenSessionInViewFilter pour garder votre session ouverte pendant le rendu de la vue (mais ce n'est pas une très bonne pratique).

Michail Nikolaev
la source
1
Comme je n'utilise pas JSP ou quoi que ce soit, je crée juste une API REST, @Transactional fera l'affaire pour moi. Mais sera utile à d'autres moments. Merci.
Matsemann
@Matsemann Je sais qu'il est tard maintenant ... mais vous pouvez utiliser OpenSessionInViewFilter même dans un contrôleur et la session existera jusqu'à ce qu'une réponse soit compilée ...
Vishwas Shashidhar
@Matsemann Merci! L'annotation transactionnelle a fait l'affaire pour moi! fyi: Cela fonctionne même si vous annotez simplement la superclasse d'une classe de repos.
desperateCoder
3

Données de printemps JpaRepository

The Spring Data JpaRepositorydéfinit les deux méthodes suivantes:

  • getOne, qui renvoie un proxy d'entité adapté à la définition d'une association @ManyToOneou @OneToOneparente lors de la persistance d'une entité enfant .
  • findById, qui renvoie l'entité POJO après l'exécution de l'instruction SELECT qui charge l'entité à partir de la table associée

Cependant, dans votre cas, vous n'avez appelé ni getOneni findById:

Person person = personRepository.findOne(1L);

Donc, je suppose que le findOne méthode est une méthode que vous avez définie dans le PersonRepository. Cependant, la findOneméthode n'est pas très utile dans votre cas. Puisque vous devez récupérer la collection Personavec is roles, il est préférable d'utiliser une findOneWithRolesméthode à la place.

Méthodes de données Spring personnalisées

Vous pouvez définir une PersonRepositoryCustominterface comme suit:

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

}

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

Et définissez sa mise en œuvre comme ceci:

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();
    }
}

C'est tout!

Vlad Mihalcea
la source
Y a-t-il une raison pour laquelle vous avez écrit la requête vous-même et n'avez pas utilisé une solution comme EntityGraph dans @rakpan answer? Cela ne produirait-il pas le même résultat?
Jeroen Vandevelde
La surcharge pour l'utilisation d'un EntityGraph est plus élevée qu'une requête JPQL. Sur le long terme, il vaut mieux écrire la requête.
Vlad Mihalcea
Pouvez-vous élaborer sur les frais généraux (d'où vient-il, est-il perceptible, ...)? Parce que je ne comprends pas pourquoi il y a une surcharge plus élevée s'ils génèrent tous les deux la même requête.
Jeroen Vandevelde
1
Parce que les plans EntityGraphs ne sont pas mis en cache comme le sont JPQL. Cela peut être un coup dur pour les performances.
Vlad Mihalcea
1
Exactement. Je devrai écrire un article à ce sujet quand j'aurai le temps.
Vlad Mihalcea
1

Vous pouvez faire la même chose comme ceci:

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

Utilisez simplement faqQuestions.getFaqAnswers (). Size () nin votre contrôleur et vous obtiendrez la taille si la liste initialisée paresseusement, sans récupérer la liste elle-même.

neel4soft
la source