Критерії сплячки повертають дітей кілька разів за допомогою FetchType.EAGER


115

У мене є Orderклас, у якому є список, OrderTransactionsі я відобразив його за допомогою картографії зі сплячим режимом «багато в чому»:

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

Як мені доведеться змінити свій код критеріїв, щоб досягти такого ж результату, як новий параметр?


1
Ви намагалися включити show_sql, щоб побачити, що відбувається під ним?
Мирко Н.

Будь ласка, додайте також код класів OrderTransaction та Order. \
Еран Медан

Відповіді:


115

Це насправді очікувана поведінка, якщо я правильно зрозумів вашу конфігурацію.

Ви отримуєте один і той же Orderекземпляр у будь-якому з результатів, але оскільки ви зараз робите з'єднання з OrderTransaction, він повинен повернути ту саму кількість результатів, що і звичайне з'єднання sql повернеться

Тож насправді воно повинно з’являтися кілька разів. це дуже добре пояснює сам автор (Гевін Кінг) тут : Це і пояснює, чому, і як все-таки отримати чіткі результати


Також згадується в FAQ про сплячку :

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

Типові приклади, які можуть повертати повторювані посилання одного і того ж об’єкта Порядку:

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 елементів, всі типу "Порядок". Лише 5 екземплярів порядку буде створено Hibernate, але дублікати набору результатів SQL зберігаються як повторювані посилання на ці 5 екземплярів. Якщо ви не розумієте цього останнього речення, вам потрібно прочитати на Java та різницю між екземпляром на купі Java та посиланням на такий екземпляр.

. у них немає рядків-позицій, правда? Якщо ні, використовуйте внутрішню групу приєднання у своєму HQL).

Hibernate за замовчуванням не фільтрує ці дублікати посилань. Деякі люди (не ви) насправді цього хочуть. Як ви можете їх відфільтрувати?

Подобається це:

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

121
Навіть якщо ви розумієте наступне пояснення, ви можете скаржитися на таку поведінку на форумі зимування, адже це відверта дурна поведінка!
Том Андерсон

17
Цілком правильно Том, я забув про зарозуміле ставлення до Гевіна Кінгса. Він також каже, що "сплячий режим" не фільтрує ці повторювані посилання за замовчуванням. Деякі люди (не ви) насправді хочуть цього "Мені цікаво, коли люди насправді це мурахи.
Пол Тейлор

16
@TomAnderson так точно. навіщо комусь потрібні ці копії? Я прошу з чистої цікавості, оскільки я поняття не маю ... Ви можете створювати дублікати самостійно, скільки завгодно бажаєте .. ;-)
Паробай

13
Зітхнути. Це насправді хибернальна вада, ІМХО. Я хочу оптимізувати свої запити, тому я переходжу від "вибору" до "приєднатися" у моєму картографічному файлі. Раптом мій код БЕЗПЕЧАТИ повсюди. Потім я оббігаю і виправляю всі мої DAO, додаючи трансформатори результатів і все таке. Досвід користувачів == дуже негативно. Я розумію, що деякі люди абсолютно люблять мати дублікати з химерних причин, але чому я не можу сказати "принести ці об’єкти Швидше, але не помилуйте мене дублікатами", вказавши fetch = "justworkplease"?
Роман Зенька

@Eran: Я стикаюся з подібною проблемою. Я не отримую дублюючих батьківських об'єктів, але я отримую, щоб діти в кожному батьківському об'єкті повторювалися стільки разів, скільки у відповіді є кількість батьківських об'єктів. Будь-яка ідея, чому ця проблема?
мантрі

93

Окрім того, що згадує Еран, ще один спосіб отримати потрібну поведінку - це встановити трансформатор результату:

criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);

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, але забезпечує лише один екземпляр на запис кореневої сутності. У цьому випадку сплячий режим запустить вибраний для кожної дитини запис. Тож вам слід це враховувати з точки зору продуктивності.
Біпул

1
@BipulKumar так, але це варіант, коли ми не можемо використовувати ледачий збір, тому що нам потрібно підтримувати сеанс для ледачого вибору для доступу до під об’єктів.
матчі

18

Не використовуйте List і ArrayList, але встановіть і HashSet.

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

2
Це випадкова згадка про найкращу практику сплячки або має відношення до запитання щодо отримання кількох дітей з ОП?
Яків Цвіерс


Зрозумів. Вторинне питання щодо ОП. Хоча статтю про зону, мабуть, слід сприймати із зерном солі ... на основі власного визнання автора у коментарях.
Яків Цвіерс

2
Це дуже хороша відповідь ІМО. Якщо ви не хочете дублікатів, велика ймовірність, що ви скористаєтеся набором, ніж списком. Використання набору (та впровадження правильних методів рівних / має код коду) вирішило проблему для мене. Обережно, використовуючи хеш-код / ​​дорівнює, як зазначено в документі redhat, не використовувати поле id.
Мат

1
Дякуємо за ваш ІМО. Більше того, не заважайте створювати методи 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, щоб використовувати лізинговий тип вибору. Я йду в рефактор.


3

У мене є та ж проблема, щоб отримати 2 асоційовані колекції: користувач має 2 ролі (Set) та 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 рішення:

  1. Змінити список на LinkedHashSet
  2. Використовуйте 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);

1

Замість використання хаків на зразок:

  • Set замість List
  • criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);

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

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

який змінює отриманий запит sql, тим самим маючи DISTINCTв ньому.


0

Це не звучить чудовою поведінкою, застосовуючи зовнішнє з'єднання та приносить дублювані результати. Залишилося єдине рішення - відфільтрувати результат за допомогою потоків. Завдяки java8 дає простіший спосіб фільтрації.

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