Come funziona FetchMode in Spring Data JPA


91

Ho una relazione tra tre oggetti modello nel mio progetto (frammenti di modello e repository alla fine del post.

Quando chiamo PlaceRepository.findById, attiva tre query di selezione:

("sql")

  1. SELECT * FROM place p where id = arg
  2. SELECT * FROM user u where u.id = place.user.id
  3. SELECT * FROM city c LEFT OUTER JOIN state s on c.woj_id = s.id where c.id = place.city.id

Questo è un comportamento piuttosto insolito (per me). Per quanto ne so dopo aver letto la documentazione di Hibernate, dovrebbe sempre utilizzare le query JOIN. Non c'è differenza nelle query quando si FetchType.LAZYcambia in FetchType.EAGERnella Placeclasse (query con SELECT aggiuntivo), lo stesso per la Cityclasse quando si FetchType.LAZYcambia in FetchType.EAGER(query con JOIN).

Quando uso la CityRepository.findByIdsoppressione degli incendi, due selezioni:

  1. SELECT * FROM city c where id = arg
  2. SELECT * FROM state s where id = city.state.id

Il mio obiettivo è quello di avere un comportamento identico in tutte le situazioni (o sempre JOIN o SELECT, ma JOIN è preferibile).

Definizioni del modello:

Posto:

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

Città:

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

Archivi:

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

Hava uno sguardo a 5 modi per inizializzare relazioni pigre: pensieri-on-java.org/…
Grigory Kislin

Risposte:


109

Penso che Spring Data ignori il FetchMode. Uso sempre le annotazioni @NamedEntityGraphe @EntityGraphquando lavoro 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);

}

Controlla la documentazione qui


1
Non sembra lavorare per me. Voglio dire, funziona ma ... Quando annoto il repository con '@EntityGraph' non funziona da solo (di solito). Ad esempio: `Place findById (int id);` funziona ma List<Place> findAll();finisce con l'eccezione org.springframework.data.mapping.PropertyReferenceException: No property find found for type Place!. Funziona quando aggiungo manualmente @Query("select p from Place p"). Sembra però una soluzione alternativa.
SirKometa

Forse funziona bene su findAll () poiché è un metodo esistente dall'interfaccia JpaRepository mentre l'altro metodo "findById" è un metodo di query personalizzato generato in fase di esecuzione.
wesker317

Ho deciso di contrassegnare questa come la risposta corretta poiché è la migliore. Tuttavia non è perfetto. Funziona nella maggior parte degli scenari, ma finora ho notato bug in spring-data-jpa con EntityGraph più complessi. Grazie :)
SirKometa

2
@EntityGraphè quasi ununsable in scenari reali in quanto cant essere specificato che tipo di Fetchvogliamo usare ( JOIN, SUBSELECT, SELECT, BATCH). Questo in combinazione con l' @OneToManyassociazione e rende Hibernate Fetch l'intera tabella in memoria anche se usiamo query MaxResults.
Ondrej Bozek

1
Grazie, volevo dire che le query JPQL possono sostituire la strategia di recupero predefinita con la politica di recupero selezionata .
adrhc

51

Prima di tutto @Fetch(FetchMode.JOIN)e@ManyToOne(fetch = FetchType.LAZY) sono antagonisti, uno istruisce un EAGER a prendere, mentre l'altro suggerisce un PIGRO a prendere.

Il fetching è raramente una buona scelta e per un comportamento prevedibile, è meglio usare la JOIN FETCHdirettiva query-time :

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

3
C'è un modo per ottenere lo stesso risultato con Criteria API e Spring Data Specifications?
svlada

2
Non la parte di recupero, che richiede profili di recupero JPA.
Vlad Mihalcea

Vlad Mihalcea, potresti condividere il link con un esempio di come farlo utilizzando i criteri JPA Spring Data (specifica)? Per favore
Yan Khonski

Non ho alcun esempio del genere, ma puoi sicuramente trovarne uno nei tutorial JPA di Spring Data.
Vlad Mihalcea

se si utilizza query-time ..... sarà comunque necessario definire @OneToMany ... ecc. sull'entità?
Eric Huang

19

Spring-jpa crea la query utilizzando il gestore di entità e Hibernate ignorerà la modalità di recupero se la query è stata creata dal gestore di entità.

Quello che segue è il lavoro intorno che ho usato:

  1. Implementa un repository personalizzato che eredita da SimpleJpaRepository

  2. Sostituisci il metodo 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));
    }

    A metà del metodo, aggiungi applyFetchMode(root); per applicare la modalità di recupero, per fare in modo che Hibernate crei la query con il join corretto.

    (Sfortunatamente dobbiamo copiare l'intero metodo e i relativi metodi privati ​​dalla classe base perché non c'erano altri punti di estensione.)

  3. Implementare 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);
            }
        }
    }

Sfortunatamente questo non funziona per le query generate utilizzando il nome del metodo del repository.
Ondrej Bozek

potresti aggiungere tutte le dichiarazioni di importazione? grazie.
granadaCoder

3

"FetchType.LAZY " si attiverà solo per la tabella principale. Se nel codice chiami un altro metodo che ha una dipendenza dalla tabella padre, verrà eseguita una query per ottenere le informazioni sulla tabella. (INCENDI SELEZIONE MULTIPLA)

" FetchType.EAGER" creerà un join di tutte le tabelle comprese le tabelle principali pertinenti direttamente. (USIJOIN )

Quando usarlo: supponi di dover obbligatoriamente utilizzare le informazioni della tabella padre dipendente, quindi scegli FetchType.EAGER. Se hai bisogno solo di informazioni per determinati record, usaFetchType.LAZY .

Ricorda, è FetchType.LAZYnecessaria una session factory db attiva nel punto del codice in cui si sceglie di recuperare le informazioni sulla tabella padre.

Ad esempio per 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

Riferimento aggiuntivo


È interessante notare che questa risposta mi ha portato sulla strada giusta per l'utilizzo NamedEntityGraphpoiché volevo un oggetto grafico non idratato.
JJ Zabkar

questa risposta merita più voti positivi. È succinto e mi ha aiutato molto a capire perché vedevo molte domande "innescate magicamente" ... grazie mille!
Clint Eastwood

3

La modalità di recupero funzionerà solo quando si seleziona l'oggetto tramite ID, ovvero utilizzando entityManager.find(). Poiché Spring Data creerà sempre una query, la configurazione della modalità di recupero non ti servirà. Puoi utilizzare query dedicate con fetch join o utilizzare grafici di entità.

Quando si desidera ottenere le migliori prestazioni, è necessario selezionare solo il sottoinsieme dei dati di cui si ha realmente bisogno. Per fare ciò, si consiglia generalmente di utilizzare un approccio DTO per evitare il recupero di dati non necessari, ma ciò di solito si traduce in un sacco di codice boilerplate soggetto a errori, poiché è necessario definire una query dedicata che costruisce il modello DTO tramite un JPQL espressione del costruttore.

Le proiezioni di Spring Data possono aiutare qui, ma a un certo punto avrai bisogno di una soluzione come Blaze-Persistence Entity Views che lo rende piuttosto semplice e ha molte più funzionalità nella manica che ti torneranno utili! Devi solo creare un'interfaccia DTO per entità in cui i getter rappresentano il sottoinsieme di dati di cui hai bisogno. Una soluzione al tuo problema potrebbe assomigliare a questa

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

Disclaimer, sono l'autore di Blaze-Persistence, quindi potrei essere di parte.


2

Ho elaborato la risposta di dream83619 per far sì che gestisca le @Fetchannotazioni di Hibernate annidate . Ho usato il metodo ricorsivo per trovare annotazioni nelle classi associate annidate.

Quindi devi implementare un repository personalizzato e un getQuery(spec, domainClass, sort)metodo di sostituzione . Sfortunatamente devi anche copiare tutti i metodi privati ​​referenziati :(.

Ecco il codice, i metodi privati ​​copiati vengono omessi.
EDIT: Aggiunti i restanti metodi privati.

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

Sto provando la tua soluzione ma ho una variabile di metadati privata in uno dei metodi per copiare che sta dando problemi. Puoi condividere il codice finale?
Homer1980ar

Il recupero ricorsivo non funziona. Se ho OneToMany passa java.util.List alla successiva iterazione
antohoho

non l'ho ancora testato bene, ma penso che dovrebbe essere qualcosa del genere ((Join) descent) .getJavaType () invece di field.getType () quando si chiama in modo ricorsivo applyFetchMode
antohoho

2

http://jdpgrailsdev.github.io/blog/2014/09/09/spring_data_hibernate_join.html
da questo link:

se stai usando JPA sopra Hibernate, non c'è modo di impostare FetchMode usato da Hibernate su JOIN.Tuttavia, se stai usando JPA sopra Hibernate, non c'è modo di impostare FetchMode usato da Hibernate su JOIN.

La libreria Spring Data JPA fornisce un'API delle specifiche di progettazione guidata dal dominio che consente di controllare il comportamento della query generata.

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

2

Secondo Vlad Mihalcea (vedi https://vladmihalcea.com/hibernate-facts-the-importance-of-fetch-strategy/ ):

Le query JPQL possono sovrascrivere la strategia di recupero predefinita. Se non dichiariamo esplicitamente ciò che vogliamo recuperare utilizzando le direttive di recupero del join interno o sinistro, viene applicato il criterio di selezione predefinito per il recupero.

Sembra che la query JPQL possa sovrascrivere la tua strategia di recupero dichiarata, quindi dovrai usarla join fetchper caricare avidamente qualche entità referenziata o semplicemente caricare per id con EntityManager (che obbedirà alla tua strategia di recupero ma potrebbe non essere una soluzione per il tuo caso d'uso ).

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.