Как работает FetchMode в Spring Data JPA

95

У меня есть связь между тремя объектами модели в моем проекте (фрагменты модели и репозитория в конце сообщения.

Когда я звоню, PlaceRepository.findByIdон запускает три запроса выбора:

("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

Это довольно необычное поведение (для меня). Насколько я могу судить после прочтения документации Hibernate, он всегда должен использовать запросы JOIN. Нет никакой разницы в запросах при FetchType.LAZYизменении на FetchType.EAGERв Placeклассе (запрос с дополнительным SELECT), то же самое для Cityкласса при FetchType.LAZYизменении на FetchType.EAGER(запрос с JOIN).

Когда я использую CityRepository.findByIdтушение пожаров, два выбора:

  1. SELECT * FROM city c where id = arg
  2. 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);
}
SirKometa
источник
Предлагаю вам 5 способов установить ленивые отношения: мысли-
on-java.org/…

Ответы:

114

Я думаю, что 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);

}

Проверьте документацию здесь

wesker317
источник
1
Кажется, я не работаю на меня. Я имею в виду, что это работает, но ... Когда я аннотирую репозиторий с помощью @EntityGraph, он не работает сам (обычно). Например: `Place findById (int id);` работает, но в List<Place> findAll();итоге возникает исключение org.springframework.data.mapping.PropertyReferenceException: No property find found for type Place!. Работает, когда добавляю вручную @Query("select p from Place p"). Хотя похоже на обходной путь.
SirKometa
Возможно, он не работает с findAll (), поскольку это уже существующий метод из интерфейса JpaRepository, в то время как ваш другой метод «findById» представляет собой настраиваемый метод запроса, созданный во время выполнения.
wesker317
Я решил отметить это как правильный ответ, так как он лучший. Однако это не идеально. Он работает в большинстве сценариев, но до сих пор я заметил ошибки в spring-data-jpa с более сложными EntityGraphs. Спасибо :)
SirKometa
2
@EntityGraphпочти ununsable в реальных сценариях , так как это не может быть указано , какой Fetchмы хотим использовать ( JOIN, SUBSELECT, SELECT, BATCH). Это в сочетании с @OneToManyассоциацией делает Hibernate Fetch целой таблицей в память, даже если мы используем запрос MaxResults.
Ондрей Бозек
1
Спасибо, я хотел сказать, что запросы JPQL могут переопределять стратегию выборки по умолчанию с помощью политики выборки .
adrhc
53

Прежде всего, @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);
}
Влад Михалча
источник
3
Есть ли способ достичь того же результата с помощью Criteria API и Spring Data Specification?
svlada
2
Не часть выборки, для которой требуются профили выборки JPA.
Влад Михалча
Влад Михалча, не могли бы вы поделиться ссылкой с примером, как это сделать, используя критерии (спецификацию) Spring Data JPA? Пожалуйста
Ян Хонски
У меня нет такого примера, но вы наверняка найдете его в учебниках Spring Data JPA.
Влад Михалча
при использовании времени запроса ..... вам все равно нужно будет определять @OneToMany ... и т. д. для объекта?
Эрик Хуанг
19

Spring-jpa создает запрос с помощью диспетчера сущностей, а Hibernate будет игнорировать режим выборки, если запрос был создан диспетчером сущностей.

Я использовал следующее решение:

  1. Реализуйте собственный репозиторий, наследуемый от SimpleJpaRepository

  2. Переопределите метод 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 создал запрос с правильным соединением.

    (К сожалению, нам нужно скопировать весь метод и связанные с ним частные методы из базового класса, потому что другой точки расширения не было.)

  3. Реализовать 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);
            }
        }
    }
    
мечта83619
источник
К сожалению, это не работает для запросов, созданных с использованием имени метода репозитория.
Ондрей Бозек
не могли бы вы добавить все операторы импорта? Спасибо.
granadaCoder
3

" FetchType.LAZY" будет срабатывать только для основной таблицы. Если в вашем коде вы вызываете любой другой метод, который имеет зависимость от родительской таблицы, он запускает запрос для получения информации об этой таблице. (ПОЖАР НЕСКОЛЬКО ВЫБОР)

" FetchType.EAGER" создаст объединение всей таблицы, включая соответствующие родительские таблицы напрямую. (ИСПОЛЬЗУЕТ JOIN)

Когда использовать: Предположим, вам обязательно нужно использовать информацию о зависимой родительской таблице, затем выберите 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

Дополнительная ссылка

Годвин
источник
Интересно, что этот ответ привел меня к правильному пути к использованию, NamedEntityGraphтак как мне нужен негидратированный граф объектов.
JJ Zabkar
этот ответ заслуживает большего количества голосов. Он лаконичен и очень помог мне понять, почему я видел множество запросов, "запускаемых магией" ... большое спасибо!
Клинт Иствуд
3

Режим выборки будет работать только при выборе объекта по идентификатору, то есть с использованием 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, поэтому могу быть предвзятым.

Кристиан Бейков
источник
2

Я подробно остановился на ответе 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;
    }
}
Ондрей Бозек
источник
Я пробую ваше решение, но у меня есть личная переменная метаданных в одном из методов копирования, которая вызывает проблемы. Вы можете поделиться окончательным кодом?
Homer1980ar
рекурсивная выборка не работает. Если у меня OneToMany, он передает java.util.List на следующую итерацию
antohoho
еще не тестировал это хорошо, но думаю должно быть что-то вроде этого ((Присоединиться) спуск) .getJavaType () вместо field.getType () при рекурсивном вызове applyFetchMode
antohoho
2

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);
кафки
источник
2

По словам Влада Михалчи (см. Https://vladmihalcea.com/hibernate-facts-the-importance-of-fetch-strategy/ ):

Запросы JPQL могут переопределять стратегию выборки по умолчанию. Если мы явно не объявляем, что мы хотим получить, используя директивы внутреннего или левого соединения, применяется политика выборочной выборки по умолчанию.

Кажется, что запрос JPQL может переопределить вашу объявленную стратегию выборки, поэтому вам придется использовать ее, join fetchчтобы с готовностью загрузить какой-либо ссылочный объект или просто загрузить по идентификатору с помощью EntityManager (который будет подчиняться вашей стратегии выборки, но может не быть решением для вашего варианта использования ).

adrhc
источник