Критерии Hibernate возвращают дочерние элементы несколько раз с помощью FetchType.EAGER

У меня есть класс Order, который имеет список OrderTransactions, и я сопоставил его с сопоставлением Hibernate от одного до многих, например:

@OneToMany(targetEntity = OrderTransaction.class, cascade = CascadeType.ALL)
public List<OrderTransaction> getOrderTransactions() {
    return orderTransactions;
}

Эти Order также имеют поле orderStatus, которое используется для фильтрации со следующими критериями:

public List<Order> getOrderForProduct(OrderFilter orderFilter) {
    Criteria criteria = getHibernateSession()
            .createCriteria(Order.class)
            .add(Restrictions.in("orderStatus", orderFilter.getStatusesToShow()));
    return criteria.list();
}

Это работает, и результат будет таким, как ожидалось.

Теперь вот мой вопрос. Почему, когда я устанавливаю тип выборки явно на EAGER, повторяйте Order несколько раз в результирующем списке?

@OneToMany(targetEntity = OrderTransaction.class, fetch = FetchType.EAGER, cascade = CascadeType.ALL)
public List<OrderTransaction> getOrderTransactions() {
    return orderTransactions;
}

Как мне изменить код Criteria, чтобы получить тот же результат с новым параметром?

Ответ 1

Это действительно ожидаемое поведение, если я правильно понял вашу конфигурацию.

Вы получаете тот же экземпляр Order в любом из результатов, но поскольку теперь вы делаете соединение с OrderTransaction, он должен возвращать то же количество результатов, что и обычное соединение sql вернет

Так на самом деле это должно появиться несколько раз. это очень хорошо объясняется автором (Gavin King) здесь: Это и объясняет, почему, и как все еще получать отличные результаты.


Также упоминается в Hibernate FAQ:

Hibernate не возвращает отличные результаты для запроса с внешней привязкой присоединения для коллекции (даже если я использую различные ключевое слово)? Во-первых, вам нужно понять SQL и как работают OUTER JOINs в SQL. Если вы не полностью понимаете и понимаете внешние объединения в SQL, не продолжайте чтение этого пункта часто задаваемых вопросов, но обратитесь к руководству по SQL или руководство. В противном случае вы не поймете следующее объяснение и вы будете жаловаться на это поведение на форуме Hibernate.

Типичные примеры, которые могут возвращать повторяющиеся ссылки того же Объект заказа:

List result = session.createCriteria(Order.class)
                    .setFetchMode("lineItems", FetchMode.JOIN)
                    .list();

<class name="Order">
    ...
    <set name="lineItems" fetch="join">

List result = session.createCriteria(Order.class)
                       .list();
List result = session.createQuery("select o from Order o left join fetch o.lineItems").list();

Все эти примеры производят один и тот же оператор SQL:

SELECT o.*, l.* from ORDER o LEFT OUTER JOIN LINE_ITEMS l ON o.ID = l.ORDER_ID

Хотите знать, почему дубликаты есть? Посмотрите на набор результатов SQL, Спящий режим не скрывает эти дубликаты в левой части внешнего но возвращает все дубликаты таблицы вождения. Если у вас есть 5 заказов в базе данных, и каждый заказ имеет 3 позиции, результат будет 15 строк. Список результатов из этих запросов Java будет иметь 15 элементов, все типа Order. Только 5 экземпляров заказа будут создаются Hibernate, но дубликаты набора результатов SQL сохраняются как дубликаты ссылок на эти 5 экземпляров. Если ты не понять это последнее предложение, вам нужно прочитать на Java и разница между экземпляром в куче Java и ссылкой на такой экземпляр.

(Почему левое внешнее соединение? Если у вас будет дополнительный заказ без строки элементов, набор результатов будет 16 строк с заполнением NULL справа где данные позиции для другого порядка. Вы хотите заказать даже если у них нет позиций, не так ли? Если нет, используйте внутреннее соединение fetch в вашем HQL).

Hibernate не фильтрует эти дубликаты ссылок по умолчанию. Некоторые люди (а не вы) на самом деле этого хотят. Как вы можете отфильтровать их?

Вот так:

Collection result = new LinkedHashSet( session.create*(...).list() );

Ответ 2

В дополнение к тому, что упоминается Эраном, другим способом получить нужное поведение является установка трансформатора результата:

criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);

Ответ 3

попробовать

@Fetch (FetchMode.SELECT) 

например

@OneToMany(targetEntity = OrderTransaction.class, fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@Fetch (FetchMode.SELECT)
public List<OrderTransaction> getOrderTransactions() {
return orderTransactions;

}

Ответ 4

Не используйте List и ArrayList, а Set и HashSet.

@OneToMany(targetEntity = OrderTransaction.class, cascade = CascadeType.ALL)
public Set<OrderTransaction> getOrderTransactions() {
    return orderTransactions;
}

Ответ 5

Используя Java 8 и потоки, я добавляю в свой метод утилиты этот возвращаемый статус:

return results.stream().distinct().collect(Collectors.toList());

Потоки удаляют дубликат очень быстро. Я использую аннотацию в моем классе Entity следующим образом:

@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinTable(name = "STUDENT_COURSES")
private List<Course> courses;

Я думаю, что в моем приложении предлагается использовать сеанс в методе, где мне нужны данные из базы данных. Закрытие сессии, когда я это сделал. Конечно, мой класс Entity должен использовать тип лизинга. Я перехожу к рефактору.

Ответ 6

У меня есть та же проблема, что и для извлечения 2 связанных коллекций: у пользователя есть 2 роли (набор) и 2 приема пищи (список), и обеды дублируются.

@Table(name = "users")
public class User extends AbstractNamedEntity {

   @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
   @Column(name = "role")
   @ElementCollection(fetch = FetchType.EAGER)
   @BatchSize(size = 200)
   private Set<Role> roles;

   @OneToMany(fetch = FetchType.LAZY, mappedBy = "user")
   @OrderBy("dateTime DESC")
   protected List<Meal> meals;
   ...
}

DISTINCT не помогает (запрос DATA-JPA):

@EntityGraph(attributePaths={"meals", "roles"})
@QueryHints({@QueryHint(name= org.hibernate.jpa.QueryHints.HINT_PASS_DISTINCT_THROUGH, value = "false")}) // remove unnecessary distinct from select
@Query("SELECT DISTINCT u FROM User u WHERE u.id=?1")
User getWithMeals(int id);

Наконец, я нашел 2 решения:

  • Изменить список в LinkedHashSet
  • Используйте EntityGraph только с полем "еда" и введите LOAD, который загружает роли по объявлению (EAGER и BatchSize = 200 для предотвращения проблемы N + 1):

Окончательное решение:

@EntityGraph(attributePaths = {"meals"}, type = EntityGraph.EntityGraphType.LOAD)
@Query("SELECT u FROM User u WHERE u.id=?1")
User getWithMeals(int id);

UPDATE: если javadoc для org.springframework.data.jpa.repository.EntityGraph.EntityGraphType#FETCH

которые указаны узлами атрибутов графа сущности, рассматриваются как FetchType.EAGER, а атрибуты, которые не указаны, рассматриваются как FetchType.LAZY

с этими типами, которые также выбраны.

Ответ 7

Это не звучит здорово, применяя внешнее соединение, и дает дублированные результаты. Единственное оставшееся решение - отфильтровать наш результат, используя потоки. Спасибо java8, предоставляя более простой способ фильтрации.

return results.stream().distinct().collect(Collectors.toList());