Comment fonctionne le FetchMode dans Spring Data JPA

91

J'ai une relation entre trois objets de modèle dans mon projet (des extraits de modèle et de référentiel à la fin de l'article.

Quand j'appelle PlaceRepository.findById il déclenche trois requêtes de sélection:

("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

C'est un comportement plutôt inhabituel (pour moi). Pour autant que je sache après avoir lu la documentation Hibernate, il devrait toujours utiliser les requêtes JOIN. Il n'y a aucune différence dans les requêtes lorsqu'elles sont FetchType.LAZYmodifiées en FetchType.EAGERdans la Placeclasse (requête avec SELECT supplémentaire), de même pour la Cityclasse lorsqu'elle est FetchType.LAZYmodifiée en FetchType.EAGER(requête avec JOIN).

Lorsque j'utilise la CityRepository.findByIdsuppression des incendies, deux sélections:

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

Mon objectif est d'avoir un comportement sam dans toutes les situations (soit toujours JOIN ou SELECT, JOIN préféré cependant).

Définitions du modèle:

Endroit:

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

Ville:

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

Dépôts:

LieuRepository

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);
}
SirKometa
la source
Hava un regard sur 5 façons d'initialiser les relations paresseuses: pensées
java.org/...

Réponses:

109

Je pense que Spring Data ignore le FetchMode. J'utilise toujours les annotations @NamedEntityGraphet @EntityGraphlorsque je travaille avec 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);

}

Consultez la documentation ici

wesker317
la source
1
Je ne semble pas travailler pour moi. Je veux dire que cela fonctionne mais ... Quand j'annote le référentiel avec '@EntityGraph', cela ne fonctionne pas tout seul (généralement). Par exemple: `Place findById (int id);` fonctionne mais List<Place> findAll();se termine par l'exception org.springframework.data.mapping.PropertyReferenceException: No property find found for type Place!. Cela fonctionne lorsque j'ajoute manuellement @Query("select p from Place p"). Cela semble être une solution de contournement.
SirKometa
Peut-être que cela fonctionne sur findAll () car c'est une méthode existante de l'interface JpaRepository tandis que votre autre méthode "findById" est une méthode de requête personnalisée générée à l'exécution.
wesker317
J'ai décidé de marquer cela comme la bonne réponse car c'est la meilleure. Ce n'est cependant pas parfait. Cela fonctionne dans la plupart des scénarios, mais jusqu'à présent, j'ai remarqué des bogues dans spring-data-jpa avec des EntityGraphs plus complexes. Merci :)
SirKometa
2
@EntityGraphest presque impossible dans les scénarios réels car il ne peut pas être spécifié quel type de produit Fetchnous voulons utiliser (JOIN , SUBSELECT, SELECT, BATCH). Ceci en combinaison avec l' @OneToManyassociation et rend Hibernate Fetch toute la table en mémoire même si nous utilisons la requête MaxResults.
Ondrej Bozek
1
Merci, je voulais dire que les requêtes JPQL peuvent remplacer la stratégie de récupération par défaut avec récupération sélectionnée .
adrhc
51

Tout d'abord, @Fetch(FetchMode.JOIN)et @ManyToOne(fetch = FetchType.LAZY)sont antagonistes, l'un ordonnant une récupération EAGER, tandis que l'autre suggérant une récupération LAZY.

La récupération impatiente est rarement un bon choix et pour un comportement prévisible, il vaut mieux utiliser la JOIN FETCHdirective 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);
}
Vlad Mihalcea
la source
3
Y a-t-il moyen d'obtenir le même résultat avec l'API Criteria et les spécifications Spring Data?
svlada
2
Pas la partie d'extraction, qui nécessite des profils d'extraction JPA.
Vlad Mihalcea
Vlad Mihalcea, pourriez-vous partager le lien avec un exemple comment faire cela en utilisant les critères Spring Data JPA (spécification)? S'il vous plaît
Yan Khonski
Je n'ai pas d'exemple de ce type, mais vous pouvez sûrement en trouver un dans les didacticiels Spring Data JPA.
Vlad Mihalcea
si vous utilisez query-time ..... aurez-vous encore besoin de définir @OneToMany ... etc sur l'entité?
Eric Huang
19

Spring-jpa crée la requête à l'aide du gestionnaire d'entités et Hibernate ignorera le mode de récupération si la requête a été créée par le gestionnaire d'entités.

Ce qui suit est le travail autour que j'ai utilisé:

  1. Implémenter un référentiel personnalisé qui hérite de SimpleJpaRepository

  2. Remplacez la méthode 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));
    }

    Au milieu de la méthode, ajoutez applyFetchMode(root);pour appliquer le mode de récupération, pour que Hibernate crée la requête avec la jointure correcte.

    (Malheureusement, nous devons copier la méthode entière et les méthodes privées associées de la classe de base car il n'y avait pas d'autre point d'extension.)

  3. Mettre en œuvre 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);
            }
        }
    }
rêve83619
la source
Malheureusement, cela ne fonctionne pas pour les requêtes générées à l'aide du nom de la méthode du référentiel.
Ondrej Bozek
pourriez-vous s'il vous plaît ajouter toutes les instructions d'importation? Merci.
grenadeCoder
3

" FetchType.LAZY" ne se déclenchera que pour la table primaire. Si dans votre code vous appelez une autre méthode qui a une dépendance de table parente, elle lancera une requête pour obtenir ces informations de table. (SÉLECTION MULTIPLE INCENDIE)

" FetchType.EAGER" créera directement la jointure de toutes les tables, y compris les tables parent pertinentes. (UTILISATIONS JOIN)

Quand utiliser: Supposons que vous deviez obligatoirement utiliser les informations de la table parent dépendante, puis choisissez FetchType.EAGER. Si vous n'avez besoin d'informations que pour certains enregistrements, utilisezFetchType.LAZY .

N'oubliez pas, a FetchType.LAZYbesoin d'une fabrique de session de base de données active à l'endroit de votre code où si vous choisissez de récupérer les informations de la table parent.

Par exemple pour 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

Référence supplémentaire

Godwin
la source
Fait intéressant, cette réponse m'a mis sur la bonne voie NamedEntityGraphcar je voulais un graphique d'objets non hydratés.
JJ Zabkar
cette réponse mérite plus de votes positifs. C'est succinct et cela m'a beaucoup aidé à comprendre pourquoi je voyais beaucoup de requêtes "déclenchées par magie" ... merci beaucoup!
Clint Eastwood
3

Le mode de récupération ne fonctionnera que lors de la sélection de l'objet par id c'est-à-dire en utilisant entityManager.find(). Étant donné que Spring Data créera toujours une requête, la configuration du mode de récupération ne vous sera d'aucune utilité. Vous pouvez utiliser des requêtes dédiées avec des jointures d'extraction ou utiliser des graphiques d'entités.

Lorsque vous souhaitez obtenir les meilleures performances, vous ne devez sélectionner que le sous-ensemble de données dont vous avez réellement besoin. Pour ce faire, il est généralement recommandé d'utiliser une approche DTO pour éviter de récupérer des données inutiles, mais cela entraîne généralement beaucoup de code passe-partout sujet aux erreurs, car vous devez définir une requête dédiée qui construit votre modèle DTO via un JPQL expression du constructeur.

Les projections de Spring Data peuvent aider ici, mais à un moment donné, vous aurez besoin d'une solution comme Blaze-Persistence Entity Views, ce qui rend cela assez facile et a beaucoup plus de fonctionnalités dans sa pochette qui vous seront utiles! Vous créez simplement une interface DTO par entité où les getters représentent le sous-ensemble de données dont vous avez besoin. Une solution à votre problème pourrait ressembler à ceci

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

Clause de non-responsabilité, je suis l'auteur de Blaze-Persistence, donc je suis peut-être partial.

Christian Beikov
la source
2

J'ai développé la réponse dream83619 pour qu'elle gère Hibernate imbriquée @Fetch annotations . J'ai utilisé une méthode récursive pour trouver des annotations dans des classes associées imbriquées.

Vous devez donc implémenter un référentiel personnalisé et remplacergetQuery(spec, domainClass, sort) méthode de . Malheureusement, vous devez également copier toutes les méthodes privées référencées :(.

Voici le code, les méthodes privées copiées sont omises.
EDIT: Ajout des méthodes privées 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;
    }
}
Ondrej Bozek
la source
J'essaie votre solution mais j'ai une variable de métadonnées privée dans l'une des méthodes de copie qui pose problème. Pouvez-vous partager le code final?
Homer1980ar
La récupération récursive ne fonctionne pas. Si j'ai OneToMany, il passe java.util.List à l'itération suivante
antohoho
je ne l'ai pas encore bien testé, mais je pense que cela devrait être quelque chose comme ça ((Join) descente) .getJavaType () au lieu de field.getType () lors de l'appel récursif applyFetchMode
antohoho
2

http://jdpgrailsdev.github.io/blog/2014/09/09/spring_data_hibernate_join.html à
partir de ce lien:

si vous utilisez JPA sur Hibernate, il n'y a aucun moyen de définir le FetchMode utilisé par Hibernate sur JOIN Cependant, si vous utilisez JPA sur Hibernate, il n'y a aucun moyen de définir le FetchMode utilisé par Hibernate sur JOIN.

La bibliothèque Spring Data JPA fournit une API de spécifications de conception pilotées par domaine qui vous permet de contrôler le comportement de la requête générée.

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);
kafkas
la source
2

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

Les requêtes JPQL peuvent remplacer la stratégie de récupération par défaut. Si nous ne déclarons pas explicitement ce que nous voulons récupérer à l'aide des directives de récupération de jointure interne ou gauche, la stratégie de récupération de sélection par défaut est appliquée.

Il semble que la requête JPQL puisse remplacer votre stratégie de récupération déclarée, vous devrez donc l'utiliser join fetchpour charger avec impatience une entité référencée ou simplement charger par identifiant avec EntityManager (ce qui obéira à votre stratégie de récupération mais pourrait ne pas être une solution pour votre cas d'utilisation ).

adrhc
la source