JPA : 동일한 엔티티 유형의 일대 다 관계를 갖는 방법


99

엔티티 클래스 "A"가 있습니다. 클래스 A에는 동일한 유형 "A"의 하위가있을 수 있습니다. 또한 "A"는 자녀 인 경우 부모를 보유해야합니다.

이게 가능해? 그렇다면 Entity 클래스에서 관계를 어떻게 매핑해야합니까? [ "A"에는 id 열이 있습니다.]

답변:


170

네, 가능합니다. 이것은 표준 양방향 @ManyToOne/ @OneToMany관계 의 특별한 경우입니다 . 관계의 각 끝에있는 엔티티가 동일하기 때문에 특별합니다. 일반적인 경우는 JPA 2.0 사양 의 섹션 2.10.2에 자세히 설명되어 있습니다.

다음은 작동하는 예입니다. 첫째, 엔티티 클래스 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속성을 통해 구성 할 수 있습니다. 예를 들어 두 주석을 모두 설정 하면 엔티티 중 하나를 안전하게 유지하고 나머지는 무시할 수 있습니다. 거래를 지속했다고 가정 해 보겠습니다 . JPA 구현 은로 표시되어 있으므로 의 속성을 탐색 합니다. JPA를 구현 발견 하고 있다. 그런 다음 내가 명시 적으로 요청하지 않았더라도 나를 대신하여 두 자녀를 모두 유지합니다.A@OneToMany@ManyToOnecascade=CascadeType.ALLparentparentchildrenCascadeType.ALLsondaughter

메모 하나 더. 양방향 관계의 양쪽을 업데이트하는 것은 항상 프로그래머의 책임입니다. 즉, 어떤 부모에게 자식을 추가 할 때마다 그에 따라 자식의 부모 속성을 업데이트해야합니다. 양방향 관계의 한쪽 만 업데이트하는 것은 JPA에서 오류입니다. 항상 관계의 양쪽을 모두 업데이트하십시오. 이것은 JPA 2.0 사양의 42 페이지에 명확하게 작성되었습니다.

예를 들어, 애플리케이션이 런타임에 관계를 업데이트 할 때 양방향 관계의 "일"측면과 "다"측면이 서로 일치하도록 보장하기 위해 런타임 관계의 일관성을 유지하는 책임은 애플리케이션입니다. .


자세한 설명 감사합니다! 예는 요점이며 첫 번째 실행에서 작동했습니다.
sanjayav

@sunnyj 기꺼이 도와주세요. 프로젝트에 행운을 빕니다.
Dan LaRocque

하위 카테고리가있는 카테고리 엔티티를 생성 할 때 이전에이 문제가 해결되었습니다. 도움이됩니다!
Truong Ha

@DanLaRocque 아마도 내가 오해하고 있거나 (또는 ​​엔티티 매핑 오류가 있음) 예기치 않은 동작이 나타납니다. 사용자와 주소간에 일대 다 관계가 있습니다. 기존 사용자가 주소를 추가 할 때 사용자와 주소를 모두 업데이트하라는 제안을 따랐습니다 (둘 다 '저장'을 호출). 그러나 이로 인해 내 주소 테이블에 중복 행이 삽입되었습니다. 사용자 주소 필드에서 CascadeType을 잘못 구성했기 때문입니까?
Alex

@ DanLaRocque이 관계를 단방향으로 정의 할 수 있습니까 ??
Ali Arda Orhan 2015

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
자체 참조 속성이있는 @ManyToMany (...) 대신 더 간단한 @OneToMany (mappedBy = "DIV_PARENT_ID")를 사용하지 않는 이유는 무엇입니까? 이와 같이 테이블 및 열 이름을 다시 입력하면 DRY를 위반합니다. 이유가 있을지 모르지만 모르겠습니다. 또한 EntityListener 예제는 Top관계 라고 가정하면 깔끔하지만 이식성 이 없습니다. JPA 2.0 사양, 엔티티 리스너 및 콜백 메소드의 93 페이지 : "일반적으로 이식 가능한 애플리케이션의 라이프 사이클 메소드는 EntityManager 또는 쿼리 작업을 호출하거나 다른 엔티티 인스턴스에 액세스하거나 관계를 수정해서는 안됩니다." 권리? 내가 떠나면 알려주세요.
Dan LaRocque

내 솔루션은 JPA 1.0을 사용하는 3 년 전입니다. 프로덕션 코드에서 변경하지 않고 적용했습니다. 일부 열 이름을 제거 할 수 있다고 확신하지만 요점이 아닙니다. 귀하의 답변은 정확하고 간단합니다. 그 당시 다대 다를 사용한 이유는 확실하지 않지만 작동하며 더 복잡한 솔루션이 이유가 있다고 확신합니다. 하지만 지금 다시 방문해야합니다.
topchef

예, top은 자기 참조이므로 관계입니다. 엄밀히 말하면 수정하지 않고 초기화 만하면됩니다. 또한 단방향이므로 반대로 의존성이 없으며 self 이외의 다른 엔티티를 참조하지 않습니다. 견적 사양에 따라 "일반적으로"가 있으며 이는 엄격한 정의가 아님을 의미합니다. 이 경우 이식성 위험이 매우 낮다고 생각합니다.
topchef
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.