Jak działa FetchMode w Spring Data JPA

95

Mam relację między trzema obiektami modelu w moim projekcie (fragmenty modelu i repozytorium na końcu postu.

Kiedy dzwonię PlaceRepository.findById, uruchamia trzy zapytania wybierające:

(„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 dość niezwykłe zachowanie (dla mnie). O ile wiem po przeczytaniu dokumentacji Hibernate'a, powinien on zawsze używać zapytań JOIN. Nie ma różnicy w zapytaniach po FetchType.LAZYzmianie na FetchType.EAGERw Placeklasie (zapytanie z dodatkowym SELECT), to samo dla Cityklasy po FetchType.LAZYzmianie na FetchType.EAGER(zapytanie z JOIN).

Kiedy używam CityRepository.findByIdtłumienia pożarów, dwie opcje:

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

Moim celem jest zachowanie takiego samego zachowania we wszystkich sytuacjach (zawsze JOIN lub SELECT, preferowane JOIN).

Definicje modeli:

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

MiastoRepozytorium:

public interface CityRepository extends JpaRepository<City, Long>, CityRepositoryCustom {    
    City findById(int id);
}
SirKometa
źródło
Spójrz na 5 sposobów na zainicjowanie leniwych relacji: myśli
java.org/ ...

Odpowiedzi:

114

Myślę, że Spring Data ignoruje FetchMode. Zawsze używam adnotacji @NamedEntityGraphi @EntityGraphpodczas pracy z 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);

}

Sprawdź dokumentację tutaj

wesker317
źródło
1
Wydaje się, że nie pracuję dla mnie. Mam na myśli to, że działa, ale ... Kiedy adnotuję repozytorium za pomocą „@EntityGraph”, nie działa samo (zwykle). Na przykład: `Place findById (int id);` działa, ale List<Place> findAll();kończy się na wyjątku org.springframework.data.mapping.PropertyReferenceException: No property find found for type Place!. Działa, gdy dodam ręcznie @Query("select p from Place p"). Wygląda jednak na obejście tego problemu.
SirKometa
Może nie działa na findAll (), ponieważ jest to istniejąca metoda z interfejsu JpaRepository, podczas gdy inna metoda „findById” jest niestandardową metodą zapytania generowaną w czasie wykonywania.
wesker317
Postanowiłem oznaczyć to jako właściwą odpowiedź, ponieważ jest najlepsza. Nie jest to jednak doskonałe. Działa w większości scenariuszy, ale do tej pory zauważyłem błędy w spring-data-jpa z bardziej złożonymi EntityGraphs. Dzięki :)
SirKometa
2
@EntityGraphjest prawie ununsable realnych scenariuszy, ponieważ mogę to określić, jakie Fetchchcemy wykorzystać ( JOIN, SUBSELECT, SELECT, BATCH). To w połączeniu z @OneToManyasocjacją sprawia, że ​​Hibernacja pobiera całą tabelę do pamięci, nawet jeśli używamy zapytania MaxResults.
Ondrej Bozek
1
Dzięki, chciałem powiedzieć, że zapytania JPQL mogą przesłonić domyślną strategię pobierania z wybraną polityką pobierania .
adrhc
53

Przede wszystkim, @Fetch(FetchMode.JOIN)i @ManyToOne(fetch = FetchType.LAZY)są antagonistyczni, jeden instruuje pobieranie EAGER, a drugi sugeruje pobieranie LENIWE.

Chętne pobieranie rzadko jest dobrym wyborem, a dla przewidywalnego zachowania lepiej jest użyć JOIN FETCHdyrektywy czasu zapytania :

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
źródło
3
Czy istnieje sposób na osiągnięcie tego samego wyniku za pomocą Criteria API i Spring Data Specifications?
svlada
2
Nie część pobierania, która wymaga profili pobierania JPA.
Vlad Mihalcea
Vlad Mihalcea, czy mógłbyś udostępnić link z przykładem, jak to zrobić, korzystając z kryteriów (specyfikacji) Spring Data JPA? Proszę
Yan Khonski,
Nie mam takiego przykładu, ale na pewno znajdziesz go w tutorialach Spring Data JPA.
Vlad Mihalcea
jeśli używasz czasu zapytania ..... czy nadal będziesz musiał zdefiniować @OneToMany ... itd. w encji?
Eric Huang
19

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

Oto obejście, którego użyłem:

  1. Zaimplementuj niestandardowe repozytorium, które dziedziczy po SimpleJpaRepository

  2. Zastąp metodę 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 utworzył zapytanie z poprawnym złączeniem.

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

  3. Wdrożenie 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);
            }
        }
    }
    
dream83619
źródło
Niestety to nie działa w przypadku zapytań generowanych przy użyciu nazwy metody repozytorium.
Ondrej Bozek
czy mógłbyś dodać wszystkie oświadczenia dotyczące importu? Dziękuję Ci.
granadaCoder
3

FetchType.LAZY” będzie uruchamiany tylko dla tabeli podstawowej. Jeśli w swoim kodzie wywołasz inną metodę, która ma zależność od tabeli nadrzędnej, uruchomi ona zapytanie w celu uzyskania informacji o tej tabeli. (WYBIERZ WIELOKROTNE)

FetchType.EAGER” utworzy bezpośrednie połączenie wszystkich tabel, w tym odpowiednich tabel nadrzędnych. (UŻYWA JOIN)

Kiedy używać: Załóżmy, że musisz obowiązkowo użyć informacji z tabeli rodziców zależnych, a następnie wybierz FetchType.EAGER. Jeśli potrzebujesz informacji tylko dla niektórych rekordów, użyj FetchType.LAZY.

Pamiętaj, że FetchType.LAZYpotrzebujesz aktywnej fabryki sesji bazy danych w miejscu w kodzie, w którym chcesz pobrać informacje z 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 informacje

Godwin
źródło
Co ciekawe, ta odpowiedź zaprowadziła mnie na właściwą ścieżkę do używania, NamedEntityGraphponieważ chciałem uzyskać niewodniony graf obiektów.
JJ Zabkar
ta odpowiedź zasługuje na więcej głosów. Jest zwięzły i bardzo pomógł mi zrozumieć, dlaczego widzę wiele zapytań „uruchamianych magicznie”… wielkie dzięki!
Clint Eastwood
3

Tryb pobierania będzie działał tylko wtedy, gdy wybierzesz obiekt przez id, czyli używając entityManager.find(). Ponieważ Spring Data zawsze tworzy zapytanie, konfiguracja trybu pobierania nie będzie dla Ciebie przydatna. 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ę użycie podejścia DTO, aby uniknąć niepotrzebnych danych do pobrania, ale zwykle skutkuje to dużą ilością podatnego na błędy standardowego kodu, ponieważ musisz zdefiniować dedykowane zapytanie, które konstruuje model DTO za pośrednictwem JPQL wyrażenie konstruktora.

Prognozy Spring Data mogą w tym pomóc, ale w pewnym momencie będziesz potrzebować rozwiązania takiego jak Blaze-Persistence Entity Views, które sprawia, że ​​jest to całkiem łatwe i ma o wiele więcej funkcji w rękawie, które się przydadzą! Po prostu tworzysz interfejs DTO dla encji, w którym metody pobierające reprezentują podzbiór potrzebnych danych. Rozwiązanie twojego 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);
}

Zastrzeżenie, jestem autorem Blaze-Persistence, więc mogę być stronniczy.

Christian Beikov
źródło
2

Opracowałem odpowiedź dream83619, aby obsługiwała zagnieżdżone @Fetchadnotacje Hibernate . Użyłem metody rekurencyjnej, aby znaleźć adnotacje w zagnieżdżonych klasach skojarzonych.

Musisz więc zaimplementować niestandardowe repozytorium i getQuery(spec, domainClass, sort)metodę nadpisywania . Niestety musisz również skopiować wszystkie wymienione metody prywatne :(.

Oto kod, pomijane są skopiowane metody prywatne.
EDYCJA: 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;
    }
}
Ondrej Bozek
źródło
Próbuję twojego rozwiązania, ale mam prywatną zmienną metadanych w jednej z metod kopiowania, która sprawia problemy. Czy możesz udostępnić ostateczny kod?
Homer1980ar
rekurencyjne pobieranie nie działa. Jeśli mam OneToMany, to przechodzi java.util.List do następnej iteracji
antohoho
nie przetestowałem tego jeszcze dobrze, ale myślę, że powinno być coś takiego ((Join) descent) .getJavaType () zamiast field.getType () podczas wywołania rekurencyjnego applyFetchMode
antohoho
2

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

jeśli korzystasz z JPA na szczycie Hibernate, nie ma możliwości ustawienia FetchMode używanego przez Hibernate na JOINJeśli jednak używasz JPA nad Hibernate, nie ma możliwości ustawienia FetchMode używanego przez Hibernate na JOIN.

Biblioteka Spring Data JPA udostępnia interfejs API Domain Driven Design Specifications, który umożliwia sterowanie zachowaniem generowanego zapytania.

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
źródło
2

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

Zapytania JPQL mogą przesłonić domyślną strategię pobierania. Jeśli nie zadeklarujemy jawnie tego, co chcemy pobrać, używając dyrektyw pobierania wewnętrznego lub lewego sprzężenia, stosowana jest domyślna zasada pobierania.

Wygląda na to, że zapytanie JPQL może zastąpić zadeklarowaną strategię pobierania, więc będziesz musiał użyć join fetch, aby chętnie załadować jakąś przywoływaną jednostkę lub po prostu załadować według identyfikatora za pomocą EntityManager (który będzie zgodny z twoją strategią pobierania, ale może nie być rozwiązaniem dla twojego przypadku użycia ).

adrhc
źródło