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”)
SELECT * FROM place p where id = arg
SELECT * FROM user u where u.id = place.user.id
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.LAZY
zmianie na FetchType.EAGER
w Place
klasie (zapytanie z dodatkowym SELECT), to samo dla City
klasy po FetchType.LAZY
zmianie na FetchType.EAGER
(zapytanie z JOIN).
Kiedy używam CityRepository.findById
tłumienia pożarów, dwie opcje:
SELECT * FROM city c where id = arg
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);
}
Odpowiedzi:
Myślę, że Spring Data ignoruje FetchMode. Zawsze używam adnotacji
@NamedEntityGraph
i@EntityGraph
podczas 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
źródło
List<Place> findAll();
kończy się na wyjątkuorg.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.@EntityGraph
jest prawie ununsable realnych scenariuszy, ponieważ mogę to określić, jakieFetch
chcemy wykorzystać (JOIN
,SUBSELECT
,SELECT
,BATCH
). To w połączeniu z@OneToMany
asocjacją sprawia, że Hibernacja pobiera całą tabelę do pamięci, nawet jeśli używamy zapytaniaMaxResults
.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 FETCH
dyrektywy 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); }
źródło
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:
Zaimplementuj niestandardowe repozytorium, które dziedziczy po SimpleJpaRepository
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).
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); } } }
źródło
„
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ŻYWAJOIN
)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żyjFetchType.LAZY
.Pamiętaj, że
FetchType.LAZY
potrzebujesz aktywnej fabryki sesji bazy danych w miejscu w kodzie, w którym chcesz pobrać informacje z tabeli nadrzędnej.Np. Dla
LAZY
:Dodatkowe informacje
źródło
NamedEntityGraph
ponieważ chciałem uzyskać niewodniony graf obiektów.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.
źródło
Opracowałem odpowiedź dream83619, aby obsługiwała zagnieżdżone
@Fetch
adnotacje 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; } }
źródło
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);
źródło
Według Vlada Mihalcea (patrz https://vladmihalcea.com/hibernate-facts-the-importance-of-fetch-strategy/ ):
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 ).źródło