Come recuperare le associazioni FetchType.LAZY con JPA e Hibernate in un controller Spring


146

Ho una classe Person:

@Entity
public class Person {

    @Id
    @GeneratedValue
    private Long id;

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

Con una relazione molti-a-molti che è pigro.

Nel mio controller ho

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

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

E il PersonRepository è proprio questo codice, scritto secondo questa guida

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

Tuttavia, in questo controller ho effettivamente bisogno dei dati pigri. Come posso attivare il suo caricamento?

Cercare di accedervi fallirà

impossibile inizializzare pigramente una raccolta di ruoli: no.dusken.momus.model.Person.roles, impossibile inizializzare il proxy - nessuna sessione

o altre eccezioni a seconda di ciò che provo.

La mia descrizione xml , se necessario.

Grazie.


Puoi scrivere un metodo, che creerà una query per recuperare un Personoggetto dati alcuni parametri? In questo Query, includi la fetchclausola e carica Rolesanche quella per la persona.
SudoRahul,

Risposte:


206

Dovrai effettuare una chiamata esplicita sulla raccolta pigra per inizializzarla (la pratica comune è quella di chiamare .size()per questo scopo). In Hibernate esiste un metodo dedicato per questo ( Hibernate.initialize()), ma JPA non ha equivalenti. Ovviamente dovrai assicurarti che l'invocazione sia terminata, quando la sessione è ancora disponibile, quindi annota il tuo metodo controller @Transactional. Un'alternativa è quella di creare un livello di servizio intermedio tra il controller e il repository che potrebbe esporre metodi che inizializzano raccolte pigre.

Aggiornare:

Si noti che la soluzione sopra è semplice, ma comporta due query distinte nel database (una per l'utente, un'altra per i suoi ruoli). Se si desidera ottenere prestazioni migliori, aggiungere il seguente metodo all'interfaccia del repository JPA Spring Data:

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

}

Questo metodo utilizzerà JPQL clausola fetch join di per caricare con impazienza l'associazione dei ruoli in un singolo round-trip nel database e quindi mitigherà la penalità di prestazione sostenuta dalle due query distinte nella soluzione precedente.


3
Questa è una soluzione semplice, ma comporta due query distinte nel database (una per l'utente, un'altra per i suoi ruoli). Se vuoi ottenere prestazioni migliori, prova a scrivere un metodo dedicato che recuperi avidamente l'utente e i suoi ruoli associati in un unico passaggio utilizzando JPQL o l'API Criteria come altri hanno suggerito.
zagyi,

Ora ho chiesto un esempio per la risposta di Jose, devo ammettere che non capisco del tutto.
Matsemann,

Controlla una possibile soluzione per il metodo di query desiderato nella mia risposta aggiornata.
zagyi,

7
Cosa interessante da notare, se semplicemente joinsenza il fetch, il set verrà restituito con initialized = false; pertanto emette ancora una seconda query una volta effettuato l'accesso al set. fetchè la chiave per assicurarsi che la relazione sia completamente caricata ed evitare la seconda query.
FGreg

Sembra che il problema nel fare entrambi e recuperare e unire sia che i criteri del predicato di join vengono ignorati e si finisce per ottenere tutto nell'elenco o nella mappa. Se vuoi tutto, usa un fetch, se vuoi qualcosa di specifico, quindi un join, ma, come detto, il join sarà vuoto. Ciò vanifica lo scopo dell'utilizzo del caricamento .LAZY.
K. Nicholas

37

Anche se questo è un vecchio post, considera l'utilizzo di @NamedEntityGraph (Javax Persistence) e @EntityGraph (Spring Data JPA). La combinazione funziona.

Esempio

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

e quindi il repository primavera come di seguito

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

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

}

1
Si noti che @NamedEntityGraphfa parte dell'API JPA 2.1, che non è implementata in Hibernate prima della versione 4.3.0.
naXa,

2
@EntityGraph(attributePaths = "employeeGroups")può essere utilizzato direttamente in un repository di dati di primavera per annotare un metodo senza la necessità di un @NamedEntityGraphcodice @Entity, meno codice, facile da capire quando si apre il repository.
Desislav Kamenov,

13

Hai alcune opzioni

  • Scrivi un metodo sul repository che restituisce un'entità inizializzata come suggerito da RJ.

Più lavoro, migliori prestazioni.

  • Utilizzare OpenEntityManagerInViewFilter per mantenere aperta la sessione per l'intera richiesta.

Meno lavoro, generalmente accettabile negli ambienti web.

  • Utilizzare una classe di supporto per inizializzare le entità quando richiesto.

Meno lavoro, utile quando OEMIV non è disponibile, ad esempio in un'applicazione Swing, ma può essere utile anche nelle implementazioni del repository per inizializzare qualsiasi entità in un colpo solo.

Per l'ultima opzione, ho scritto una classe di utilità, JpaUtils per inizializzare le entità a qualche profondità.

Per esempio:

@Transactional
public class RepositoryHelper {

    @PersistenceContext
    private EntityManager em;

    public void intialize(Object entity, int depth) {
        JpaUtils.initialize(em, entity, depth);
    }
}

Poiché tutte le mie richieste sono semplici chiamate REST senza rendering ecc., La transazione è sostanzialmente la mia intera richiesta. Grazie per il tuo contributo.
Matsemann,

Come faccio il primo? So come scrivere una query, ma non come fare quello che dici. Potresti per favore mostrare un esempio? Sarebbe molto utile
Matsemann,

zagyi ha fornito un esempio nella sua risposta, grazie comunque per avermi indicato nella giusta direzione.
Matsemann,

Non so come si chiamerebbe la tua classe! la soluzione non completata fa perdere altro tempo
Shady Sherif,

Utilizzare OpenEntityManagerInViewFilter per mantenere aperta la sessione per l'intera richiesta. - Cattiva idea. Vorrei fare una richiesta aggiuntiva per recuperare tutte le raccolte per le mie entità.
Yan Khonski,


6

Penso che sia necessario OpenSessionInViewFilter per mantenere aperta la sessione durante il rendering della vista (ma non è una buona pratica).


1
Poiché non sto usando JSP o altro, sto solo facendo un REST-api, @Transactional farà per me. Ma sarà utile altre volte. Grazie.
Matsemann,

@Matsemann So che è tardi ora ... ma puoi usare OpenSessionInViewFilter anche in un controller e la sessione esisterà fino a quando non verrà compilata una risposta ...
Vishwas Shashidhar

@Matsemann Grazie! L'annotazione transazionale ha fatto il trucco per me! A proposito: funziona anche se si annota semplicemente la superclasse di una classe di riposo.
desperateCoder

3

Dati di primavera JpaRepository

I dati di primavera JpaRepositorydefiniscono i seguenti due metodi:

  • getOne, che restituisce un proxy di entità adatto per l'impostazione di un'associazione principale @ManyToOneo quando persiste un'entità secondaria .@OneToOne
  • findById, che restituisce l'entità POJO dopo aver eseguito l'istruzione SELECT che carica l'entità dalla tabella associata

Tuttavia, nel tuo caso, non hai chiamato sia getOneo findById:

Person person = personRepository.findOne(1L);

Quindi, presumo che il findOnemetodo sia un metodo che hai definito in PersonRepository. Tuttavia, il findOnemetodo non è molto utile nel tuo caso. Dato che devi recuperare Personinsieme a is rolescollection, è meglio usare afindOneWithRoles metodo.

Metodi dati primavera personalizzati

È possibile definire PersonRepositoryCustomun'interfaccia, come segue:

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

}

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

E definirne l'implementazione in questo modo:

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

Questo è tutto!


C'è un motivo per cui hai scritto tu stesso la query e non hai usato una soluzione come EntityGraph nella risposta di @rakpan? Questo non produrrebbe lo stesso risultato?
Jeroen Vandevelde,

Il sovraccarico per l'utilizzo di EntityGraph è maggiore di una query JPQL. A lungo termine, è meglio scrivere la query.
Vlad Mihalcea,

Puoi approfondire le spese generali (da dove viene, è evidente, ...)? Causa Non capisco perché vi sia un sovraccarico maggiore se entrambi generano la stessa query.
Jeroen Vandevelde

1
Perché i piani EntityGraphs non sono memorizzati nella cache come lo sono JPQL. Questo può essere un notevole successo in termini di prestazioni.
Vlad Mihalcea,

1
Esattamente. Dovrò scrivere un articolo a riguardo quando avrò del tempo.
Vlad Mihalcea

1

Puoi fare lo stesso in questo modo:

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

Basta usare faqQuestions.getFaqAnswers (). Size () nin il tuo controller e otterrai le dimensioni se l'elenco è pigramente integrato, senza recuperare l'elenco stesso.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.