Як працює 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);
}

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

Подивіться на 5 способів ініціалізації лінивих стосунків: misli-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);

}

Перевірте документацію тут


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.
Ondrej Bozek

1
Дякую, я хотів сказати, що запити JPQL можуть замінити стратегію отримання за замовчуванням стратегією вибору вибірки .
adrhc

53

Перш за все, @Fetch(FetchMode.JOIN)і@ManyToOne(fetch = FetchType.LAZY) є антагоністичними, один вказує вибору EAGER, в той час як інший пропонує LENY отримати.

Стрімке завантаження рідко є вдалим вибором, і для передбачуваної поведінки вам краще скористатися 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 Specifications?
svlada

2
Не частина отримання, яка вимагає профілі отримання JPA.
Влад Міхалча

Влад Міхалче, чи можете ви поділитися посиланням із прикладом, як це зробити за допомогою критеріїв JPA Spring Data (специфікація)? Будь ласка
Ян Хонскі

У мене немає такого прикладу, але ви, безсумнівно, можете знайти його у підручниках 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);
            }
        }
    }
    

На жаль, це не працює для запитів, згенерованих з використанням імені методу сховища.
Ondrej Bozek

не могли б ви додати всі заяви про імпорт? Дякую.
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 (який буде підкорятися вашій стратегії отримання, але може не бути рішенням для вашого випадку використання ).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.