Hibernate Criteria trả về phần tử con nhiều lần với FetchType.EAGER


115

Tôi có một Orderlớp có danh sách OrderTransactionsvà tôi đã ánh xạ nó với ánh xạ một-nhiều-nhiều-ngủ-đông như vậy:

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

Các trường này Ordercũng có một trường orderStatus, được sử dụng để lọc với các Tiêu chí sau:

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

Điều này hoạt động và kết quả là như mong đợi.

Đây là câu hỏi của tôi : Tại sao, khi tôi đặt kiểu tìm nạp một cách rõ ràng EAGER, các Orders xuất hiện nhiều lần trong danh sách kết quả?

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

Tôi sẽ phải thay đổi mã Tiêu chí của mình như thế nào để đạt được kết quả tương tự với cài đặt mới?


1
Bạn đã thử bật show_sql để xem điều gì đang xảy ra bên dưới chưa?
Mirko N.

Vui lòng thêm mã lớp OrderTransaction và Order. \
Eran Medan

Câu trả lời:


115

Đây thực sự là hành vi được mong đợi nếu tôi hiểu đúng cấu hình của bạn.

Bạn nhận được cùng một Ordertrường hợp trong bất kỳ kết quả nào, nhưng vì bây giờ bạn đang thực hiện một phép nối với OrderTransaction, nó phải trả về cùng một lượng kết quả mà một phép nối sql thông thường sẽ trả về

Vì vậy, thực sự nó sẽ nở nhiều lần. điều này được chính tác giả (Gavin King) giải thích rất rõ ở đây : Nó vừa giải thích lý do tại sao, vừa giải thích cách thu được kết quả khác biệt


Cũng được đề cập trong Câu hỏi thường gặp về Hibernate :

Hibernate không trả lại các kết quả khác biệt cho một truy vấn có bật tìm nạp kết hợp bên ngoài cho một tập hợp (ngay cả khi tôi sử dụng từ khóa riêng biệt)? Trước tiên, bạn cần hiểu SQL và cách OUTER JOIN hoạt động trong SQL. Nếu bạn không hiểu đầy đủ và hiểu các phép nối ngoài trong SQL, đừng tiếp tục đọc mục Câu hỏi thường gặp này mà hãy tham khảo sách hướng dẫn hoặc hướng dẫn SQL. Nếu không, bạn sẽ không hiểu lời giải thích sau đây và bạn sẽ phàn nàn về hành vi này trên diễn đàn Hibernate.

Các ví dụ điển hình có thể trả về các tham chiếu trùng lặp của cùng một đối tượng 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();

Tất cả các ví dụ này đều tạo ra cùng một câu lệnh SQL:

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

Bạn muốn biết tại sao có các bản sao ở đó? Nhìn vào tập kết quả SQL, Hibernate không ẩn các bản sao này ở bên trái của kết quả được nối bên ngoài nhưng trả về tất cả các bản sao của bảng lái xe. Nếu bạn có 5 đơn hàng trong cơ sở dữ liệu và mỗi đơn hàng có 3 mục hàng, tập hợp kết quả sẽ là 15 hàng. Danh sách kết quả Java của các truy vấn này sẽ có 15 phần tử, tất cả đều thuộc kiểu Order. Chỉ có 5 phiên bản Order sẽ được tạo bởi Hibernate, nhưng các bản sao của tập kết quả SQL được lưu giữ dưới dạng các tham chiếu trùng lặp đến 5 trường hợp này. Nếu bạn không hiểu câu cuối cùng này, bạn cần phải đọc trên Java và sự khác biệt giữa một cá thể trên Java heap và một tham chiếu đến một cá thể như vậy.

(Tại sao lại kết hợp ngoài cùng bên trái? Nếu bạn có một đơn đặt hàng bổ sung không có mục hàng, tập kết quả sẽ là 16 hàng với NULL lấp đầy phía bên phải, nơi dữ liệu mục hàng dành cho đơn đặt hàng khác. Bạn muốn đơn đặt hàng ngay cả khi họ không có mục hàng, phải không? Nếu không, hãy sử dụng tìm nạp kết hợp bên trong trong HQL của bạn).

Hibernate không lọc ra các tham chiếu trùng lặp này theo mặc định. Một số người (không phải bạn) thực sự muốn điều này. Làm thế nào bạn có thể lọc chúng ra?

Như thế này:

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

121
Ngay cả khi bạn hiểu lời giải thích sau đây, bạn cũng có thể phàn nàn về hành vi này trên diễn đàn Hibernate, vì nó đang lật tẩy hành vi ngu ngốc!
Tom Anderson

17
Đúng vậy, Tom, tôi quên mất thái độ kiêu ngạo của Gavin Kings. Ông cũng nói rằng 'Hibernate không lọc ra các tham chiếu trùng lặp này theo mặc định. Một số người (không phải bạn) thực sự muốn điều này 'Tôi sẽ quan tâm khi mọi người thực sự phản đối điều này.
Paul Taylor

16
@TomAnderson vâng chính xác. tại sao bất cứ ai sẽ cần những bản sao? Tôi hỏi vì tò mò tinh khiết, vì tôi không có ý tưởng ... Bạn có thể tạo các bản sao chính mình, như nhiều người như bạn muốn .. ;-)
Parobay

13
Thở dài. Đây thực sự là lỗ hổng Hibernate, IMHO. Tôi muốn tối ưu hóa các truy vấn của mình, vì vậy tôi chuyển từ "chọn" sang "tham gia" vào tệp ánh xạ của mình. Đột nhiên mã của tôi BREAKS khắp nơi. Sau đó, tôi chạy xung quanh và sửa chữa tất cả các DAO của mình bằng cách thêm các máy biến áp kết quả và những thứ khác. Trải nghiệm người dùng == rất tiêu cực. Tôi hiểu rằng một số người hoàn toàn thích có các bản sao vì những lý do kỳ lạ, nhưng tại sao tôi không thể nói "tìm nạp các đối tượng này NHANH hơn nhưng không làm tôi khó chịu với các bản sao" bằng cách chỉ định tìm nạp = "justworkplease"?
Roman Zenka

@Eran: Tôi đang đối mặt với loại vấn đề tương tự. Tôi không nhận được các đối tượng mẹ trùng lặp, nhưng tôi nhận được các đối tượng con trong mỗi đối tượng mẹ được lặp lại nhiều lần khi có số lượng đối tượng mẹ trong phản hồi. Bất kỳ ý tưởng tại sao vấn đề này?
mantri,

93

Ngoài những gì được đề cập bởi Eran, một cách khác để có được hành vi bạn muốn, đó là đặt biến áp kết quả:

criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);

8
Điều này sẽ hoạt động cho hầu hết các trường hợp .... ngoại trừ khi bạn cố gắng sử dụng Tiêu chí để tìm nạp 2 bộ sưu tập / liên kết.
JamesD

42

thử

@Fetch (FetchMode.SELECT) 

ví dụ

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

}


11
FetchMode.SELECT tăng số lượng truy vấn SQL được kích hoạt bởi Hibernate nhưng đảm bảo chỉ một phiên bản cho mỗi bản ghi thực thể gốc. Hibernate sẽ kích hoạt một lựa chọn cho mọi bản ghi con trong trường hợp này. Vì vậy, bạn nên giải thích nó đối với các cân nhắc về hiệu suất.
Bipul

1
@BipulKumar có, nhưng đây là tùy chọn khi chúng ta không thể sử dụng tìm nạp lười vì chúng ta cần duy trì một phiên tìm nạp lười để truy cập các đối tượng con.
mathi

18

Không sử dụng List và ArrayList mà là Set và HashSet.

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

2
Đây có phải là đề cập ngẫu nhiên về phương pháp hay nhất Hibernate hay liên quan đến câu hỏi truy xuất nhiều trẻ em từ OP không?
Jacob Zwiers


Hiểu rồi. Thứ hai cho câu hỏi của OP. Mặc dù vậy, bài báo của dzone có lẽ nên được coi là muối bỏ bể ... dựa trên sự thừa nhận của chính tác giả trong các bình luận.
Jacob Zwiers

2
Đây là một câu trả lời rất tốt IMO. Nếu bạn không muốn các bản sao, rất có thể bạn muốn sử dụng Tập hợp hơn là Danh sách- Việc sử dụng Tập hợp (và triển khai các phương thức bằng / mã băm đúng) đã giải quyết được vấn đề cho tôi. Hãy cẩn thận khi triển khai mã băm / bằng, như đã nêu trong tài liệu redhat, không sử dụng trường id.
Mat

1
Cảm ơn IMO của bạn. Hơn nữa, đừng gặp rắc rối khi tạo phương thức equals () và hashCode (). Hãy để IDE hoặc Lombok của bạn tạo chúng cho bạn.
Αλέκος

3

Sử dụng Java 8 và Luồng, tôi thêm vào phương thức tiện ích của mình trạng thái trả về này:

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

Luồng loại bỏ trùng lặp rất nhanh. Tôi sử dụng chú thích trong lớp Thực thể của mình như thế này:

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

Tôi nghĩ rằng ứng dụng của tôi có thể sử dụng phiên trong phương pháp mà tôi cần dữ liệu từ cơ sở dữ liệu. Phiên họp của Closse khi tôi làm xong. Tất nhiên đặt lớp Thực thể của tôi để sử dụng kiểu tìm nạp leasy. Tôi đi tái cấu trúc.


3

Tôi gặp vấn đề tương tự khi tìm nạp 2 bộ sưu tập được liên kết: người dùng có 2 vai trò (Bộ) và 2 bữa ăn (Danh sách) và các bữa ăn bị trùng lặp.

@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 không trợ giúp (truy vấn 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);

Cuối cùng, tôi đã tìm thấy 2 giải pháp:

  1. Thay đổi danh sách thành LinkedHashSet
  2. Sử dụng EntityGraph chỉ với trường "bữa ăn" và nhập LOAD, tải các vai trò như chúng đã khai báo (EAGER và bởi BatchSize = 200 để ngăn N + 1 sự cố):

Giải pháp cuối cùng:

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

1

Thay vì sử dụng các bản hack như:

  • Set thay vì List
  • criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);

mà không sửa đổi truy vấn sql của bạn, chúng tôi có thể sử dụng (trích dẫn thông số kỹ thuật JPA)

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

mà sửa đổi truy vấn sql kết quả, do đó có một DISTINCTtrong đó.


0

Nghe có vẻ không phải là một hành vi tuyệt vời khi áp dụng một phép nối bên ngoài và mang lại kết quả trùng lặp. Giải pháp duy nhất còn lại là lọc kết quả của chúng tôi bằng cách sử dụng các luồng. Cảm ơn java8 đã cho cách lọc dễ dàng hơn.

return results.stream().distinct().collect(Collectors.toList());
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.