Kế thừa JPA @EntityGraph bao gồm các hiệp hội tùy chọn của các lớp con


12

Đưa ra mô hình miền sau, tôi muốn tải tất cả các Answers bao gồm cả con Valuevà các phần tử con tương ứng của chúng và đặt nó vào một AnswerDTOđể sau đó chuyển đổi thành JSON. Tôi có một giải pháp làm việc nhưng nó gặp phải vấn đề N + 1 mà tôi muốn loại bỏ bằng cách sử dụng quảng cáo @EntityGraph. Tất cả các hiệp hội được cấu hình LAZY.

nhập mô tả hình ảnh ở đây

@Query("SELECT a FROM Answer a")
@EntityGraph(attributePaths = {"value"})
public List<Answer> findAll();

Sử dụng một quảng cáo @EntityGraphtrên Repositoryphương pháp tôi có thể đảm bảo rằng các giá trị được tìm nạp trước để ngăn N + 1 trên Answer->Valueliên kết. Trong khi kết quả của tôi vẫn ổn, có một vấn đề N + 1 khác, vì lười tải selectedliên kết của MCValues.

Sử dụng cái này

@EntityGraph(attributePaths = {"value.selected"})

thất bại, vì selectedtrường tất nhiên chỉ là một phần của một số Valuethực thể:

Unable to locate Attribute  with the the given name [selected] on this ManagedType [x.model.Value];

Làm thế nào tôi có thể nói với JPA chỉ thử tìm nạp selectedliên kết trong trường hợp giá trị là a MCValue? Tôi cần một cái gì đó như optionalAttributePaths.

Câu trả lời:


8

Bạn chỉ có thể sử dụng EntityGraphnếu thuộc tính kết hợp là một phần của siêu lớp và bởi đó cũng là một phần của tất cả các lớp con. Nếu không, ý EntityGraphchí luôn luôn thất bại với những Exceptiongì bạn hiện đang nhận được.

Cách tốt nhất để tránh sự cố chọn N + 1 của bạn là chia truy vấn của bạn thành 2 truy vấn:

Truy vấn thứ nhất tìm nạp các MCValuethực thể bằng cách sử dụng EntityGraphđể tìm nạp liên kết được ánh xạ bởi selectedthuộc tính. Sau truy vấn đó, các thực thể này sau đó được lưu trữ trong bộ đệm cấp 1 / bối cảnh tồn tại của Hibernate. Hibernate sẽ sử dụng chúng khi nó xử lý kết quả của truy vấn thứ 2.

@Query("SELECT m FROM MCValue m") // add WHERE clause as needed ...
@EntityGraph(attributePaths = {"selected"})
public List<MCValue> findAll();

Truy vấn thứ 2 sau đó tìm nạp Answerthực thể và sử dụng một EntityGraphđể cũng tìm nạp các Valuethực thể liên quan. Đối với mỗi Valuethực thể, Hibernate sẽ khởi tạo lớp con cụ thể và kiểm tra xem bộ đệm cấp 1 đã có chứa một đối tượng cho tổ hợp khóa chính và lớp đó chưa. Nếu đó là trường hợp, Hibernate sử dụng đối tượng từ bộ đệm cấp 1 thay vì dữ liệu được truy vấn trả về.

@Query("SELECT a FROM Answer a")
@EntityGraph(attributePaths = {"value"})
public List<Answer> findAll();

Vì chúng tôi đã tìm nạp tất cả các MCValuethực thể với các selectedthực thể được liên kết , giờ đây chúng tôi có được Answercác thực thể có valueliên kết được khởi tạo . Và nếu hiệp hội chứa một MCValuethực thể, selectedliên kết của nó cũng sẽ được khởi tạo.


Tôi nghĩ về việc có hai truy vấn, thứ nhất để tìm nạp câu trả lời + giá trị và câu hỏi thứ hai để tìm nạp selectedcho những câu trả lời có a MCValue. Tôi không thích rằng điều này sẽ yêu cầu một vòng lặp bổ sung và tôi sẽ cần quản lý ánh xạ giữa các bộ dữ liệu. Tôi thích ý tưởng của bạn để khai thác bộ đệm Hibernate cho việc này. Bạn có thể giải thích mức độ an toàn (về tính nhất quán) dựa vào bộ đệm để chứa kết quả không? Điều này có hoạt động khi các truy vấn được thực hiện trong một giao dịch? Tôi sợ khó phát hiện và lẻ tẻ lỗi khởi tạo lười biếng.
Bị kẹt

1
Bạn cần thực hiện cả hai truy vấn trong cùng một giao dịch. Miễn là bạn làm điều đó và không xóa bối cảnh kiên trì của bạn, nó hoàn toàn an toàn. Bộ nhớ cache cấp 1 của bạn sẽ luôn chứa các MCValuethực thể. Và bạn không cần một vòng lặp bổ sung. Bạn nên tìm nạp tất cả các MCValuethực thể có 1 truy vấn tham gia Answervà sử dụng mệnh đề WHERE giống như truy vấn hiện tại của bạn. Tôi cũng đã nói về điều này trong luồng trực tiếp ngày hôm nay: youtu.be/70B9znTmi00?t=238 Nó bắt đầu lúc 3:58 nhưng tôi đã hỏi một vài câu hỏi khác ở giữa ...
Thorben Janssen

Tuyệt vời, cảm ơn đã theo dõi! Ngoài ra tôi muốn thêm, rằng giải pháp này yêu cầu 1 truy vấn cho mỗi Lớp con. Vì vậy, khả năng bảo trì là ok-ish đối với chúng tôi nhưng giải pháp này có thể không phù hợp với mọi trường hợp.
Bị kẹt

Tôi cần sửa lại nhận xét cuối cùng của mình một chút: Tất nhiên bạn chỉ cần một truy vấn cho mỗi lớp con bị vấn đề. Ngoài ra, điều đáng chú ý là, đối với các thuộc tính của các lớp con, điều này dường như không gây ra vấn đề gì, vì sử dụng SINGLE_TABLE_INHERITANCE.
Bị kẹt

7

Tôi không biết Spring-Data đang làm gì ở đó, nhưng để làm điều đó, bạn thường phải sử dụng TREATtoán tử để có thể truy cập vào hiệp hội phụ nhưng việc triển khai cho Toán tử đó khá lỗi. Hibernate hỗ trợ truy cập thuộc tính ngầm định là điều bạn cần ở đây, nhưng rõ ràng Spring-Data không thể xử lý việc này đúng cách. Tôi có thể khuyên bạn nên xem Blaze-Persistence Entity-Views , một thư viện hoạt động trên JPA, cho phép bạn ánh xạ các cấu trúc tùy ý theo mô hình thực thể của bạn. Bạn có thể ánh xạ mô hình DTO của mình theo cách an toàn, cũng là cấu trúc kế thừa. Chế độ xem thực thể cho trường hợp sử dụng của bạn có thể trông như thế này

@EntityView(Answer.class)
interface AnswerDTO {
  @IdMapping
  Long getId();
  ValueDTO getValue();
}
@EntityView(Value.class)
@EntityViewInheritance
interface ValueDTO {
  @IdMapping
  Long getId();
}
@EntityView(TextValue.class)
interface TextValueDTO extends ValueDTO {
  String getText();
}
@EntityView(RatingValue.class)
interface RatingValueDTO extends ValueDTO {
  int getRating();
}
@EntityView(MCValue.class)
interface TextValueDTO extends ValueDTO {
  @Mapping("selected.id")
  Set<Long> getOption();
}

Với tích hợp dữ liệu mùa xuân được cung cấp bởi Blaze-Persistence, bạn có thể xác định một kho lưu trữ như thế này và trực tiếp sử dụng kết quả

@Transactional(readOnly = true)
interface AnswerRepository extends Repository<Answer, Long> {
  List<AnswerDTO> findAll();
}

Nó sẽ tạo ra một truy vấn HQL chỉ chọn những gì bạn đã ánh xạ trong AnswerDTOđó giống như sau.

SELECT
  a.id, 
  v.id,
  TYPE(v), 
  CASE WHEN TYPE(v) = TextValue THEN v.text END,
  CASE WHEN TYPE(v) = RatingValue THEN v.rating END,
  CASE WHEN TYPE(v) = MCValue THEN s.id END
FROM Answer a
LEFT JOIN a.value v
LEFT JOIN v.selected s

Hmm cảm ơn bạn đã gợi ý thư viện của bạn mà tôi đã tìm thấy, nhưng chúng tôi sẽ không sử dụng nó vì 2 lý do chính: 1) chúng tôi không thể dựa vào lib để được hỗ trợ trong suốt thời gian thực hiện dự án của chúng tôi (công ty của bạn khá nhỏ và trong sự khởi đầu của nó). 2) Chúng tôi sẽ không cam kết với một ngăn xếp công nghệ phức tạp hơn để tối ưu hóa một truy vấn duy nhất. (Tôi biết rằng lib của bạn có thể làm được nhiều hơn, nhưng chúng tôi thích một ngăn xếp công nghệ chung và thay vào đó sẽ chỉ thực hiện một truy vấn / chuyển đổi tùy chỉnh nếu không có giải pháp JPA).
Bị kẹt

1
Blaze-Persistence là mã nguồn mở và Entity-Views được triển khai ít nhiều trên JPQL / HQL là tiêu chuẩn. Các tính năng mà nó triển khai ổn định và vẫn sẽ hoạt động với các phiên bản tương lai của Hibernate, vì nó hoạt động trên tiêu chuẩn. Tôi hiểu rằng bạn không muốn giới thiệu thứ gì đó vì một trường hợp sử dụng duy nhất, nhưng tôi nghi ngờ đó là trường hợp sử dụng duy nhất mà bạn có thể sử dụng Chế độ xem Thực thể. Giới thiệu Chế độ xem thực thể thường dẫn đến việc giảm đáng kể số lượng mã soạn sẵn và cũng tăng hiệu suất truy vấn. Nếu bạn không muốn sử dụng các công cụ giúp bạn, thì hãy là nó.
Christian Beikov

Ít nhất bạn giải quyết vấn đề và bạn cung cấp một giải pháp. Vì vậy, bạn nhận được tiền thưởng mặc dù các câu trả lời không giải thích được những gì đang xảy ra trong vấn đề ban đầu và làm thế nào JPA có thể giải quyết nó. Từ nhận thức của tôi, nó chỉ không được hỗ trợ bởi JPA và nó sẽ trở thành một yêu cầu tính năng. Tôi sẽ cung cấp một tiền thưởng khác cho câu trả lời chi tiết hơn chỉ nhắm mục tiêu vào JPA.
Bị kẹt

Điều đó đơn giản là không thể với JPA. Bạn cần toán tử TREAT không được hỗ trợ đầy đủ trong bất kỳ nhà cung cấp JPA nào, cũng không được hỗ trợ trong các chú thích EntityGraph. Vì vậy, cách duy nhất bạn có thể mô hình hóa điều này là thông qua tính năng phân giải thuộc tính ngầm định Hibernate, yêu cầu bạn sử dụng các phép nối rõ ràng.
Christian Beikov

1
Trong câu trả lời của bạn, định nghĩa chế độ xem phải làinterface MCValueDTO extends ValueDTO { @Mapping("selected.id") Set<Long> getOption(); }
Bị kẹt

0

Dự án mới nhất của tôi đã sử dụng GraphQL (lần đầu tiên đối với tôi) và chúng tôi gặp vấn đề lớn với các truy vấn N + 1 và cố gắng tối ưu hóa các truy vấn để chỉ tham gia cho các bảng khi chúng được yêu cầu. Tôi đã tìm thấy Cosium / spring-data-jpa-entity-graph không thể thay thế. Nó mở rộng JpaRepositoryvà thêm các phương thức để chuyển trong biểu đồ thực thể vào truy vấn. Sau đó, bạn có thể xây dựng các biểu đồ thực thể động trong thời gian chạy để thêm vào các phép nối trái chỉ cho dữ liệu bạn cần.

Luồng dữ liệu của chúng tôi trông giống như thế này:

  1. Nhận yêu cầu GraphQL
  2. Phân tích yêu cầu GraphQL và chuyển đổi sang danh sách các nút biểu đồ thực thể trong truy vấn
  3. Tạo biểu đồ thực thể từ các nút được phát hiện và chuyển vào kho lưu trữ để thực thi

Để giải quyết vấn đề không bao gồm các nút không hợp lệ vào biểu đồ thực thể (ví dụ __typenametừ graphql), tôi đã tạo một lớp tiện ích xử lý việc tạo biểu đồ thực thể. Lớp gọi đi trong tên lớp mà nó đang tạo biểu đồ, sau đó xác nhận hợp lệ từng nút trong biểu đồ dựa trên siêu mô hình được duy trì bởi ORM. Nếu nút không có trong mô hình, nó sẽ loại bỏ nó khỏi danh sách các nút đồ thị. (Kiểm tra này cần phải được đệ quy và kiểm tra từng đứa trẻ)

Trước khi tìm thấy điều này, tôi đã thử các phép chiếu và mọi phương án khác được đề xuất trong các tài liệu Spring JPA / Hibernate, nhưng dường như không có gì có thể giải quyết vấn đề một cách thanh lịch hoặc ít nhất là với một tấn mã bổ sung


Làm thế nào để nó giải quyết vấn đề tải các hiệp hội không được biết đến từ siêu loại? Ngoài ra, như đã nói với câu trả lời khác, chúng tôi muốn biết liệu có giải pháp JPA thuần túy hay không, nhưng tôi cũng nghĩ rằng lib phải chịu cùng một vấn đề mà selectedhiệp hội không có sẵn cho tất cả các loại phụ value.
Bị kẹt

Nếu bạn đang quan tâm đến GraphQL, chúng tôi cũng có một hội nhập của Blaze-Persistence Entity Lần với graphql-java: persistence.blazebit.com/documentation/1.5/entity-view/manual/...
Christian Beikov

@ChristianBeikov cảm ơn nhưng chúng tôi đang sử dụng SQPR để tạo lược đồ của chúng tôi theo chương trình từ các mô hình / phương thức của chúng tôi
aarbor

Nếu bạn thích cách tiếp cận mã đầu tiên, bạn sẽ thích tích hợp GraphQL. Nó xử lý chỉ tìm nạp các cột / biểu thức thực sự được sử dụng làm giảm các phép nối v.v.
Christian Beikov

0

Chỉnh sửa sau khi nhận xét của bạn:

Tôi xin lỗi, tôi đã không hiểu rõ vấn đề của bạn trong vòng đầu tiên, vấn đề của bạn xảy ra khi khởi động dữ liệu mùa xuân, không chỉ khi bạn cố gắng gọi hàm findAll ().

Vì vậy, bây giờ bạn có thể điều hướng ví dụ đầy đủ có thể được lấy từ github của tôi: https://github.com/bdzzaid/stackoverflow-java/blob/master/jpa-hibernate/

Bạn có thể dễ dàng tái tạo và khắc phục vấn đề của bạn trong dự án này.

Thực tế, dữ liệu Spring và ngủ đông không có khả năng xác định biểu đồ "được chọn" theo mặc định và bạn cần chỉ định cách để thu thập tùy chọn đã chọn.

Vì vậy, trước tiên, bạn phải khai báo NamedEntityGraphs của lớp Trả lời

Như bạn có thể thấy, có hai NamedEntityGraph cho giá trị thuộc tính của lớp Trả lời

  • Đầu tiên cho tất cả Giá trị mà không có mối quan hệ cụ thể để tải

  • Thứ hai cho giá trị Multichoice cụ thể . Nếu bạn loại bỏ cái này, bạn tái tạo ngoại lệ.

Thứ hai, bạn cần ở trong bối cảnh giao dịch answerRep repository.find ALL () nếu bạn muốn tìm nạp dữ liệu theo kiểu LAZY

@Entity
@Table(name = "answer")
@NamedEntityGraphs({
    @NamedEntityGraph(
            name = "graph.Answer", 
            attributeNodes = @NamedAttributeNode(value = "value")
    ),
    @NamedEntityGraph(
            name = "graph.AnswerMultichoice",
            attributeNodes = @NamedAttributeNode(value = "value"),
            subgraphs = {
                    @NamedSubgraph(
                            name = "graph.AnswerMultichoice.selected",
                            attributeNodes = {
                                    @NamedAttributeNode("selected")
                            }
                    )
            }
    )
}
)
public class Answer
{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(updatable = false, nullable = false)
    private int id;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "value_id", referencedColumnName = "id")
    private Value value;
// ..
}

Vấn đề không phải là tìm nạp value-association của Answernhưng có được sự selectedliên kết trong trường hợp valuelà a MCValue. Câu trả lời của bạn không bao gồm bất kỳ thông tin liên quan đến điều đó.
Bị kẹt

@Stuck Cảm ơn câu trả lời của bạn, bạn có thể vui lòng chia sẻ với tôi lớp MCValue không, tôi sẽ cố gắng tái tạo vấn đề của bạn tại địa phương.
bdzzaid

Ví dụ của bạn chỉ hoạt động vì bạn đã xác định hiệp hội OneToManynhư FetchType.EAGERnhưng như đã nêu trong câu hỏi: tất cả các hiệp hội là LAZY.
Bị kẹt

@Stuck Tôi đã cập nhật câu trả lời của mình kể từ lần cập nhật cuối cùng của bạn, hy vọng biết câu trả lời của tôi sẽ giúp bạn giải quyết vấn đề của mình và giúp bạn hiểu cách tải biểu đồ thực thể bao gồm các mối quan hệ tùy chọn.
bdzzaid

"Giải pháp" của bạn vẫn gặp phải vấn đề N + 1 ban đầu mà câu hỏi này là: đặt phương thức chèn và tìm trong các giao dịch khác nhau của bài kiểm tra của bạn và bạn thấy rằng jpa sẽ đưa ra một truy vấn DB selectedcho mỗi câu trả lời thay vì tải chúng trả trước.
Bị kẹt
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.