Jak działa FetchMode w Spring Data JPA

Mam relację pomiędzy trzema obiektami modelu w moim projekcie (urywki modelu i repozytorium na końcu posta.

Kiedy wywołuję {[5] } odpala trzy zapytania select:

("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
To raczej nietypowe zachowanie (jak dla mnie). Z tego co wiem po przeczytaniu dokumentacji Hibernate powinien on zawsze używać zapytań JOIN. Nie ma różnicy w zapytaniach, gdy FetchType.LAZY zmieniono na FetchType.EAGER w klasie Place (zapytanie z dodatkowym SELECT), to samo dla klasy City, gdy FetchType.LAZY zmieniono na FetchType.EAGER (zapytanie z JOIN).

Kiedy używam CityRepository.findById tłumienie pożarów dwa selekcje:

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

Moim celem jest zachowanie sam we wszystkich sytuacjach(zawsze Dołącz lub wybierz, Dołącz preferowane chociaż).

Definicje modelu:

Miejsce:

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

Miasto:

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

Repozytoria:

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);
}
Author: Vlad Mihalcea, 2015-04-13

8 answers

Myślę, że Spring Data ignoruje FetchMode. Zawsze używam adnotacji @NamedEntityGraph i @EntityGraph podczas pracy z danymi źródłowymi

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

}

Sprawdź dokumentację proszę.

 75
Author: wesker317,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-05-19 08:15:00

Po Pierwsze, @Fetch(FetchMode.JOIN) i @ManyToOne(fetch = FetchType.LAZY) są antagonistyczne, jeden instruuje chętnego aportowania, a drugi sugeruje leniwego aportowania.

Eager fetching jest rzadko dobrym wyborem i dla przewidywalnego zachowania lepiej jest użyć dyrektywy 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);
}
 34
Author: Vlad Mihalcea,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-01-04 11:41:00

Spring-jpa tworzy zapytanie używając menedżera encji, a Hibernate zignoruje tryb pobierania, jeśli zapytanie zostało zbudowane przez menedżera encji.

Poniżej znajduje się obejście, którego użyłem:

  1. Zaimplementuj własne repozytorium z SimpleJpaRepository

  2. Nadpisanie metody 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));
    }
    

    W środku metody dodaj applyFetchMode(root); aby zastosować tryb pobierania, aby Hibernate stworzył zapytanie z poprawnym połączeniem.

    (niestety musimy skopiować całą metodę i powiązane prywatne metody z klasy bazowej, ponieważ nie było innego punktu rozszerzenia.)

  3. Implementacja 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);
            }
        }
    }
    
 14
Author: dream83619,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-03-08 06:43:36

"FetchType.LAZY" będzie strzelać tylko do głównego stołu. Jeśli w Twoim kodzie wywołasz jakąkolwiek inną metodę, która ma zależność od tabeli nadrzędnej, to odpali zapytanie, aby uzyskać informacje o tej tabeli. (FIRES MULTIPLE SELECT)

"FetchType.EAGER" utworzy połączenie wszystkich tabel, w tym odpowiednich tabel nadrzędnych bezpośrednio. (Używa JOIN)

Kiedy stosować: Załóżmy, że musisz użyć dependant table parent informartion, a następnie wybierz FetchType.EAGER. Jeśli potrzebujesz informacji tylko dla niektórych rekordów, użyj FetchType.LAZY.

Pamiętaj, FetchType.LAZY wymaga aktywnej fabryki sesji db w miejscu w kodzie, w którym, jeśli zdecydujesz się pobrać informacje o tabeli nadrzędnej.

Np. dla 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

Dodatkowe odniesienie

 2
Author: Godwin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-04-22 06:56:56

Opracowałem odpowiedź dream83619 , aby obsługiwał zagnieżdżone adnotacje Hibernate @Fetch. Użyłem metody rekurencyjnej, aby znaleźć adnotacje w zagnieżdżonych klasach powiązanych.

Trzeba więc zaimplementować własne repozytorium i nadpisać metodę getQuery(spec, domainClass, sort). Niestety musisz również skopiować wszystkie wymienione metody prywatne :(.

Oto kod, skopiowane prywatne metody są pomijane.
EDIT: Dodano pozostałe metody prywatne.

@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
Author: Ondrej Bozek,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-07-27 07:11:44

Według Vlada Mihalcea (patrz https://vladmihalcea.com/hibernate-facts-the-importance-of-fetch-strategy/):

Zapytania JPQL mogą zastąpić domyślną strategię pobierania. Jeśli nie jawnie zadeklarować, co chcemy pobrać za pomocą Inner lub left join dyrektywy pobierania, stosowana jest domyślna Polityka pobierania select.

Wydaje się, że zapytanie JPQL może nadpisać Twoją zadeklarowaną strategię pobierania, więc będziesz musiał użyć join fetch, aby chętnie załadować niektóre encja referencyjna lub po prostu załaduj id za pomocą EntityManager(który będzie posłuszny Twojej strategii pobierania, ale może nie być rozwiązaniem dla Twojego przypadku użycia).

 1
Author: adrhc,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-07-09 11:57:05

Http://jdpgrailsdev.github.io/blog/2014/09/09/spring_data_hibernate_join.html
z tego linku:

Jeśli używasz JPA na górze Hibernate, nie ma sposobu, aby ustawić FetchMode używany przez Hibernate do dołączenia, jeśli używasz JPA na górze Hibernate, nie ma sposobu, aby ustawić FetchMode używany przez Hibernate do dołączenia.

Biblioteka Spring Data JPA zapewnia Domain Driven Design Specifications API, które pozwala kontrolować zachowanie generowanych zapytanie.

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
Author: kafkas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-01-07 19:21:11

Tryb pobierania będzie działał tylko przy wybraniu obiektu przez id tzn. przy użyciu entityManager.find(). Ponieważ Spring Data zawsze utworzy zapytanie, konfiguracja trybu pobierania nie będzie Ci potrzebna. Możesz użyć dedykowanych zapytań z połączeniami pobierania lub użyć Wykresów encji.

Jeśli chcesz uzyskać najlepszą wydajność, powinieneś wybrać tylko podzbiór danych, których naprawdę potrzebujesz. Aby to zrobić, ogólnie zaleca się stosowanie podejścia DTO, aby uniknąć niepotrzebnych danych do pobrania, ale zwykle powoduje to dość dużo podatnych na błędy kodu boilerplate, ponieważ musisz zdefiniować dedykowane zapytanie, które konstruuje twój model DTO za pomocą wyrażenia konstruktora JPQL.

Wiosenne prognozy danych mogą tu pomóc, ale w pewnym momencie będziesz potrzebował rozwiązania, takiego jak Blaze-Persistence Entity Views , które czyni to dość łatwym i ma o wiele więcej funkcji w rękawie, które się przydadzą! Po prostu tworzysz interfejs DTO dla jednostki, gdzie gettery reprezentują podzbiór danych, których potrzebujesz. Rozwiązanie problemu może wyglądać tak:

@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, jestem autorem Blaze-Persistence, więc mogę być stronniczy.

 0
Author: Christian Beikov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-07-18 07:20:40