web-dev-qa-db-fra.com

Comment fonctionne le FetchMode dans Spring Data JPA

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

Lorsque j'appelle PlaceRepository.findById, trois requêtes de sélection sont déclenchées:

("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). Autant que je sache, après avoir lu la documentation d'Hibernate, il convient de toujours utiliser les requêtes JOIN. Il n'y a pas de différence dans les requêtes lorsque FetchType.LAZY a été remplacé par FetchType.EAGER dans la classe Place (requête avec SELECT supplémentaire), identique pour la classe City lorsque FetchType.LAZY a été remplacé par FetchType.EAGER (requête avec JOIN). 

Lorsque j'utilise CityRepository.findById, la suppression des incendies se fait de deux manières: 

  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 identique dans toutes les situations (soit toujours JOIN ou SELECT, bien que JOIN soit préféré). 

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: 

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

Je pense que Spring Data ignore le FetchMode. J'utilise toujours les annotations @NamedEntityGraph et @EntityGraph lorsque 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

80
wesker317

Tout d’abord, @Fetch(FetchMode.JOIN) et @ManyToOne(fetch = FetchType.LAZY) sont antagonistes, l’une demandant un extraction EAGER, l’autre suggérant une extraction LAZY.

Extraire est rarement un bon choix et pour un comportement prévisible, il vaut mieux utiliser la directive query-time JOIN FETCH:

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);
}
40
Vlad Mihalcea

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.

Voici le travail que j'ai utilisé autour de:

  1. Implémenter un référentiel personnalisé héritant 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. Implémentez 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);
            }
        }
    }
    
15
dream83619

"FetchType.LAZY" ne se déclenchera que pour la table primaire. Si dans votre code, vous appelez une autre méthode ayant une dépendance de table parent, il lancera une requête pour obtenir les informations de cette table. (FIRE MULTIPLE SELECT)

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

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

N'oubliez pas que FetchType.LAZY a besoin d'une fabrique de sessions de base de données active à l'emplacement de votre code où vous souhaitez extraire 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 additionnelle

2
Godwin

J'ai élaboré sur dream83619 answer pour lui faire gérer les annotations Hibernate @Fetch imbriquées. J'ai utilisé une méthode récursive pour rechercher des annotations dans des classes associées imbriquées.

Vous devez donc implémenter un référentiel personnalisé et redéfinir la méthode getQuery(spec, domainClass, sort). 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;
    }
}
2
Ondrej Bozek

Le mode de récupération fonctionnera uniquement lors de la sélection de l’objet par id, c’est-à-dire à l'aide de entityManager.find(). Comme 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é.

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

Les projections Spring Data peuvent vous aider, mais vous aurez besoin d’une solution telle que Blaze-Persistence Entity Views , ce qui facilite les choses et offre beaucoup plus de fonctionnalités qui seront utiles! Vous créez simplement une interface DTO par entité où les accesseurs 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);
}

Disclaimer, je suis l'auteur de Blaze-Persistence, donc je pourrais être partial.

1
Christian Beikov

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 le faisons pas déclarer explicitement ce que nous voulons extraire en utilisant une jointure interne ou gauche directives de récupération, la stratégie de sélection par défaut est appliquée.

Il semble que les requêtes JPQL puissent remplacer votre stratégie de récupération déclarée. Vous devrez donc utiliser join fetch pour pouvoir charger avec impatience une entité référencée ou simplement pour charger id avec EntityManager (qui obéira à votre stratégie de récupération mais ne constituera peut-être pas une solution adaptée à votre utilisation. Cas).

1
adrhc

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

si vous utilisez JPA sur Hibernate, vous ne pouvez pas définir le mode FetchMode utilisé par Hibernate sur JOIN. Cependant, si vous utilisez JPA sur Hibernate, vous ne pouvez pas définir le mode FetchMode utilisé par Hibernate sur JOIN.

La bibliothèque Spring Data JPA fournit une API de spécification de conception pilotée 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);
0
kafkas