Kryteria hibernacji zwracają elementy potomne wielokrotnie z FetchType.EAGER

115

Mam Orderklasę, która ma listę OrderTransactionsi zmapowałem ją za pomocą mapowania Hibernacja jeden do wielu w następujący sposób:

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

Te Orderpola mają również pole orderStatus, które jest używane do filtrowania według następujących kryteriów:

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

To działa, a wynik jest zgodny z oczekiwaniami.

Teraz tutaj jest moje pytanie : Dlaczego, kiedy ustawić typ pobrać bezpośrednio do EAGERwykonaj Orders pojawiają się wielokrotnie w otrzymanym liście?

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

W jaki sposób musiałbym zmienić kod kryteriów, aby osiągnąć ten sam wynik w nowym ustawieniu?

Raoulsson
źródło
1
Czy próbowałeś włączyć show_sql, aby zobaczyć, co się dzieje pod spodem?
Mirko N.
Proszę również dodać kod klas OrderTransaction i Order. \
Eran Medan

Odpowiedzi:

115

Jest to faktycznie oczekiwane zachowanie, jeśli poprawnie zrozumiałem twoją konfigurację.

Otrzymujesz to samo Orderwystąpienie w każdym z wyników, ale od teraz wykonujesz sprzężenie zOrderTransaction , musi zwrócić taką samą ilość wyników, jaką zwróci zwykłe sprzężenie sql

W rzeczywistości powinno się to pojawiać wiele razy. wyjaśnia to bardzo dobrze sam autor (Gavin King) tutaj : Obaj wyjaśnia, dlaczego i jak nadal uzyskać wyraźne wyniki


Wspomniane również w FAQ Hibernacji :

Hibernate nie zwraca odrębnych wyników dla zapytania z włączonym pobieraniem sprzężenia zewnętrznego dla kolekcji (nawet jeśli używam odrębnego słowa kluczowego)? Po pierwsze, musisz zrozumieć SQL i jak działają OUTER JOIN w SQL. Jeśli nie rozumiesz w pełni i nie rozumiesz sprzężeń zewnętrznych w języku SQL, nie czytaj dalej tego artykułu FAQ, ale zapoznaj się z instrukcją lub samouczkiem SQL. W przeciwnym razie nie zrozumiesz następującego wyjaśnienia i będziesz narzekać na to zachowanie na forum Hibernate.

Typowe przykłady, które mogą zwrócić zduplikowane odwołania do tego samego obiektu 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();

Wszystkie te przykłady tworzą tę samą instrukcję SQL:

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

Chcesz wiedzieć, dlaczego istnieją duplikaty? Spójrz na zestaw wyników SQL. Hibernate nie ukrywa tych duplikatów po lewej stronie zewnętrznego połączonego wyniku, ale zwraca wszystkie duplikaty tabeli sterującej. Jeśli masz w bazie danych 5 zamówień, a każde zamówienie ma 3 pozycje, zestaw wyników będzie miał 15 wierszy. Lista wyników Java dla tych zapytań będzie zawierać 15 elementów, wszystkie typu Order. Tylko 5 instancji Order zostanie utworzonych przez Hibernate, ale duplikaty zestawu wyników SQL są zachowywane jako zduplikowane odwołania do tych 5 instancji. Jeśli nie rozumiesz tego ostatniego zdania, musisz poczytać o Javie i różnicy między instancją na stercie Java a odwołaniem do takiej instancji.

(Dlaczego lewe sprzężenie zewnętrzne? Jeśli miałbyś dodatkowe zamówienie bez elementów zamówienia, zestaw wyników składałby się z 16 wierszy z NULL wypełniającymi po prawej stronie, gdzie dane elementu zamówienia dotyczą innego zamówienia. Chcesz zamówień, nawet jeśli nie mają elementów zamówienia, prawda? Jeśli nie, użyj pobierania sprzężenia wewnętrznego w HQL).

Hibernate nie odfiltrowuje domyślnie tych zduplikowanych odniesień. Niektórzy ludzie (nie ty) naprawdę tego chcą. Jak możesz je odfiltrować?

Lubię to:

Collection result = new LinkedHashSet( session.create*(...).list() );
Eran Medan
źródło
121
Nawet jeśli rozumiesz poniższe wyjaśnienie, możesz narzekać na to zachowanie na forum Hibernate, ponieważ jest to głupie zachowanie!
Tom Anderson,
17
Całkiem dobrze Tom, zapomniałam o aroganckiej postawie Gavina Kingsa. Mówi również, że „Hibernate nie filtruje domyślnie tych zduplikowanych odniesień. Niektórzy ludzie (nie ty) naprawdę tego chcą. Byłbym zainteresowany, kiedy ludzie rzeczywiście to robią.
Paul Taylor,
16
@TomAnderson tak, dokładnie. dlaczego ktoś miałby potrzebować tych duplikatów? Pytam z czystej ciekawości, bo nie mam pojęcia ... Możesz samodzielnie tworzyć duplikaty, ile zechcesz .. ;-)
Parobay
13
Westchnienie. W rzeczywistości jest to wada Hibernate, IMHO. Chcę zoptymalizować moje zapytania, więc przechodzę od „wybierz” do „dołącz” w moim pliku mapowania. Nagle mój kod ŁAMI SIĘ w każdym miejscu. Potem biegam i naprawiam wszystkie moje DAO, dołączając transformatory wyników i tak dalej. Doświadczenie użytkownika == bardzo negatywne. Rozumiem, że niektórzy uwielbiają mieć duplikaty z dziwnych powodów, ale dlaczego nie mogę powiedzieć „pobierz te obiekty SZYBCIEJ, ale nie męcz mnie duplikatami”, podając fetch = „justworkplease”?
Roman Zenka
@Eran: Mam podobny problem. Nie otrzymuję zduplikowanych obiektów nadrzędnych, ale dzieci w każdym obiekcie nadrzędnym są powtarzane tyle razy, ile jest obiektów nadrzędnych w odpowiedzi. Każdy pomysł, dlaczego ten problem?
mantri
93

Oprócz tego, o czym wspomniał Eran, innym sposobem uzyskania pożądanego zachowania jest ustawienie transformatora wyników:

criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
EJB
źródło
8
To zadziała w większości przypadków ... z wyjątkiem sytuacji, gdy spróbujesz użyć kryteriów do pobrania 2 kolekcji / skojarzeń.
JamesD
42

próbować

@Fetch (FetchMode.SELECT) 

na przykład

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

}

mathi
źródło
11
FetchMode.SELECT zwiększa liczbę zapytań SQL uruchamianych przez Hibernate, ale zapewnia tylko jedno wystąpienie na rekord jednostki głównej. W tym przypadku Hibernacja uruchomi wybór dla każdego rekordu podrzędnego. Dlatego należy to uwzględnić w odniesieniu do kwestii wydajności.
Bipul
1
@BipulKumar tak, ale jest to opcja, gdy nie możemy użyć pobierania z opóźnieniem, ponieważ musimy utrzymywać sesję pobierania z opóźnieniem w celu uzyskania dostępu do obiektów podrzędnych.
mathi
18

Nie używaj List i ArrayList, ale Set i HashSet.

@OneToMany(targetEntity = OrderTransaction.class, cascade = CascadeType.ALL)
public Set<OrderTransaction> getOrderTransactions() {
    return orderTransactions;
}
Αλέκος
źródło
2
Czy jest to przypadkowa wzmianka o najlepszych praktykach Hibernate czy też ma znaczenie dla pytania o pobieranie wielu dzieci z PO?
Jacob Zwiers
Rozumiem. Wtórne w stosunku do pytania PO. Choć artykuł dzone należy chyba potraktować z przymrużeniem oka… na podstawie własnych wypowiedzi autora w komentarzach.
Jacob Zwiers
2
To bardzo dobra odpowiedź IMO. Jeśli nie chcesz duplikatów, jest wysoce prawdopodobne, że wolisz użyć zestawu niż listy - użycie zestawu (i zaimplementowanie poprawnych metod equals / hascode) rozwiązało problem za mnie. Uważaj podczas implementowania hashcode / equals, jak stwierdzono w dokumencie redhat, aby nie używać pola id.
Mat
1
Dzięki za twój IMO. Ponadto nie kłopocz się tworzeniem metod equals () i hashCode (). Niech Twoje IDE lub Lombok wygenerują je dla Ciebie.
Αλέκος
3

Używając Java 8 i strumieni, dodaję w mojej metodzie narzędziowej następującą informację zwrotną:

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

Strumienie bardzo szybko usuwają duplikaty. W mojej klasie Entity używam adnotacji w następujący sposób:

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

Myślę, że w mojej aplikacji lepiej jest używać sesji w metodzie, w której potrzebuję danych z bazy danych. Zamknięta sesja, kiedy skończyłem. Oczywiście ustaw moją klasę Entity, aby używała typu pobierania leasy. Idę do refaktoryzacji.

easyScript
źródło
3

Mam ten sam problem, aby pobrać 2 powiązane kolekcje: użytkownik ma 2 role (zestaw) i 2 posiłki (lista), a posiłki są zduplikowane.

@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 nie pomaga (zapytanie 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);

Nareszcie znalazłem 2 rozwiązania:

  1. Zmień listę na LinkedHashSet
  2. Użyj EntityGraph tylko z polem „posiłek” i wpisz LOAD, które ładują role zgodnie z deklaracją (EAGER i BatchSize = 200, aby zapobiec problemowi N + 1):

Ostateczne rozwiązanie:

@EntityGraph(attributePaths = {"meals"}, type = EntityGraph.EntityGraphType.LOAD)
@Query("SELECT u FROM User u WHERE u.id=?1")
User getWithMeals(int id);
Grigorij Kislin
źródło
1

Zamiast używać hacków, takich jak:

  • Set zamiast List
  • criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);

które nie modyfikują zapytania sql, możemy użyć (cytując specyfikacje JPA)

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

który nie modyfikuje wynikowego zapytania sql, w ten sposób mając DISTINCTw sobie.

Kaustubh
źródło
0

Stosowanie zewnętrznego sprzężenia nie wydaje się dobrym zachowaniem i przynosi zduplikowane rezultaty. Jedyne, co pozostało, to filtrowanie naszego wyniku za pomocą strumieni. Dzięki java8 dając łatwiejszy sposób filtrowania.

return results.stream().distinct().collect(Collectors.toList());
Pravin
źródło