Как работает FetchMode в Spring Data JPA
у меня есть связь между тремя объектами модели в моем проекте (Модель и фрагменты репозитория в конце сообщения.
когда я называю PlaceRepository.findById он запускает три запроса select:
("sql")
SELECT * FROM place p where id = argSELECT * FROM user u where u.id = place.user.idSELECT * 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 класс (запрос с дополнительным выбором), то же самое для City класс, когда FetchType.LAZY изменено на FetchType.EAGER (запрос соединения).
когда я использую CityRepository.findById подавление пожаров два выбора:
SELECT * FROM city c where id = argSELECT * FROM state s where id = city.state.id
моя цель состоит в том, чтобы иметь одинаковое поведение во всех ситуациях (либо всегда присоединиться или выбрать, присоединиться предпочтительно, хотя).
модель определения:
место:
@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);
}
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);
}
8 ответов:
Я думаю, что Spring Data игнорирует FetchMode. Я всегда использую
@NamedEntityGraphи@EntityGraphаннотации при работе с весенними данными@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); }Регистрация документации здесь
прежде всего,
@Fetch(FetchMode.JOIN)и@ManyToOne(fetch = FetchType.LAZY)антагонистичны, один инструктирует нетерпеливую выборку, а другой предлагает ленивую выборку.нетерпеливая выборка редко хороший выбор и для предсказуемого поведения вам лучше использовать время запроса :
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)когда использовать: Предположим, вам обязательно нужно использовать зависимую родительскую таблицу informartion затем выберите
FetchType.EAGER. Если вам нужна только информация наверняка записи затем использоватьFetchType.LAZY.помните, что
FetchType.LAZYтребуется активная фабрика сеансов БД в том месте вашего кода, где, если вы решите получить информацию о родительской таблице.например, для
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
я подробно остановился на dream83619 ответ, чтобы сделать его обрабатывать вложенные Hibernate
@FetchПримечание. Я использовал рекурсивный метод для поиска аннотаций во вложенных связанных классах.таким образом, вы должны реализовать пользовательский репозиторий и заменить
getQuery(spec, domainClass, sort)метод. К сожалению, вы также должны скопировать все указанные частные методы: (.здесь код
скопированные частные методы опущены.
EDIT: добавил остальные частные методы.@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; } }
по словам Влада Михальча (см. https://vladmihalcea.com/hibernate-facts-the-importance-of-fetch-strategy/):
запросы JPQL могут переопределять стратегию выборки по умолчанию. Если мы не явно объявите, что мы хотим получить, используя внутреннее или левое соединение директивы выборки, применяется политика выборки select по умолчанию.
похоже, что запрос JPQL может переопределить объявленную стратегию выборки, поэтому вам придется использовать
join fetchin чтобы охотно загрузить некоторую ссылочную сущность или просто загрузить по идентификатору с помощью EntityManager (который будет подчиняться вашей стратегии выборки, но может не быть решением для вашего варианта использования).
http://jdpgrailsdev.github.io/blog/2014/09/09/spring_data_hibernate_join.html
по этой ссылке:Если вы используете JPA на вершине спящего, нет никакого способа, чтобы установить FetchMode используется Hibernate для JOINHowever, если вы используете JPA на вершине спящего, нет никакого способа, чтобы установить FetchMode используется спящий режим, чтобы присоединиться.
библиотека 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);
режим выборки будет работать только при выборе объекта идентификатор т. е. с помощью
entityManager.find(). Поскольку Spring Data всегда будет создавать запрос, конфигурация режима выборки вам не понадобится. Вы можете использовать выделенные запросы с соединениями выборки или использовать графики сущностей.Если вы хотите лучшую производительность, вы должны выбрать только подмножество данных, которые вам действительно нужны. Для этого обычно рекомендуется использовать подход DTO, чтобы избежать ненужных данных выборка, но это обычно приводит к довольно большому количеству ошибок, подверженных шаблонному коду, так как вам нужно определить выделенный запрос, который строит вашу модель DTO с помощью выражения конструктора JPQL.
весенние прогнозы данных могут помочь здесь, но в какой-то момент вам понадобится такое решение, как 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, поэтому я могу быть предвзятым.
Comments