Подтвердить что ты не робот

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

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

Когда я вызываю 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);
}

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

Ответ 1

Я думаю, что Spring Data игнорирует FetchMode. Я всегда использую аннотации @NamedEntityGraph и @EntityGraph при работе с Spring Data​​p >

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

}

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

Ответ 2

Прежде всего, @Fetch(FetchMode.JOIN) и @ManyToOne(fetch = FetchType.LAZY) являются антагонистическими, один из которых инструктирует EAGER, а другой предлагает LAZY fetch.

Ожидаемая выборка редко хороший выбор и для предсказуемого поведения вам лучше использовать директиву времени запроса 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

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

Ответ 4

"FetchType.LAZY" будет работать только для основной таблицы. Если в коде вы вызываете какой-либо другой метод, имеющий зависимость родительской таблицы, он будет запускать запрос для получения этой информации таблицы. (FIRES MULTIPLE SELECT)

"FetchType.EAGER" создаст объединение всей таблицы, включая соответствующие родительские таблицы. (USES JOIN)

Когда использовать: Предположим, вам обязательно нужно использовать зависимую родительскую таблицу, затем выберите FetchType.EAGER. Если вам нужна только информация для определенных записей, используйте FetchType.LAZY.

Помните, что FetchType.LAZY требуется активный сеанс db factory в месте вашего кода, если вы хотите получить информацию родительской таблицы.

например. для 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

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

Ответ 5

Я разработал ответ 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;
    }
}