У меня есть связь между тремя объектами модели в моем проекте (фрагменты модели и репозитория в конце сообщения.
Когда я звоню, PlaceRepository.findById
он запускает три запроса выбора:
("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
Это довольно необычное поведение (для меня). Насколько я могу судить после прочтения документации Hibernate, он всегда должен использовать запросы JOIN. Нет никакой разницы в запросах при FetchType.LAZY
изменении на FetchType.EAGER
в Place
классе (запрос с дополнительным SELECT), то же самое для City
класса при FetchType.LAZY
изменении на FetchType.EAGER
(запрос с JOIN).
Когда я использую CityRepository.findById
тушение пожаров, два выбора:
SELECT * FROM city c where id = arg
SELECT * FROM state s where id = city.state.id
Моя цель - иметь поведение sam во всех ситуациях (либо всегда JOIN, либо SELECT, хотя предпочтительнее JOIN).
Определения модели:
Место:
@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
}
Город:
@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
}
Репозитории:
PlaceRepository
public interface PlaceRepository extends JpaRepository<Place, Long>, PlaceRepositoryCustom {
Place findById(int id);
}
Репозиторий пользователей:
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);
}
Ответы:
Я думаю, что Spring Data игнорирует FetchMode. Я всегда использовать
@NamedEntityGraph
и@EntityGraph
аннотации при работе с 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); }
Проверьте документацию здесь
источник
List<Place> findAll();
итоге возникает исключениеorg.springframework.data.mapping.PropertyReferenceException: No property find found for type Place!
. Работает, когда добавляю вручную@Query("select p from Place p")
. Хотя похоже на обходной путь.@EntityGraph
почти ununsable в реальных сценариях , так как это не может быть указано , какойFetch
мы хотим использовать (JOIN
,SUBSELECT
,SELECT
,BATCH
). Это в сочетании с@OneToMany
ассоциацией делает Hibernate Fetch целой таблицей в память, даже если мы используем запросMaxResults
.Прежде всего,
@Fetch(FetchMode.JOIN)
и@ManyToOne(fetch = FetchType.LAZY)
являются антагонистами, один инструктаж нетерпеливой выборки, а другие предполагают ленивую выборку.Активная выборка редко бывает хорошим выбором, и для предсказуемого поведения лучше использовать
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); }
источник
Spring-jpa создает запрос с помощью диспетчера сущностей, а Hibernate будет игнорировать режим выборки, если запрос был создан диспетчером сущностей.
Я использовал следующее решение:
Реализуйте собственный репозиторий, наследуемый от SimpleJpaRepository
Переопределите метод
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)); }
В середине метода добавьте,
applyFetchMode(root);
чтобы применить режим выборки, чтобы Hibernate создал запрос с правильным соединением.(К сожалению, нам нужно скопировать весь метод и связанные с ним частные методы из базового класса, потому что другой точки расширения не было.)
Реализовать
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); } } }
источник
"
FetchType.LAZY
" будет срабатывать только для основной таблицы. Если в вашем коде вы вызываете любой другой метод, который имеет зависимость от родительской таблицы, он запускает запрос для получения информации об этой таблице. (ПОЖАР НЕСКОЛЬКО ВЫБОР)"
FetchType.EAGER
" создаст объединение всей таблицы, включая соответствующие родительские таблицы напрямую. (ИСПОЛЬЗУЕТJOIN
)Когда использовать: Предположим, вам обязательно нужно использовать информацию о зависимой родительской таблице, затем выберите
FetchType.EAGER
. Если вам нужна информация только для определенных записей, используйтеFetchType.LAZY
.Помните,
FetchType.LAZY
требуется активная фабрика сеансов БД в том месте вашего кода, где вы решите получить информацию о родительской таблице.Например, для
LAZY
:Дополнительная ссылка
источник
NamedEntityGraph
так как мне нужен негидратированный граф объектов.Режим выборки будет работать только при выборе объекта по идентификатору, то есть с использованием
entityManager.find()
. Поскольку Spring Data всегда будет создавать запрос, конфигурация режима выборки вам не будет нужна. Вы можете использовать специальные запросы с объединениями выборки или использовать графы сущностей.Если вам нужна максимальная производительность, вам следует выбрать только часть данных, которые вам действительно нужны. Для этого обычно рекомендуется использовать подход DTO, чтобы избежать извлечения ненужных данных, но это обычно приводит к довольно большому количеству шаблонного кода, подверженного ошибкам, поскольку вам необходимо определить специальный запрос, который создает вашу модель DTO через JPQL. выражение конструктора.
Здесь могут помочь проекции Spring Data, но в какой-то момент вам понадобится такое решение, как Blaze-Persistence Entity Views, которое делает это довольно легко и имеет гораздо больше функций в своем рукаве, которые пригодятся! Вы просто создаете интерфейс DTO для каждой сущности, где геттеры представляют нужное вам подмножество данных. Решение вашей проблемы может выглядеть так
@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); }
Отказ от ответственности, я автор Blaze-Persistence, поэтому могу быть предвзятым.
источник
Я подробно остановился на ответе dream83619, чтобы он обрабатывал вложенные
@Fetch
аннотации Hibernate . Я использовал рекурсивный метод для поиска аннотаций во вложенных связанных классах.Поэтому вам нужно реализовать собственный репозиторий и
getQuery(spec, domainClass, sort)
метод переопределения . К сожалению, вам также необходимо скопировать все упомянутые частные методы :(.Вот код,
скопированные частные методы опущены.РЕДАКТИРОВАТЬ: добавлены оставшиеся частные методы.
@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; } }
источник
http://jdpgrailsdev.github.io/blog/2014/09/09/spring_data_hibernate_join.html
по этой ссылке:
если вы используете JPA поверх Hibernate, нет способа установить FetchMode, используемый Hibernate, на JOIN, однако, если вы используете JPA поверх Hibernate, нет способа установить FetchMode, используемый Hibernate, на JOIN.
Библиотека Spring Data JPA предоставляет API-интерфейс доменных спецификаций дизайна, который позволяет вам управлять поведением сгенерированного запроса.
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);
источник
По словам Влада Михалчи (см. Https://vladmihalcea.com/hibernate-facts-the-importance-of-fetch-strategy/ ):
Кажется, что запрос JPQL может переопределить вашу объявленную стратегию выборки, поэтому вам придется использовать ее,
join fetch
чтобы с готовностью загрузить какой-либо ссылочный объект или просто загрузить по идентификатору с помощью EntityManager (который будет подчиняться вашей стратегии выборки, но может не быть решением для вашего варианта использования ).источник