양방향 JPA OneToMany / ManyToOne 연관에서 "연결의 반대면"은 무엇입니까?


167

@OneToManyJPA 주석 참조 의 예제 섹션에서 :

예제 1-59 @OneToMany-제네릭이있는 고객 클래스

@Entity
public class Customer implements Serializable {
    ...
    @OneToMany(cascade=ALL, mappedBy="customer")
    public Set<Order> getOrders() { 
        return orders; 
    }
    ...
}

예제 1-60 @ManyToOne-제네릭이있는 주문 클래스

@Entity
public class Order implements Serializable {
    ...
    @ManyToOne
    @JoinColumn(name="CUST_ID", nullable=false)
    public Customer getCustomer() { 
        return customer; 
    }
    ...
}

Customer실체가 협회의 소유자 인 것 같습니다 . 그러나 mappedBy동일한 문서 의 속성에 대한 설명 에서 다음과 같이 작성되었습니다.

관계가 양방향 인 경우, 연관의 비 (소유하지 않은)면에서 mappingBy 요소를 예 1-60에 표시된대로 관계를 소유하는 필드 또는 특성의 이름으로 설정하십시오.

그러나 내가 틀리지 않으면 예와 같이 보입니다. mappedBy 실제로는 비 소유 측이 아닌 협회의 소유 측에 지정되어 있습니다.

그래서 내 질문은 기본적으로 :

  1. 양방향 (일대 다 / 다 대일) 연결에서 소유자 중 어느 엔터티입니까? 한면을 소유자로 어떻게 지정할 수 있습니까? 다수를 소유자로 어떻게 지정할 수 있습니까?

  2. "협회의 반대면"이란 무엇입니까? 우리는 어떻게 한쪽을 역으로 지정할 수 있습니까? 우리는 어떻게 많은면을 역수로 지정할 수 있습니까?


1
제공하신 링크가 오래되었습니다. 업데이트하십시오.
MartinL

답변:


306

이를 이해하려면 한발 물러서야합니다. OO에서 고객은 주문을 소유합니다 (주문은 고객 오브젝트의 목록입니다). 고객이 없으면 주문할 수 없습니다. 따라서 고객은 주문의 소유자 인 것 같습니다.

그러나 SQL 세계에서 한 항목에는 실제로 다른 항목에 대한 포인터가 포함됩니다. N 개의 주문에 대해 1 명의 고객이 있으므로 각 주문에는 자신이 속한 고객에 대한 외래 키가 포함됩니다. 이것은 "연결"이며 연결 (정보)을 "소유"(또는 문자 그대로 포함)하는 순서를 의미합니다. 이것은 OO / 모델 세계와 정반대입니다.

이것은 이해하는 데 도움이 될 수 있습니다.

public class Customer {
     // This field doesn't exist in the database
     // It is simulated with a SQL query
     // "OO speak": Customer owns the orders
     private List<Order> orders;
}

public class Order {
     // This field actually exists in the DB
     // In a purely OO model, we could omit it
     // "DB speak": Order contains a foreign key to customer
     private Customer customer;
}

반대쪽은 개체,이 경우 고객의 OO "소유자"입니다. 고객은 주문을 저장할 테이블에 열이 없으므로 주문 테이블에서이 데이터를 저장할 수있는 위치를 알려야합니다 (이를 통해 발생 함 mappedBy).

또 다른 일반적인 예는 부모와 자식이 될 수있는 노드가있는 나무입니다. 이 경우 두 필드는 하나의 클래스에서 사용됩니다.

public class Node {
    // Again, this is managed by Hibernate.
    // There is no matching column in the database.
    @OneToMany(cascade = CascadeType.ALL) // mappedBy is only necessary when there are two fields with the type "Node"
    private List<Node> children;

    // This field exists in the database.
    // For the OO model, it's not really necessary and in fact
    // some XML implementations omit it to save memory.
    // Of course, that limits your options to navigate the tree.
    @ManyToOne
    private Node parent;
}

이것은 "외국 키"다 대일 디자인 작업에 대해 설명합니다. 관계를 유지하기 위해 다른 테이블을 사용하는 두 번째 방법이 있습니다. 즉, 첫 번째 예의 경우 고객이있는 테이블과 주문이있는 테이블, 기본 키 쌍 (customerPK, orderPK)이있는 2 열 테이블의 세 가지 테이블이 있습니다.

이 방법은 위의 방법보다 융통성이 있습니다 (일대일, 다 대일, 일대 다 및 다대 다를 쉽게 처리 할 수 ​​있음). 가격은

  • 조금 느립니다 (다른 테이블을 유지하고 조인하면 두 테이블 대신 세 테이블을 사용합니다).
  • 조인 구문은 더 복잡합니다 (예를 들어 무언가를 디버깅하려고 할 때 많은 쿼리를 수동으로 작성해야하는 경우 지루할 수 있음)
  • 연결 테이블을 관리하는 코드에서 무언가 잘못되었을 때 갑자기 너무 많거나 너무 적은 결과를 얻을 수 있기 때문에 오류가 발생하기 쉽습니다.

그렇기 때문에이 방법을 거의 권장하지 않습니다.


36
명확히하기 위해 : 많은 편이 주인입니다. 한쪽은 반대입니다. 선택의 여지가 없습니다 (실제로 말하면).
John

11
아니, Hibernate는 이것을 발명했다. 구현의 일부를 OO 모델에 노출시키기 때문에 마음에 들지 않습니다. 내가 선호하는 것 @Parent또는@Child 어떤 연결 상태 "XtoY"대신 주석을 수단 (이 방법보다는 구현 )
아론 Digulla에게

4
@AaronDigulla는 OneToMany 매핑을 거쳐야 할 때 마다이 답변을 읽게되었습니다. 아마도 SO 주제에 가장 적합합니다.
유진

7
와. ORM 프레임 워크 문서 만 그러한 좋은 설명을했다면, 모든 것이 삼키기 쉬워 질 것입니다! 훌륭한 답변!
NickJ

2
@klausch : Hibernate 문서는 혼란 스럽다. 무시해. 코드, 데이터베이스의 SQL 및 외래 키 작동 방식을 살펴보십시오. 원한다면 지혜로운 가정을 가져갈 수 있습니다. 문서는 거짓말입니다. 출처를 사용하십시오, Luke.
아론 디 굴가

41

믿을 수 없을 정도로, 3 년 안에 아무도 당신의 관계에 대한 두 가지 방법의 모범으로 당신의 훌륭한 질문에 대답하지 못했습니다.

다른 사람들이 언급했듯이 "소유자"측은 데이터베이스의 포인터 (외부 키)를 포함합니다. 한쪽을 소유자로 지정할 수 있지만, 한쪽을 소유자로 지정하면 관계가 양방향이 아닙니다 (역의 "다수"쪽은 "소유자"에 대한 지식이 없음). 이것은 캡슐화 / 느슨한 커플 링에 바람직 할 수 있습니다.

// "One" Customer owns the associated orders by storing them in a customer_orders join table
public class Customer {
    @OneToMany(cascade = CascadeType.ALL)
    private List<Order> orders;
}

// if the Customer owns the orders using the customer_orders table,
// Order has no knowledge of its Customer
public class Order {
    // @ManyToOne annotation has no "mappedBy" attribute to link bidirectionally
}

유일한 양방향 맵핑 솔루션은 "many"측이 "one"에 대한 포인터를 소유하고 @OneToMany "mappedBy"속성을 사용하는 것입니다. "mappedBy"속성이 없으면 Hibernate는 이중 매핑을 예상 할 것이다 (데이터베이스는 조인 열과 조인 테이블을 둘 다 가질 것이다.

// "One" Customer as the inverse side of the relationship
public class Customer {
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "customer")
    private List<Order> orders;
}

// "many" orders each own their pointer to a Customer
public class Order {
    @ManyToOne
    private Customer customer;
}

2
단방향 예제에서 JPA는 추가 customer_orders 테이블이 존재할 것으로 예상합니다. JPA2를 사용하면 고객의 주문 필드에서 @JoinColumn 주석 (자주 사용하는 것 같음)을 사용하여 사용해야하는 주문 테이블의 데이터베이스 외래 키 열을 나타낼 수 있습니다. 이 방법으로 Order 테이블에 외래 키 열이 여전히 있으면서 Java에서 단방향 관계를 유지할 수 있습니다. 따라서 오브젝트 세계에서는 주문이 고객에 대해 알지 못하는 반면 데이터베이스 세계에서는 고객이 주문에 대해 알지 못합니다.
Henno Vermeulen

1
완전하게하기 위해 고객이 관계의 소유 측 인 양방향 사례를 보여줄 수 있습니다.
HDave

35

데이터베이스에 외래 키가있는 테이블이있는 엔터티는 소유 엔터티이며, 가리키고있는 다른 테이블은 역 엔터티입니다.


30
더 간단합니다 : 소유자는 FK 칼럼이있는 테이블입니다
jacktrades

2
간단하고 좋은 설명. 어느 쪽이든 소유자가 될 수 있습니다. 고객 필드 <Customer.java에서 mappingby에 의해 제거>에서 Order.java에서 mappingBy를 사용하면 Order_Customer와 같이 2 개의 열이있는 새 테이블이 작성됩니다. ORDER_ID 및 CUSTOMER_ID
HakunaMatata 오전

14

양방향 관계의 간단한 규칙 :

1. 다 대일 양방향 관계의 경우 많은 쪽이 항상 관계의 소유 측입니다. 예 : 1 방에 많은 사람이 있습니다 (사람은 한 방에만 속함)-> 소유 측은 사람입니다

2. 일대일 양방향 관계의 경우, 소유 측은 해당 외래 키를 포함하는 측에 해당합니다.

다 대다 양방향 관계의 경우, 어느 쪽이든 소유하는 쪽일 수있다.

희망이 당신을 도울 수 있습니다.


왜 주인과 역을 가져야합니까? 우리는 이미 일방 및 다방면의 의미있는 개념을 가지고 있으며 다 대다 상황에서 누가 소유자인지는 중요하지 않습니다. 결정의 결과는 무엇입니까? 데이터베이스 엔지니어로서 남은 사람이 이러한 중복 개념을 만들어 내기로 결정했다고 믿기가 어렵습니다.
Dan Cancro

3

두 개의 엔티티 클래스 Customer 및 Order의 경우 최대 절전 모드는 두 개의 테이블을 작성합니다.

가능한 경우 :

  1. mappingBy는 Customer.java 및 Order.java 클래스에서 사용되지 않습니다.

    고객 측에서 CUSTOMER_ID와 ORDER_ID의 매핑을 유지하는 새 테이블이 생성됩니다 [이름 = CUSTOMER_ORDER]. 이들은 고객 및 주문 테이블의 기본 키입니다. 주문 측에서 해당 Customer_ID 레코드 매핑을 저장하려면 추가 열이 필요합니다.

  2. mappingBy는 Customer.java에서 사용됩니다. [문제 설명에 제공된대로] 이제 추가 테이블 [CUSTOMER_ORDER]이 작성되지 않습니다. 주문 테이블에서 하나의 열만

  3. mappingby가 Order.java에서 사용됩니다. 이제 추가 테이블이 최대 절전 모드로 작성됩니다. [name = CUSTOMER_ORDER] 주문 테이블에는 맵핑을위한 추가 열 [Customer_ID]가 없습니다.

어느 쪽이든 관계의 소유자가 될 수 있습니다. 그러나 xxxToOne 쪽을 선택하는 것이 좋습니다.

코딩 효과-> 엔티티의 소유 측만 관계 ​​상태를 변경할 수 있습니다. 아래 예제에서 BoyFriend 클래스는 관계의 소유자입니다. 여자 친구가 헤어지기를 원하더라도 할 수 없습니다.

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "BoyFriend21")
public class BoyFriend21 {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "Boy_ID")
    @SequenceGenerator(name = "Boy_ID", sequenceName = "Boy_ID_SEQUENCER", initialValue = 10,allocationSize = 1)
    private Integer id;

    @Column(name = "BOY_NAME")
    private String name;

    @OneToOne(cascade = { CascadeType.ALL })
    private GirlFriend21 girlFriend;

    public BoyFriend21(String name) {
        this.name = name;
    }

    public BoyFriend21() {
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public BoyFriend21(String name, GirlFriend21 girlFriend) {
        this.name = name;
        this.girlFriend = girlFriend;
    }

    public GirlFriend21 getGirlFriend() {
        return girlFriend;
    }

    public void setGirlFriend(GirlFriend21 girlFriend) {
        this.girlFriend = girlFriend;
    }
}

import org.hibernate.annotations.*;
import javax.persistence.*;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.Table;
import java.util.ArrayList;
import java.util.List;

@Entity 
@Table(name = "GirlFriend21")
public class GirlFriend21 {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "Girl_ID")
    @SequenceGenerator(name = "Girl_ID", sequenceName = "Girl_ID_SEQUENCER", initialValue = 10,allocationSize = 1)
    private Integer id;

    @Column(name = "GIRL_NAME")
    private String name;

    @OneToOne(cascade = {CascadeType.ALL},mappedBy = "girlFriend")
    private BoyFriend21 boyFriends = new BoyFriend21();

    public GirlFriend21() {
    }

    public GirlFriend21(String name) {
        this.name = name;
    }


    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public GirlFriend21(String name, BoyFriend21 boyFriends) {
        this.name = name;
        this.boyFriends = boyFriends;
    }

    public BoyFriend21 getBoyFriends() {
        return boyFriends;
    }

    public void setBoyFriends(BoyFriend21 boyFriends) {
        this.boyFriends = boyFriends;
    }
}


import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import java.util.Arrays;

public class Main578_DS {

    public static void main(String[] args) {
        final Configuration configuration = new Configuration();
         try {
             configuration.configure("hibernate.cfg.xml");
         } catch (HibernateException e) {
             throw new RuntimeException(e);
         }
        final SessionFactory sessionFactory = configuration.buildSessionFactory();
        final Session session = sessionFactory.openSession();
        session.beginTransaction();

        final BoyFriend21 clinton = new BoyFriend21("Bill Clinton");
        final GirlFriend21 monica = new GirlFriend21("monica lewinsky");

        clinton.setGirlFriend(monica);
        session.save(clinton);

        session.getTransaction().commit();
        session.close();
    }
}

import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import java.util.List;

public class Main578_Modify {

    public static void main(String[] args) {
        final Configuration configuration = new Configuration();
        try {
            configuration.configure("hibernate.cfg.xml");
        } catch (HibernateException e) {
            throw new RuntimeException(e);
        }
        final SessionFactory sessionFactory = configuration.buildSessionFactory();
        final Session session1 = sessionFactory.openSession();
        session1.beginTransaction();

        GirlFriend21 monica = (GirlFriend21)session1.load(GirlFriend21.class,10);  // Monica lewinsky record has id  10.
        BoyFriend21 boyfriend = monica.getBoyFriends();
        System.out.println(boyfriend.getName()); // It will print  Clinton Name
        monica.setBoyFriends(null); // It will not impact relationship

        session1.getTransaction().commit();
        session1.close();

        final Session session2 = sessionFactory.openSession();
        session2.beginTransaction();

        BoyFriend21 clinton = (BoyFriend21)session2.load(BoyFriend21.class,10);  // Bill clinton record

        GirlFriend21 girlfriend = clinton.getGirlFriend();
        System.out.println(girlfriend.getName()); // It will print Monica name.
        //But if Clinton[Who owns the relationship as per "mappedby" rule can break this]
        clinton.setGirlFriend(null);
        // Now if Monica tries to check BoyFriend Details, she will find Clinton is no more her boyFriend
        session2.getTransaction().commit();
        session2.close();

        final Session session3 = sessionFactory.openSession();
        session1.beginTransaction();

        monica = (GirlFriend21)session3.load(GirlFriend21.class,10);  // Monica lewinsky record has id  10.
        boyfriend = monica.getBoyFriends();

        System.out.println(boyfriend.getName()); // Does not print Clinton Name

        session3.getTransaction().commit();
        session3.close();
    }
}

1

테이블 관계와 엔티티 관계

관계형 데이터베이스 시스템에서는 세 가지 유형의 테이블 관계 만있을 수 있습니다.

  • 일대 다 (외래 키 열을 통해)
  • 일대일 (공유 기본 키를 통해)
  • 다 대다 (2 개의 개별 부모 테이블을 참조하는 2 개의 외래 키가있는 링크 테이블을 통해)

따라서 one-to-many테이블 관계 는 다음과 같습니다.

<code> 일대 다 </ code> 테이블 관계

관계는 post_id자식 테이블 의 외래 키 열 (예 :)을 기반으로 합니다.

따라서 one-to-many테이블 관계 를 관리 할 때 진실의 원천이 있습니다.

이제 one-to-many이전에 보았던 테이블 관계 에 맵핑되는 양방향 엔티티 관계를 사용하는 경우 :

양방향 <code> 일대 다 </ code> 엔티티 연결

위의 다이어그램을 보면이 관계를 관리하는 두 가지 방법이 있음을 알 수 있습니다.

에서 Post엔티티, 당신은이 comments모음 :

@OneToMany(
    mappedBy = "post",
    cascade = CascadeType.ALL,
    orphanRemoval = true
)
private List<PostComment> comments = new ArrayList<>();

그리고 PostComment에서 post연결은 다음과 같이 매핑됩니다.

@ManyToOne(
    fetch = FetchType.LAZY
)
@JoinColumn(name = "post_id")
private Post post;

따라서 엔티티 연관을 변경할 수있는 두 가지 측면이 있습니다.

  • comments하위 컬렉션에 항목을 추가하면 새 post_comment행이 하위 post항목을 통해 상위 항목 과 연결되어야합니다.post_id .
  • 엔터티 의 post속성을 설정하여 열도 업데이트해야합니다.PostCommentpost_id

외래 키 열을 나타내는 두 가지 방법이 있기 때문에 연결 상태 변경을 해당 외래 키 열 값 수정으로 변환 할 때 진실의 원인을 정의해야합니다.

MappedBy (일명 반대쪽)

mappedBy특성은 @ManyToOne측면이 외래 키 열 관리를 담당하고 컬렉션은 하위 엔터티를 가져오고 상위 엔터티 상태 변경을 하위 항목으로 계단식으로 만드는 데만 사용됩니다 (예 : 상위를 제거하면 하위 엔터티도 제거해야 함).

이 테이블 관계를 관리하는 자식 엔터티 속성을 참조하기 때문에 역측 이라고합니다 .

양방향 연관의 양쪽을 동기화

이제 mappedBy속성 을 정의하고 하위 측 @ManyToOne연결이 외래 키 열을 관리하더라도 양방향 연결의 양쪽을 동기화해야합니다.

이를 수행하는 가장 좋은 방법은 다음 두 가지 유틸리티 방법을 추가하는 것입니다.

public void addComment(PostComment comment) {
    comments.add(comment);
    comment.setPost(this);
}

public void removeComment(PostComment comment) {
    comments.remove(comment);
    comment.setPost(null);
}

addCommentremoveComment방법은 양측이 동기화되어 있는지 확인합니다. 따라서 자식 엔터티를 추가하면 자식 엔터티가 부모를 가리켜 야하며 부모 엔터티는 자식 컬렉션에 자식이 포함되어 있어야합니다.

모든 양방향 엔티티 연관 유형을 동기화하는 가장 좋은 방법에 대한 자세한 내용은 이 기사를 확인 하십시오 .

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.