ฉันจะทำให้ความสัมพันธ์ JPA OneToOne ขี้เกียจได้อย่างไร


212

ในแอปพลิเคชันนี้เรากำลังพัฒนาเราสังเกตว่ามุมมองนั้นช้าเป็นพิเศษ ฉันทำมุมมองและสังเกตว่ามีหนึ่งแบบสอบถามดำเนินการโดยจำศีลซึ่งใช้เวลา 10 วินาทีแม้ว่าจะมีเพียงสองวัตถุในฐานข้อมูลเพื่อดึงข้อมูล ทั้งหมดOneToManyและManyToManyความสัมพันธ์ที่มีความขี้เกียจเพื่อให้เป็นไม่ได้เป็นปัญหา เมื่อตรวจสอบ SQL ที่เกิดขึ้นจริงฉันสังเกตว่ามีการรวมมากกว่า 80 รายการในแบบสอบถาม

การตรวจสอบเพิ่มเติมปัญหาฉันสังเกตเห็นว่าปัญหาเกิดจากลำดับชั้นลึกOneToOneและManyToOneความสัมพันธ์ระหว่างคลาสเอนทิตี้ ดังนั้นฉันคิดว่าฉันจะทำให้พวกเขาขี้เกียจที่จะแก้ปัญหา แต่คำอธิบายประกอบอย่างใดอย่างหนึ่ง@OneToOne(fetch=FetchType.LAZY)หรือ@ManyToOne(fetch=FetchType.LAZY)ดูเหมือนจะไม่ทำงาน ทั้งฉันได้รับข้อยกเว้นหรือไม่พวกเขาจะไม่ถูกแทนที่ด้วยวัตถุพร็อกซีและทำให้ขี้เกียจ

ความคิดใดที่ฉันจะทำให้มันทำงานได้? โปรดทราบว่าฉันไม่ได้ใช้persistence.xmlเพื่อกำหนดความสัมพันธ์หรือรายละเอียดการกำหนดค่าทุกอย่างจะทำในรหัสจาวา

คำตอบ:


218

ก่อนอื่นขอให้ชี้แจงคำตอบของKLE :

  1. การเชื่อมโยงแบบหนึ่งต่อหนึ่งแบบไม่ จำกัด (เป็นโมฆะ) เป็นสิ่งเดียวที่ไม่สามารถใช้พร็อกซีได้ เหตุผลนี้คือว่าเจ้าของกิจการต้องทราบว่าคุณสมบัติการเชื่อมโยงควรมีวัตถุพร็อกซีหรือ NULL และไม่สามารถระบุได้ว่าโดยการดูที่คอลัมน์ของตารางฐานเนื่องจากปกติหนึ่งต่อหนึ่งถูกแมปผ่าน PK ที่ใช้ร่วมกันดังนั้น จะต้องมีการดึงข้อมูลอย่างกระตือรือร้นทำให้พร็อกซีไม่มีจุดหมาย นี่คือคำอธิบายโดยละเอียดเพิ่มเติม

  2. การเชื่อมโยงแบบหลายต่อหนึ่ง (และแบบหนึ่งต่อหลาย) ไม่ประสบปัญหานี้ เจ้าของกิจการสามารถตรวจสอบ FK ของตัวเองได้อย่างง่ายดาย (และในกรณีที่มีการสร้างพร็อกซีคอลเลกชันที่ว่างเปล่าแบบหนึ่งต่อหลายคนจะถูกสร้างขึ้นครั้งแรกและเติมตามความต้องการ) ดังนั้นการเชื่อมโยงจึงขี้เกียจ

  3. การแทนที่แบบหนึ่งต่อหนึ่งด้วยแบบหนึ่งต่อหลายคนค่อนข้างจะไม่เป็นความคิดที่ดี คุณสามารถแทนที่ด้วยตัวเลือกแบบตัวต่อตัว แต่มีตัวเลือกอื่น ๆ (อาจดีกว่า)

Rob H.มีจุดที่ถูกต้อง แต่คุณอาจไม่สามารถใช้งานได้ขึ้นอยู่กับรุ่นของคุณ (เช่นถ้าการเชื่อมต่อแบบหนึ่งต่อหนึ่งของคุณเป็นโมฆะ)

ตอนนี้เท่าที่คำถามเดิมไป:

A) @ManyToOne(fetch=FetchType.LAZY)ควรใช้งานได้ดี คุณแน่ใจหรือไม่ว่าไม่มีการเขียนทับลงในแบบสอบถาม เป็นไปได้ที่จะระบุjoin fetchใน HQL และ / หรือตั้งค่าโหมดดึงข้อมูลอย่างชัดเจนผ่าน Criteria API ซึ่งจะมีความสำคัญเหนือกว่าคำอธิบายประกอบในชั้นเรียน หากไม่ใช่ในกรณีนี้และคุณยังคงมีปัญหาอยู่โปรดโพสต์คลาสการสืบค้นและผลลัพธ์ SQL สำหรับการสนทนาที่ตรงประเด็นมากขึ้น

B) @OneToOneมีเล่ห์เหลี่ยม ถ้ามันไม่เป็นโมฆะแน่นอนไปกับข้อเสนอแนะของ Rob H. และระบุว่าเป็นเช่นนี้:

@OneToOne(optional = false, fetch = FetchType.LAZY)

มิฉะนั้นถ้าคุณสามารถเปลี่ยนฐานข้อมูลของคุณ (เพิ่มคอลัมน์คีย์ต่างประเทศลงในตารางเจ้าของ) ให้ทำและแมปเป็น "เข้าร่วม":

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name="other_entity_fk")
public OtherEntity getOther()

และใน OtherEntity:

@OneToOne(mappedBy = "other")
public OwnerEntity getOwner()

หากคุณไม่สามารถทำเช่นนั้น (และไม่สามารถอยู่กับการดึงข้อมูลด้วยความกระตือรือร้น) เครื่องมือวัด bytecode เป็นทางเลือกเดียว ฉันต้องเห็นด้วยกับCPerkinsอย่างไรก็ตาม - ถ้าคุณมี80 !!! เข้าร่วมเนื่องจากความสนใจ OneToOne สมาคมคุณมีปัญหามากขึ้นแล้ว :-) นี้


บางทีนั่นอาจจะเป็นตัวเลือกอื่น แต่ฉันไม่ได้ทดสอบเองมันในด้านข้อ จำกัด ที่ไม่ใช้สูตรเหมือนone-to-one select other_entity.id from other_entity where id = other_entity.idแน่นอนว่ามันไม่เหมาะสำหรับการแสดงคำค้นหา
Frédéric

1
ทางเลือก = false ไม่ทำงานสำหรับฉัน @OneToOne (fetch = FetchType.LAZY, mappedBy = "fundSeries", optional = false) ส่วนตัว FundSeriesDetailEntity fundSeriesDetail;
Oleg Kuts

21

เพื่อให้การโหลดแบบขี้เกียจทำงานบนการแม็พแบบหนึ่งต่อหนึ่งที่ไม่มีค่าคุณต้องให้ไฮเบอร์เนตทำการคอมไพล์เวลาและเพิ่ม@LazyToOne(value = LazyToOneOption.NO_PROXY)ความสัมพันธ์แบบหนึ่งต่อหนึ่ง

ตัวอย่างการแม็พ:

@OneToOne(fetch = FetchType.LAZY)  
@JoinColumn(name="other_entity_fk")
@LazyToOne(value = LazyToOneOption.NO_PROXY)
public OtherEntity getOther()

ตัวอย่างนามสกุลไฟล์ Ant Build (สำหรับการทำเครื่องมือการคอมไพล์เวลาไฮเบอร์เนต):

<property name="src" value="/your/src/directory"/><!-- path of the source files --> 
<property name="libs" value="/your/libs/directory"/><!-- path of your libraries --> 
<property name="destination" value="/your/build/directory"/><!-- path of your build directory --> 

<fileset id="applibs" dir="${libs}"> 
  <include name="hibernate3.jar" /> 
  <!-- include any other libraries you'll need here --> 
</fileset> 

<target name="compile"> 
  <javac srcdir="${src}" destdir="${destination}" debug="yes"> 
    <classpath> 
      <fileset refid="applibs"/> 
    </classpath> 
  </javac> 
</target> 

<target name="instrument" depends="compile"> 
  <taskdef name="instrument" classname="org.hibernate.tool.instrument.javassist.InstrumentTask"> 
    <classpath> 
      <fileset refid="applibs"/> 
    </classpath> 
  </taskdef> 

  <instrument verbose="true"> 
    <fileset dir="${destination}"> 
      <!-- substitute the package where you keep your domain objs --> 
      <include name="/com/mycompany/domainobjects/*.class"/> 
    </fileset> 
  </instrument> 
</target>

3
ทำไมLazyToOneOption.NO_PROXYไม่LazyToOneOption.PROXY?
Telmo Marques

สิ่งนี้ไม่ได้ตอบว่า "ทำไม" แต่ความจริงข้อนี้ถูกยืนยันที่นี่เช่นกัน (ในตอนท้ายของส่วน "การทำแผนที่ทั่วไป"): vladmihalcea.com/…
DanielM

12

แนวคิดพื้นฐานที่ทำหน้าที่ XToOnes ใน Hibernate คือพวกเขาไม่ขี้เกียจในกรณีส่วนใหญ่

เหตุผลหนึ่งคือเมื่อไฮเบอร์เนตต้องตัดสินใจใส่พร็อกซี (ที่มี id) หรือเป็นโมฆะ
มันต้องดูในตารางอื่นเพื่อเข้าร่วม ค่าใช้จ่ายในการเข้าถึงตารางอื่น ๆ ในฐานข้อมูลมีความสำคัญดังนั้นจึงอาจดึงข้อมูลสำหรับตารางนั้นในขณะนั้น (พฤติกรรมที่ไม่ขี้เกียจ) แทนที่จะดึงข้อมูลนั้นในการร้องขอในภายหลังซึ่งต้องการการเข้าถึงที่สองไปยัง ตารางเดียวกัน

แก้ไข: สำหรับรายละเอียดโปรดดูที่คำตอบ อันนี้มีความแม่นยำและรายละเอียดน้อยกว่าไม่มีอะไรให้ ขอบคุณ ChssPly76


มีหลายสิ่งที่ไม่ถูกต้องที่นี่ - ฉันได้ให้คำตอบอื่นด้านล่างด้วยคำอธิบาย (สิ่งที่มากเกินไปจะไม่พอดีในความคิดเห็น)
ChssPly76

8

นี่คือสิ่งที่ทำงานให้ฉัน (โดยไม่ต้องใช้เครื่องมือ):

แทนที่จะใช้@OneToOneทั้งสองด้านฉันใช้@OneToManyในส่วนผกผันของความสัมพันธ์ (ที่มีmappedBy) นั่นทำให้ทรัพย์สินเป็นชุด ( Listในตัวอย่างด้านล่าง) แต่ฉันแปลมันเป็นรายการในทะเยอทะยานทำให้มันโปร่งใสสำหรับลูกค้า

การตั้งค่านี้ใช้งานได้อย่างเกียจคร้านนั่นคือการเลือกจะทำเฉพาะเมื่อมีการโทรgetPrevious()หรือgetNext()- และเลือกได้เพียงครั้งเดียวสำหรับการโทรแต่ละครั้ง

โครงสร้างตาราง:

CREATE TABLE `TB_ISSUE` (
    `ID`            INT(9) NOT NULL AUTO_INCREMENT,
    `NAME`          VARCHAR(255) NULL,
    `PREVIOUS`      DECIMAL(9,2) NULL
    CONSTRAINT `PK_ISSUE` PRIMARY KEY (`ID`)
);
ALTER TABLE `TB_ISSUE` ADD CONSTRAINT `FK_ISSUE_ISSUE_PREVIOUS`
                 FOREIGN KEY (`PREVIOUS`) REFERENCES `TB_ISSUE` (`ID`);

ห้องเรียน:

@Entity
@Table(name = "TB_ISSUE") 
public class Issue {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected Integer id;

    @Column
    private String name;

    @OneToOne(fetch=FetchType.LAZY)  // one to one, as expected
    @JoinColumn(name="previous")
    private Issue previous;

    // use @OneToMany instead of @OneToOne to "fake" the lazy loading
    @OneToMany(mappedBy="previous", fetch=FetchType.LAZY)
    // notice the type isnt Issue, but a collection (that will have 0 or 1 items)
    private List<Issue> next;

    public Integer getId() { return id; }
    public String getName() { return name; }

    public Issue getPrevious() { return previous; }
    // in the getter, transform the collection into an Issue for the clients
    public Issue getNext() { return next.isEmpty() ? null : next.get(0); }

}

7

ดังที่ฉันได้อธิบายไว้ในบทความนี้ยกเว้นว่าคุณใช้Bytecode Enhancementคุณจะไม่สามารถดึงความสัมพันธ์ระหว่างผู้ปกครองกับผู้@OneToOneอื่น

อย่างไรก็ตามบ่อยครั้งที่คุณไม่จำเป็นต้องมีการเชื่อมโยงด้านผู้ปกครองหากคุณใช้@MapsIdในฝั่งไคลเอ็นต์:

@Entity(name = "PostDetails")
@Table(name = "post_details")
public class PostDetails {

    @Id
    private Long id;

    @Column(name = "created_on")
    private Date createdOn;

    @Column(name = "created_by")
    private String createdBy;

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId
    private Post post;

    public PostDetails() {}

    public PostDetails(String createdBy) {
        createdOn = new Date();
        this.createdBy = createdBy;
    }

    //Getters and setters omitted for brevity
}

ด้วยความ@MapsIdที่idคุณสมบัติในตารางเด็กทำหน้าที่เป็นทั้งคีย์หลักและต่างประเทศที่สำคัญที่จะตารางแม่คีย์หลัก

ดังนั้นหากคุณมีการอ้างอิงไปยังPostเอนทิตีหลักคุณสามารถดึงเอนทิตีลูกได้อย่างง่ายดายโดยใช้ตัวระบุเอนทิตีหลัก:

PostDetails details = entityManager.find(
    PostDetails.class,
    post.getId()
);

ด้วยวิธีนี้คุณจะไม่พบปัญหาการสอบถาม N + 1ที่อาจเกิดจากการmappedBy @OneToOneเชื่อมโยงทางด้านหลัก


ด้วยวิธีนี้เราไม่สามารถลดการดำเนินการจากพ่อแม่สู่ลูกอีกต่อไป: /
Hamdi

สำหรับการคงอยู่มันเป็นเพียงการโทรคงอยู่เป็นพิเศษสำหรับการลบคุณสามารถใช้ DDL แบบเรียงซ้อน
Vlad Mihalcea

6

ในการแม็พ Hibernate XML ดั้งเดิมคุณสามารถทำสิ่งนี้ได้ด้วยการประกาศการแมปแบบหนึ่งต่อหนึ่งด้วยแอตทริบิวต์ที่ถูกจำกัด ที่ตั้งค่าเป็นจริง ฉันไม่แน่ใจว่าคำอธิบายประกอบแบบไฮเบอร์เนต / JPA นั้นคืออะไรและการค้นหาเอกสารอย่างรวดเร็วนั้นไม่มีคำตอบ แต่หวังว่าคุณจะเป็นผู้นำในการดำเนินการต่อไป


5
+1 สำหรับคำแนะนำที่ดี โชคไม่ดีที่มันไม่สามารถใช้ได้เสมอไปเพราะโมเดลโดเมนอาจต้องการความไร้ค่า วิธีที่ถูกต้องในการทำแผนที่สิ่งนี้ผ่านหมายเหตุประกอบคือ@OneToOne(optional=false,fetch=FetchMode.LAZY)
ChssPly76

ฉันลองและไม่เห็นการปรับปรุงประสิทธิภาพ ฉันยังเห็นข้อความค้นหาจำนวนมากในผลลัพธ์การจำศีลผ่านดีบักเกอร์
P.Brian.Mackey

3

ตามที่อธิบายไว้แล้วอย่างสมบูรณ์โดย ChssPly76 ผู้รับมอบฉันทะของ Hibernate ไม่ได้ช่วยในการเชื่อมโยงแบบหนึ่งต่อหนึ่งที่ไม่ จำกัด (ไม่มีค่า) แต่มีคำอธิบายที่นี่เพื่อหลีกเลี่ยงการตั้งค่าเครื่องมือ ความคิดคือการหลอกไฮเบอร์เนตว่าคลาสเอนทิตีที่เราต้องการใช้นั้นได้รับการใช้งานแล้ว: คุณใช้มันด้วยตนเองในซอร์สโค้ด มันเป็นเรื่องง่าย! ฉันใช้งานกับ CGLib ในฐานะผู้ให้บริการ bytecode และใช้งานได้ (ให้แน่ใจว่าคุณกำหนดค่า lazy = "no-proxy" และ fetch = "select" ไม่ใช่ "เข้าร่วม" ใน HBM ของคุณ)

ฉันคิดว่านี่เป็นทางเลือกที่ดีในการวัดจริง (ฉันหมายถึงอัตโนมัติ) เมื่อคุณมีความสัมพันธ์แบบ null - to - one เพียงหนึ่งต่อหนึ่งที่คุณต้องการทำให้ขี้เกียจ ข้อเสียเปรียบหลักคือโซลูชันขึ้นอยู่กับผู้ให้บริการ bytecode ที่คุณใช้ดังนั้นให้แสดงความคิดเห็นในชั้นเรียนของคุณอย่างถูกต้องเพราะคุณอาจต้องเปลี่ยนผู้ให้บริการ bytecode ในอนาคต แน่นอนคุณกำลังแก้ไขโมเดล bean ของคุณด้วยเหตุผลทางเทคนิคและนี่ก็ไม่เป็นไร


1

คำถามนี้ค่อนข้างเก่า แต่ด้วย Hibernate 5.1.10 มีวิธีแก้ปัญหาที่สะดวกสบายกว่า

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

  • คุณสามารถเปิดใช้งานการเพิ่มประสิทธิภาพการโหลด bytecode ขี้เกียจ
  • หรือคุณสามารถลบด้านผู้ปกครองและใช้ฝั่งไคลเอ็นต์ด้วย @MapsId ตามที่อธิบายไว้ในบทความด้านบน ด้วยวิธีนี้คุณจะพบว่าคุณไม่จำเป็นต้องใช้ด้านผู้ปกครองตั้งแต่เด็กแชร์รหัสเดียวกันกับผู้ปกครองเพื่อให้คุณสามารถดึงข้อมูลเด็ก ๆ ได้อย่างง่ายดายด้วยการรู้รหัสผู้ปกครอง

0

หากความสัมพันธ์ต้องไม่เป็นแบบสองทิศทางดังนั้น @ElementCollection อาจจะง่ายกว่าการใช้คอลเลกชัน One2Many แบบสันหลังยาว

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