JPA: วิธีการมีความสัมพันธ์แบบหนึ่งต่อกลุ่มของเอนทิตีประเภทเดียวกัน


102

มีคลาสเอนทิตี "A" คลาส A อาจมีลูกประเภทเดียวกัน "A" นอกจากนี้ "A" ควรถือเป็นผู้ปกครองหากเป็นเด็ก

เป็นไปได้หรือไม่ ถ้าเป็นเช่นนั้นฉันจะแมปความสัมพันธ์ในคลาสเอนทิตีได้อย่างไร ["A" มีคอลัมน์รหัส]

คำตอบ:


173

ใช่เป็นไปได้ นี่เป็นกรณีพิเศษของมาตรฐานสองทิศทาง@ManyToOne/ @OneToManyความสัมพันธ์ เป็นเรื่องพิเศษเนื่องจากเอนทิตีในแต่ละจุดสิ้นสุดของความสัมพันธ์นั้นเหมือนกัน กรณีทั่วไปเป็นรายละเอียดในมาตรา 2.10.2 ของข้อมูลจำเพาะ JPA 2.0

นี่คือตัวอย่างที่ใช้งานได้ อันดับแรกคลาสเอนทิตีA:

@Entity
public class A implements Serializable {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;
    @ManyToOne
    private A parent;
    @OneToMany(mappedBy="parent")
    private Collection<A> children;

    // Getters, Setters, serialVersionUID, etc...
}

นี่เป็นmain()วิธีการคร่าวๆที่ยังคงมีอยู่สามเอนทิตีดังกล่าว:

public static void main(String[] args) {

    EntityManager em = ... // from EntityManagerFactory, injection, etc.

    em.getTransaction().begin();

    A parent   = new A();
    A son      = new A();
    A daughter = new A();

    son.setParent(parent);
    daughter.setParent(parent);
    parent.setChildren(Arrays.asList(son, daughter));

    em.persist(parent);
    em.persist(son);
    em.persist(daughter);

    em.getTransaction().commit();
}

ในกรณีนี้อินสแตนซ์เอนทิตีทั้งสามต้องคงอยู่ก่อนที่จะคอมมิตธุรกรรม หากฉันไม่สามารถคงอยู่หนึ่งในเอนทิตีในกราฟของความสัมพันธ์แม่ลูกได้ข้อยกเว้นจะเกิดcommit()ขึ้น ใน Eclipselink นี่คือไฟล์RollbackExceptionรายละเอียดของความไม่สอดคล้องกัน

ลักษณะการทำงานนี้สามารถกำหนดค่าได้โดยใช้cascadeแอตทริบิวต์Aของ@OneToManyและ@ManyToOneคำอธิบายประกอบ ตัวอย่างเช่นหากฉันตั้งค่าcascade=CascadeType.ALLคำอธิบายประกอบทั้งสองนั้นฉันสามารถคงอยู่ในเอนทิตีหนึ่งอย่างปลอดภัยและเพิกเฉยต่อรายการอื่น ๆ สมมติว่าฉันยังคงparentทำธุรกรรมของฉันอยู่ JPA การดำเนินลัดเลาะparent's คุณสมบัติเพราะมันจะมีเครื่องหมายchildren CascadeType.ALLการใช้งาน JPA พบsonและที่daughterนั่น จากนั้นเด็กทั้งสองยังคงอยู่ในนามของฉันแม้ว่าฉันจะไม่ได้ร้องขออย่างชัดเจนก็ตาม

อีกหนึ่งบันทึก เป็นความรับผิดชอบของโปรแกรมเมอร์เสมอในการอัปเดตความสัมพันธ์แบบสองทิศทางทั้งสองด้าน กล่าวอีกนัยหนึ่งคือเมื่อใดก็ตามที่ฉันเพิ่มเด็กให้กับผู้ปกครองบางคนฉันต้องอัปเดตคุณสมบัติหลักของเด็กตามนั้น การอัปเดตความสัมพันธ์แบบสองทิศทางเพียงด้านเดียวเป็นข้อผิดพลาดภายใต้ JPA อัปเดตความสัมพันธ์ทั้งสองฝ่ายเสมอ สิ่งนี้เขียนอย่างไม่น่าสงสัยในหน้า 42 ของข้อมูลจำเพาะ JPA 2.0:

โปรดทราบว่าเป็นแอปพลิเคชันที่รับผิดชอบในการรักษาความสอดคล้องกันของความสัมพันธ์รันไทม์ตัวอย่างเช่นเพื่อให้แน่ใจว่าด้าน "หนึ่ง" และด้าน "หลาย" ของความสัมพันธ์แบบสองทิศทางสอดคล้องกันเมื่อแอปพลิเคชันอัปเดตความสัมพันธ์ขณะรันไทม์ .


ขอบคุณมากสำหรับคำอธิบายโดยละเอียด! ตัวอย่างตรงประเด็นและทำงานในการรันครั้งแรก
sanjayav

@sunnyj ดีใจที่ได้ช่วย ขอให้โชคดีกับโครงการของคุณ
Dan LaRocque

พบปัญหานี้มาก่อนเมื่อสร้างเอนทิตีประเภทซึ่งมีหมวดหมู่ย่อย เป็นประโยชน์!
Truong Ha

@DanLaRocque บางทีฉันอาจเข้าใจผิด (หรือมีข้อผิดพลาดในการแมปเอนทิตี) แต่ฉันเห็นพฤติกรรมที่ไม่คาดคิด ฉันมีความสัมพันธ์แบบหนึ่งต่อกลุ่มระหว่างผู้ใช้และที่อยู่ เมื่อผู้ใช้ที่มีอยู่เพิ่มที่อยู่ฉันทำตามคำแนะนำของคุณเพื่ออัปเดตทั้งผู้ใช้และที่อยู่ (และเรียก 'บันทึก' ทั้งสองอย่าง) แต่สิ่งนี้ส่งผลให้มีการแทรกแถวที่ซ้ำกันในตารางที่อยู่ของฉัน นี่เป็นเพราะฉันกำหนดค่า CascadeType ของฉันผิดในช่องที่อยู่ของผู้ใช้หรือไม่?
Alex

@DanLaRocque เป็นไปได้ไหมที่จะทำลายความสัมพันธ์นี้แบบทิศทางเดียว ??
Ali Arda Orhan

8

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

@Entity
@Table(name = "DIVISION")
@EntityListeners( { HierarchyListener.class })
public class Division implements IHierarchyElement {

  private Long id;

  @Id
  @Column(name = "DIV_ID")
  public Long getId() {
        return id;
  }
  ...
  private Division parent;
  private List<Division> subDivisions = new ArrayList<Division>();
  ...
  @ManyToOne
  @JoinColumn(name = "DIV_PARENT_ID")
  public Division getParent() {
        return parent;
  }

  @ManyToMany
  @JoinTable(name = "DIVISION", joinColumns = { @JoinColumn(name = "DIV_PARENT_ID") }, inverseJoinColumns = { @JoinColumn(name = "DIV_ID") })
  public List<Division> getSubDivisions() {
        return subDivisions;
  }
...
}

เนื่องจากฉันมีตรรกะทางธุรกิจที่ครอบคลุมเกี่ยวกับโครงสร้างลำดับชั้นและ JPA (ตามโมเดลเชิงสัมพันธ์) อ่อนแอมากที่จะสนับสนุนฉันจึงแนะนำอินเทอร์เฟซIHierarchyElementและเอนทิตีฟังHierarchyListener:

public interface IHierarchyElement {

    public String getNodeId();

    public IHierarchyElement getParent();

    public Short getLevel();

    public void setLevel(Short level);

    public IHierarchyElement getTop();

    public void setTop(IHierarchyElement top);

    public String getTreePath();

    public void setTreePath(String theTreePath);
}


public class HierarchyListener {

    @PrePersist
    @PreUpdate
    public void setHierarchyAttributes(IHierarchyElement entity) {
        final IHierarchyElement parent = entity.getParent();

        // set level
        if (parent == null) {
            entity.setLevel((short) 0);
        } else {
            if (parent.getLevel() == null) {
                throw new PersistenceException("Parent entity must have level defined");
            }
            if (parent.getLevel() == Short.MAX_VALUE) {
                throw new PersistenceException("Maximum number of hierarchy levels reached - please restrict use of parent/level relationship for "
                        + entity.getClass());
            }
            entity.setLevel(Short.valueOf((short) (parent.getLevel().intValue() + 1)));
        }

        // set top
        if (parent == null) {
            entity.setTop(entity);
        } else {
            if (parent.getTop() == null) {
                throw new PersistenceException("Parent entity must have top defined");
            }
            entity.setTop(parent.getTop());
        }

        // set tree path
        try {
            if (parent != null) {
                String parentTreePath = StringUtils.isNotBlank(parent.getTreePath()) ? parent.getTreePath() : "";
                entity.setTreePath(parentTreePath + parent.getNodeId() + ".");
            } else {
                entity.setTreePath(null);
            }
        } catch (UnsupportedOperationException uoe) {
            LOGGER.warn(uoe);
        }
    }

}

1
ทำไมไม่ใช้ @OneToMany ที่ง่ายกว่า (mappedBy = "DIV_PARENT_ID") แทน @ManyToMany (... ) ด้วยแอตทริบิวต์การอ้างอิงตนเอง การตั้งชื่อตารางและคอลัมน์ซ้ำเช่นนั้นเป็นการละเมิด DRY อาจจะมีเหตุผลสำหรับสิ่งนั้น แต่ฉันไม่เห็นมัน นอกจากนี้ตัวอย่าง EntityListener ยังดูเรียบร้อย แต่ไม่สามารถพกพาได้โดยถือว่าTopเป็นความสัมพันธ์ หน้า 93 ของข้อมูลจำเพาะ JPA 2.0, Entity Listeners และ Callback Methods: "โดยทั่วไปแล้วเมธอด Lifecycle ของแอปพลิเคชันแบบพกพาไม่ควรเรียกใช้ EntityManager หรือการดำเนินการ Query เข้าถึงอินสแตนซ์ของเอนทิตีอื่นหรือแก้ไขความสัมพันธ์" ขวา? แจ้งให้เราทราบหากฉันไม่อยู่
Dan LaRocque

โซลูชันของฉันคือ 3 ปีโดยใช้ JPA 1.0 ฉันปรับเปลี่ยนโดยไม่เปลี่ยนแปลงจากรหัสการผลิต ฉันแน่ใจว่าฉันสามารถนำชื่อคอลัมน์ออกมาได้ แต่ไม่ใช่ประเด็น คำตอบของคุณตรงและง่ายกว่าไม่แน่ใจว่าทำไมฉันถึงใช้แบบหลายต่อกลุ่มในตอนนั้น - แต่มันใช้ได้ผลและฉันมั่นใจว่ามีเหตุผลที่ซับซ้อนกว่านี้ แต่ตอนนี้ฉันจะต้องกลับมาทบทวนอีกครั้ง
topchef

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