JPA inheritance @EntityGraph มีการเชื่อมโยงทางเลือกของคลาสย่อย


12

ด้วยรูปแบบโดเมนต่อไปนี้ฉันต้องการโหลดAnswers ทั้งหมดรวมถึงValues และ sub-children ที่เกี่ยวข้องและวางลงในAnswerDTOเพื่อแปลงเป็น JSON ฉันมีวิธีการแก้ปัญหาการทำงาน แต่มันทนทุกข์ทรมานจากปัญหา N + 1 @EntityGraphที่ฉันต้องการที่จะกำจัดโดยใช้เฉพาะกิจ LAZYสมาคมทั้งหมดมีการกำหนดค่า

ป้อนคำอธิบายรูปภาพที่นี่

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

ใช้ ad-hoc @EntityGraphบนRepositoryวิธีการที่ฉันสามารถมั่นใจได้ว่าค่าถูกดึงมาล่วงหน้าเพื่อป้องกัน N + 1 ในการAnswer->Valueเชื่อมโยง ในขณะที่ผลลัพธ์ของฉันดีมีปัญหาอีก N + 1 เพราะขี้เกียจโหลดความselectedสัมพันธ์ของMCValues

ใช้สิ่งนี้

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

ล้มเหลวเนื่องจากselectedฟิลด์เป็นส่วนหนึ่งของValueเอนทิตีบางส่วนเท่านั้น:

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

ฉันจะบอก JPA ได้เพียงพยายามดึงการselectedเชื่อมโยงในกรณีที่ค่าเป็นMCValueอย่างไร optionalAttributePathsฉันต้องการสิ่งที่ต้องการ

คำตอบ:


8

คุณสามารถใช้เฉพาะEntityGraphถ้าแอตทริบิวต์การเชื่อมโยงเป็นส่วนหนึ่งของ superclass และโดยที่เป็นส่วนหนึ่งของคลาสย่อยทั้งหมด มิฉะนั้นEntityGraphจะล้มเหลวเสมอกับสิ่งExceptionที่คุณได้รับในปัจจุบัน

วิธีที่ดีที่สุดในการหลีกเลี่ยงปัญหาการเลือก N + 1 ของคุณคือการแบ่งคำถามของคุณออกเป็น 2 ข้อความค้นหา:

แบบสอบถามที่ 1 ดึงMCValueเอนทิตีที่ใช้EntityGraphเพื่อดึงการเชื่อมโยงที่แมปโดยselectedแอตทริบิวต์ หลังจากเคียวรีนั้นเอนทิตีเหล่านี้จะถูกเก็บไว้ในแคชระดับที่ 1 ของ Hibernate / บริบทการมีอยู่ Hibernate จะใช้พวกมันเมื่อประมวลผลผลลัพธ์ของแบบสอบถามที่สอง

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

แบบสอบถามที่ 2 จะดึงAnswerเอนทิตีและใช้EntityGraphเพื่อดึงValueเอนทิตีที่เกี่ยวข้องด้วย สำหรับแต่ละValueเอนทิตีไฮเบอร์เนตจะยกตัวอย่างคลาสย่อยเฉพาะและตรวจสอบว่าแคชระดับที่ 1 มีวัตถุสำหรับคลาสนั้นและคีย์หลักรวมกันหรือไม่ ในกรณีนี้ไฮเบอร์เนตจะใช้วัตถุจากแคชระดับที่ 1 แทนข้อมูลที่ส่งคืนโดยแบบสอบถาม

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

เนื่องจากเราได้ดึงMCValueเอนทิตีทั้งหมดที่มีเอนทิตีที่เกี่ยวข้องselectedตอนนี้เราได้รับAnswerเอนทิตีที่มีความvalueสัมพันธ์เริ่มต้น และหากสมาคมมีMCValueหน่วยงานselectedสมาคมก็จะเริ่มต้นด้วย


ฉันคิดเกี่ยวกับการมีสองคำสั่งที่ 1 สำหรับการเรียกคำตอบ + ค่าและ 2 หนึ่งในการดึงข้อมูลคำตอบสำหรับผู้ที่มีselected MCValueฉันไม่ชอบสิ่งนี้จะต้องวนซ้ำเพิ่มเติมและฉันจะต้องจัดการการแมประหว่างชุดข้อมูล ฉันชอบความคิดของคุณในการใช้ประโยชน์จากแคชไฮเบอร์เนตสำหรับสิ่งนี้ คุณสามารถอธิบายรายละเอียดเกี่ยวกับความปลอดภัย (ในแง่ของความสอดคล้อง) ว่าต้องอาศัยแคชเพื่อเก็บผลลัพธ์ได้อย่างไร มันใช้งานได้เมื่อมีการสอบถามในการทำธุรกรรมหรือไม่? ฉันกลัวว่าจะพบข้อผิดพลาดในการเริ่มต้นขี้เกียจอย่างหนักเป็นระยะ
ติด

1
คุณต้องดำเนินการทั้งสองแบบสอบถามภายในธุรกรรมเดียวกัน ตราบใดที่คุณทำเช่นนั้นและไม่ล้างบริบทการคงอยู่ของคุณจะปลอดภัยอย่างแน่นอน แคชระดับที่ 1 ของคุณจะมีMCValueเอนทิตีเสมอ และคุณไม่ต้องการการวนซ้ำเพิ่มเติม คุณควรดึงMCValueเอนทิตีทั้งหมดที่มี 1 คิวรีที่รวมกับAnswerและใช้ WHERE clause เหมือนกับเคียวรีปัจจุบันของคุณ ฉันยังพูดคุยเกี่ยวกับเรื่องนี้ในสตรีมสดของวันนี้: youtu.be/70B9znTmi00?t=238มันเริ่มต้นที่ 3:58 แต่ฉันใช้คำถามอื่นอีกสองสามข้อระหว่าง ...
Thorben Janssen

เยี่ยมมากขอบคุณสำหรับการติดตาม! นอกจากนี้ฉันต้องการเพิ่มว่าโซลูชันนี้ต้องการ 1 แบบสอบถามต่อคลาสย่อย ดังนั้นการบำรุงรักษาก็โอเคสำหรับเรา แต่วิธีนี้อาจไม่เหมาะกับทุกกรณี
ติด

ฉันต้องการแก้ไขความคิดเห็นล่าสุดของฉันเล็กน้อย: แน่นอนคุณต้องใช้แบบสอบถามต่อคลาสย่อยที่มีปัญหา นอกจากนี้ยังเป็นที่น่าสังเกตว่าสำหรับแอตทริบิวต์ของ subclasses นี้ดูเหมือนว่าจะไม่ beeing SINGLE_TABLE_INHERITANCEปัญหาเพราะการใช้
ติด

7

ฉันไม่ทราบว่า Spring-Data กำลังทำอะไรที่นั่น แต่โดยปกติคุณจะต้องใช้TREATโอเปอเรเตอร์เพื่อให้สามารถเข้าถึงการเชื่อมโยงย่อย แต่การใช้งานสำหรับผู้ดำเนินการนั้นค่อนข้างบั๊ก Hibernate รองรับการเข้าถึงคุณสมบัติย่อยโดยปริยายซึ่งเป็นสิ่งที่คุณต้องการที่นี่ แต่ Spring-Data ไม่สามารถจัดการกับสิ่งนี้ได้อย่างถูกต้อง ฉันขอแนะนำให้คุณดูที่ Blaze-Persistence Entity-Viewsซึ่งเป็นไลบรารีที่ทำงานบน JPA ซึ่งช่วยให้คุณแมปโครงสร้างตามอำเภอใจกับโมเดลเอนทิตีของคุณ คุณสามารถแมปโมเดล DTO ของคุณในวิธีที่ปลอดภัยประเภทรวมถึงโครงสร้างการสืบทอด เอนทิตีมุมมองสำหรับกรณีการใช้งานของคุณอาจมีลักษณะเช่นนี้

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

ด้วยการรวมข้อมูลสปริงที่จัดทำโดย Blaze-Persistence คุณสามารถกำหนดพื้นที่เก็บข้อมูลเช่นนี้และใช้ผลลัพธ์ได้โดยตรง

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

มันจะสร้างเคียวรี HQL ที่เลือกสิ่งที่คุณแม็พAnswerDTOซึ่งเป็นอะไรต่อไปนี้

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

อืมขอบคุณสำหรับคำใบ้ไปยังห้องสมุดของคุณที่ฉันได้พบแล้ว แต่เราจะไม่ใช้มันด้วยเหตุผลหลัก 2 ข้อ: 1) เราไม่สามารถพึ่งพา lib เพื่อรับการสนับสนุนตลอดอายุการใช้งานของโครงการของเรา (blazebit บริษัท ของคุณค่อนข้างเล็กและ ในจุดเริ่มต้น) 2) เราจะไม่ผูกมัดกับเทคโนโลยี่ที่ซับซ้อนมากขึ้นเพื่อเพิ่มประสิทธิภาพการสืบค้นเดียว (ฉันรู้ว่า lib ของคุณสามารถทำอะไรได้มากกว่า แต่เราชอบสแต็คเทคโนโลยีทั่วไปและค่อนข้างจะใช้แบบสอบถาม / การเปลี่ยนแปลงที่กำหนดเองหากไม่มีโซลูชัน JPA)
ติด

1
Blaze-Persistence เป็นโอเพ่นซอร์สและ Entity-Views นั้นถูกนำไปใช้งานไม่มากก็น้อยบน JPQL / HQL ซึ่งเป็นมาตรฐาน คุณสมบัติที่ใช้มีความเสถียรและจะยังคงใช้งานได้กับ Hibernate รุ่นต่อไปในอนาคตเพราะใช้งานได้ดีกว่ามาตรฐาน ฉันเข้าใจว่าคุณไม่ต้องการที่จะแนะนำอะไรบางอย่างเนื่องจากมีเคสใช้ครั้งเดียว แต่ฉันสงสัยว่าเป็นเคสใช้เดียวที่คุณสามารถใช้เอนทิตีวิว เอนทิตีมุมมองที่แนะนำมักจะนำไปสู่การลดจำนวนของรหัสสำเร็จรูปและยังเพิ่มประสิทธิภาพการค้นหา หากคุณไม่ต้องการใช้เครื่องมือที่ช่วยคุณ
Christian Beikov

อย่างน้อยคุณก็แก้ปัญหาไม่ได้และให้วิธีแก้ปัญหา ดังนั้นคุณจะได้รับเงินรางวัลแม้ว่าคำตอบจะไม่อธิบายว่าเกิดอะไรขึ้นในปัญหาดั้งเดิมและ JPA สามารถแก้ไขได้อย่างไร จากการรับรู้ของฉัน JPA ไม่ได้รับการสนับสนุนและควรเป็นคำขอคุณลักษณะ ฉันจะเสนอเงินรางวัลเพิ่มเติมสำหรับคำตอบเพิ่มเติมที่กำหนดเป้าหมาย JPA เท่านั้น
ติด

มันเป็นไปไม่ได้กับ JPA คุณต้องการโอเปอเรเตอร์ TREAT ซึ่งไม่ได้รับการสนับสนุนอย่างเต็มที่ในผู้ให้บริการ JPA ใด ๆ และไม่ได้รับการสนับสนุนในหมายเหตุประกอบ EntityGraph ดังนั้นวิธีเดียวที่คุณสามารถสร้างโมเดลนี้คือผ่านคุณสมบัติการแก้ไขคุณสมบัติย่อยโดยนัยของไฮเบอร์เนตซึ่งคุณต้องใช้การรวมอย่างชัดเจน
Christian Beikov

1
ในคำตอบของคุณคำจำกัดความมุมมองควรเป็นinterface MCValueDTO extends ValueDTO { @Mapping("selected.id") Set<Long> getOption(); }
ติด

0

โครงการล่าสุดของฉันใช้ GraphQL (โครงการแรกสำหรับฉัน) และเรามีปัญหาใหญ่กับการสอบถาม N + 1 และพยายามเพิ่มประสิทธิภาพการค้นหาเพื่อเข้าร่วมตารางเฉพาะเมื่อจำเป็นเท่านั้น ฉันพบว่าCosium / spring-data-jpa-entity-graphไม่สามารถถูกแทนที่ได้ มันขยายJpaRepositoryและเพิ่มวิธีการที่จะผ่านในกราฟนิติบุคคลไปยังแบบสอบถาม จากนั้นคุณสามารถสร้างกราฟเอนทิตีแบบไดนามิกที่รันไทม์เพื่อเพิ่มทางซ้ายเข้าร่วมสำหรับข้อมูลที่คุณต้องการเท่านั้น

การไหลของข้อมูลของเรามีลักษณะดังนี้:

  1. รับคำขอ GraphQL
  2. แยกวิเคราะห์ GraphQL คำขอและแปลงเป็นรายการของโหนดกราฟเอนทิตี้ในแบบสอบถาม
  3. สร้างกราฟเอนทิตี้จากโหนดที่ค้นพบและส่งผ่านไปยังที่เก็บเพื่อดำเนินการ

เพื่อแก้ปัญหาการไม่รวมโหนดที่ไม่ถูกต้องในกราฟเอนทิตี้ (ตัวอย่าง__typenameจาก graphql) ฉันได้สร้างคลาสยูทิลิตี้ที่จัดการการสร้างกราฟเอนทิตี้ คลาสที่เรียกใช้ผ่านในชื่อคลาสที่กำลังสร้างกราฟสำหรับจากนั้นตรวจสอบความถูกต้องของแต่ละโหนดในกราฟกับรูปแบบจำลองที่ดูแลโดย ORM หากโหนดไม่ได้อยู่ในรูปแบบมันจะลบออกจากรายการของโหนดกราฟ (ตรวจสอบนี้จะต้องเรียกซ้ำและตรวจสอบเด็กแต่ละคนเช่นกัน)

ก่อนที่จะค้นพบสิ่งนี้ฉันได้ลองการคาดการณ์และทางเลือกอื่น ๆ ที่แนะนำใน Spring JPA / Hibernate docs แต่ดูเหมือนว่าไม่มีสิ่งใดที่จะแก้ปัญหาได้อย่างหรูหราหรืออย่างน้อยก็มีรหัสพิเศษมากมาย


มันจะแก้ปัญหาของการโหลดการเชื่อมโยงที่ไม่เป็นที่รู้จักจากประเภทซุปเปอร์ได้อย่างไร นอกจากนี้ตามที่กล่าวกันว่าคำตอบอื่น ๆ ที่เราต้องการทราบว่ามีวิธี JPA บริสุทธิ์ แต่ผมยังคิดว่าได้รับความทุกข์ lib จากปัญหาเดียวกันว่าสมาคมจะไม่สามารถใช้ได้สำหรับทุกประเภทย่อยของselected value
ติด

หากคุณมีความสนใจใน GraphQL เรายังมีการผสานรวมของมุมมองเอนทิตีของ Blaze-Persistence กับ graphql-java: persistence.blazebit.com/documentation/1.5/entity-view/manual/…
Christian Beikov

@ChristianBeikov ขอบคุณ แต่เราใช้ SQPR เพื่อสร้างสคีมาของเราจากแบบจำลอง / วิธีการของเรา
aarbor

ถ้าคุณชอบวิธีการแบบรหัสคุณจะรักการรวม GraphQL มันจัดการดึงเฉพาะคอลัมน์ / การแสดงออกที่ใช้จริงลดการเข้าร่วม ฯลฯ โดยอัตโนมัติ
Christian Beikov

0

แก้ไขหลังจากความคิดเห็นของคุณ:

ฉันขอโทษที่ฉันไม่ได้เน้นที่คุณออกในรอบแรกปัญหาของคุณเกิดขึ้นเมื่อเริ่มต้นของฤดูใบไม้ผลิข้อมูลไม่เพียง แต่เมื่อคุณพยายามโทรหา findAll ()

ดังนั้นตอนนี้คุณสามารถเลื่อนดูตัวอย่างทั้งหมดได้จาก github ของฉัน: https://github.com/bdzzaid/stackoverflow-java/blob/master/jpa-hibernate/

คุณสามารถทำซ้ำและแก้ไขปัญหาของคุณได้อย่างง่ายดายภายในโครงการนี้

ได้อย่างมีประสิทธิภาพข้อมูล Spring และไฮเบอร์เนตจะไม่สามารถกำหนดกราฟ "เลือก" โดยค่าเริ่มต้นและคุณต้องระบุวิธีในการรวบรวมตัวเลือกที่เลือก

ดังนั้นก่อนอื่นคุณต้องประกาศ NamedEntityGraphs ของคลาสAnswer

อย่างที่คุณเห็นมีสองNamedEntityGraphสำหรับค่าคุณลักษณะของAnswer Answer

  • สิ่งแรกสำหรับค่าทั้งหมดโดยไม่มีความสัมพันธ์เฉพาะในการโหลด

  • ที่สองสำหรับค่าMultichoiceเฉพาะ หากคุณลบอันนี้ออกคุณจะทำซ้ำข้อยกเว้น

ประการที่สองคุณต้องอยู่ในบริบทของท รานแซคชัน answerRepository.findAll ()ถ้าคุณต้องการดึงข้อมูลในประเภท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;
// ..
}

ปัญหาไม่ได้รับการเรียกvalue-association ของAnswerแต่ได้รับselectedการเชื่อมโยงในกรณีที่เป็นvalue MCValueคำตอบของคุณไม่รวมถึงข้อมูลใด ๆ ที่เกี่ยวข้อง
ติด

@Stuck ขอบคุณสำหรับคำตอบของคุณคุณสามารถแบ่งปัน MCValue ของชั้นเรียนกับฉันได้ไหมฉันจะพยายามทำซ้ำปัญหาท้องถิ่นของคุณ
bdzzaid

ตัวอย่างของคุณทำงานได้เพียงเพราะคุณกำหนดไว้สมาคมOneToManyเป็น FetchType.EAGERแต่ตามที่ระบุไว้ในคำถาม: LAZYสมาคมทั้งหมดที่มี
ติด

@Stuck ฉันได้อัปเดตคำตอบของฉันตั้งแต่การอัปเดตครั้งล่าสุดของคุณหวังว่าคำตอบของฉันจะช่วยคุณในการแก้ไขปัญหาของคุณและช่วยให้คุณเข้าใจวิธีการโหลดกราฟเอนทิตี้ของ
bdzzaid

"วิธีแก้ปัญหา" ของคุณยังคงมีปัญหาจาก N + 1 ดั้งเดิมที่คำถามนี้เป็น: ใส่ส่วนแทรกและค้นหาวิธีในการทำธุรกรรมที่แตกต่างกันของการทดสอบของคุณและคุณเห็นว่า jpa จะออกแบบสอบถาม DB ให้selectedสำหรับทุกคำตอบ
ติด
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.