Hibernate Criteria возвращает потомков несколько раз с помощью FetchType.EAGER

115

У меня есть 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, Orders появляются несколько раз в результирующем списке?

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

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

raoulsson
источник
1
Вы пробовали включить show_sql, чтобы увидеть, что происходит под ним?
Мирко Н.
Пожалуйста, добавьте также коды классов OrderTransaction и Order. \
Эран Медан,

Ответы:

115

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

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

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


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

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

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

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, Hibernate не скрывает эти дубликаты в левой части внешнего объединенного результата, а возвращает все дубликаты управляющей таблицы. Если у вас есть 5 заказов в базе данных, и каждый заказ имеет 3 позиции, набор результатов будет 15 строк. Список результатов Java этих запросов будет содержать 15 элементов, все типа Order. Hibernate создает только 5 экземпляров Order, но дубликаты набора результатов SQL сохраняются как повторяющиеся ссылки на эти 5 экземпляров. Если вы не понимаете это последнее предложение, вам необходимо прочитать о Java и о различиях между экземпляром в куче Java и ссылкой на такой экземпляр.

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

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

Как это:

Collection result = new LinkedHashSet( session.create*(...).list() );
Эран Медан
источник
121
Даже если вы понимаете следующее объяснение, вы вполне можете пожаловаться на такое поведение на форуме Hibernate, потому что это глупое поведение!
Tom Anderson
17
Совершенно верно, Том, я забыл о высокомерном отношении Гэвина Кингса. Он также говорит, что «Hibernate не отфильтровывает эти повторяющиеся ссылки по умолчанию. Некоторые люди (не вы) действительно хотят этого. Мне было бы интересно, когда люди на самом деле это мучают.
Пол Тейлор
16
@TomAnderson, да, именно так. зачем кому-то эти дубликаты? Я спрашиваю из чистого любопытства, так как понятия не имею ... Вы можете создавать дубликаты сами, столько, сколько захотите .. ;-)
Паробай
13
Вздох. На самом деле это недостаток Hibernate, ИМХО. Я хочу оптимизировать свои запросы, поэтому в моем файле сопоставления я перехожу от «выбрать» к «присоединиться». Внезапно мой код ЛОМАЕТСЯ повсюду. Затем я бегаю и исправляю все свои DAO, добавляя преобразователи результатов и многое другое. Пользовательский опыт == очень отрицательный. Я понимаю, что некоторым людям очень нравится иметь дубликаты по странным причинам, но почему я не могу сказать «получить эти объекты БЫСТРЕЕ, но не вызывать у меня дубликаты», указав fetch = "justworkplease"?
Роман Зенка
@Eran: Я столкнулся с подобной проблемой. Я не получаю повторяющихся родительских объектов, но я получаю дочерние объекты в каждом родительском объекте, повторяющиеся столько раз, сколько родительских объектов в ответе. Есть идеи, почему эта проблема?
мантри
93

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

criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
EJB
источник
8
Это будет работать в большинстве случаев .... за исключением случаев, когда вы пытаетесь использовать критерии для выборки 2 коллекций / ассоциаций.
JamesD
42

пытаться

@Fetch (FetchMode.SELECT) 

например

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

}

Мати
источник
11
FetchMode.SELECT увеличивает количество SQL-запросов, запускаемых Hibernate, но обеспечивает только один экземпляр для каждой записи корневого объекта. В этом случае Hibernate активирует выбор для каждой дочерней записи. Так что вы должны учитывать это с точки зрения производительности.
Бипул
1
@BipulKumar да, но это вариант, когда мы не можем использовать ленивую выборку, потому что нам нужно поддерживать сеанс ленивой выборки для доступа к подобъектам.
mathi
18

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

@OneToMany(targetEntity = OrderTransaction.class, cascade = CascadeType.ALL)
public Set<OrderTransaction> getOrderTransactions() {
    return orderTransactions;
}
Αλέκος
источник
2
Это случайное упоминание о передовой практике Hibernate или имеет отношение к вопросу извлечения нескольких дочерних элементов из OP?
Jacob Zwiers
Понял. Вторично по отношению к вопросу ОП. Хотя к статье dzone, вероятно, следует отнестись с недоверием ... исходя из собственного признания автора в комментариях.
Jacob Zwiers
2
ИМО, это очень хороший ответ. Если вам не нужны дубликаты, весьма вероятно, что вы предпочтете использовать Set, а не список. Использование Set (и, конечно, реализация правильных методов equals / hascode) решило проблему для меня. Остерегайтесь при реализации hashcode / equals, как указано в документе redhat, не использовать поле id.
Мат
1
Спасибо за вашу IMO. Более того, не нужно создавать проблемы с созданием методов equals () и hashCode (). Пусть ваша IDE или Lombok сгенерируют их за вас.
Αλέκος
3

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

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

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

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

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

Easyscript
источник
3

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

@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 решения:

  1. Изменить список на LinkedHashSet
  2. Используйте EntityGraph только с полем "food" и введите 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);
Григорий Кислин
источник
1

Вместо использования таких хаков, как:

  • Set вместо того List
  • criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);

которые не изменяют ваш sql-запрос, мы можем использовать (цитируя спецификации JPA)

q.select(emp).distinct(true);

который изменяет результирующий запрос sql, таким образом имея DISTINCTв нем.

Kaustubh
источник
0

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

return results.stream().distinct().collect(Collectors.toList());
Pravin
источник