it-swarm.com.ru

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

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

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

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

}

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

80
wesker317

Во-первых, @Fetch(FetchMode.JOIN) и @ManyToOne(fetch = FetchType.LAZY) являются антагонистами, один из них инструктирует EAGER-выборку, а другой - 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);
}
40
Vlad Mihalcea

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

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

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

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

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

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

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

2
Godwin

Я разработал 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;
    }
}
2
Ondrej Bozek

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

1
Christian Beikov

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

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

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

1
adrhc

http://jdpgrailsdev.github.io/blog/2014/09/09/spring_data_hibernate_join.html
по этой ссылке:

если вы используете JPA поверх Hibernate, нет никакого способа установить FetchMode, используемый Hibernate, в JOINHowever, если вы используете 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);
0
kafkas