17. 다양한 연관관계 매핑(3)

17. 다양한 연관관계 매핑(3)

4. 다대다[N:M]

관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다. 그래서 보통 다대다 관계를 일대다, 다대일 관계로 풀어내는 연결 테이블을 사용한다. 아래 그림을 보자. 예를 들어 회원들은 상품을 주문한다. 반대로 상품들은 회원들에 의해 주문된다. 둘의 다대다 관계다. 따라서 회원 테이블과 상품 테이블만으로는 이 관계를 표현할 수 없다.

그래서 위의 그림처럼 중간에 연결 테이블을 추가해야 한다. 이 그림을 보면 Member_Product 연결 테이블을 추가했다. 이 테이블을 사용해서 다대다 관계를 일대다, 다대일 관계로 풀어낼 수 있다. 이 연결 테이블은 회원이 주문한 상품을 나타낸다.

그런데 객체는 테이블과 다르게 객체 2개로 다대다 관계를 만들 수 있다. 예를 들어 회원 객체는 컬렉션을 사용해서 상품들을 참조하면 되고 반대로 상품들도 컬렉션을 사용해서 회원들을 참조하면 된다.

@ManyToMany를 사용하면 아래 그림처럼 이런 다대다 관계를 편리하게 매핑할 수 있다.

1. 다대다 : 단방향

다대다 단방향 관계인 회원과 상품 엔티티를 보자.

@Entity
public class Member {

    @Id @Column(name = "MEMBER_ID")
    private String id;
    
    private String username;
    
    @ManyToMany
    @JoinTable(name = "MEMBER_PRODUCT",
    joinColumns = @JoinColumn(name = "MEMBER_ID"),
    inverseJoinColumns = @JoinColumn(name = "PRODUCT_ID"))
    private List<Product> products = new ArrayList<Product>();
    ...
}
@Entity
public class Product {

    @Id @Column(name = "PRODUCT_ID")
    private String id;
    
    private String name;
    ...
}

회원 엔티티와 상품 엔티티를 @ManyToMany로 매핑했다. 여기서 중요한 점은 @ManyToMany와 @JoinTable을 사용해서 연결 테이블로 바로 매팡한 것이다. 따라서 회원과 상품을 연결하는 회원_상품(Member_Product) 엔티티 없이 매핑을 완료할 수 있다.

연결 테이블을 매핑하는 @JoinTable의 속성을 정리해보자.

  • @JoinTable.name : 연결 테이블을 지정했다. 여기서는 MEMBER_PRODUCT 테이블을 선택했다.
  • @JoinTable.joinColumns : 현재 방향은 회원과 매핑할 조인 컬럼 정보를 지정한다. MEMBER_ID로 지정했다.
  • @JoinTable.inverseJoinColumns : 반대 방향은 상품과 매핑할 조인 컬럼 정보를 지정한다. PRODUCT_ID로 지정했다.

MEMBER_PRODUCT 테이블은 다대다 관계를 일대다, 다대일 관계로 풀어내기 위해 필요한 연결 테이블일
뿐이다. @ManyToMany로 매핑한 덕분에 다대다 관계를 사용할 때는 이 연결 테이블을 신경 쓰지 않아도 된다.

다음으로 다대다 관계를 저장하는 예제를 보자.

public void save() {

    Product productA = new Product();
    productA.setId("productA");
    productA.setName("상품A");
    em.persist(productA);
    
    Member member1 = new Member();
    member1.setId("member1");
    member1.setUsername("회원1");
    member1.getProducts(0.add(productA)  // 연관관계 설정
    em.persist(member1);
    
}

회원1과 상품A의 연관관계를 설정했으므로 회원1을 저장할 때 연결 테이블에도 값이 저장된다. 따라서 이 코드를 실행하면 다음과 같은 SQL이 실행된다.

INSERT INTO PRODUCT ...
INSERT INTO MEMBER ...
INSERT INTO MEMBER_PRODUCT ...
public void find() {

    Member member = em.find(Member.class, "member1");
    List<Product> products = member.getProducts();  // 객체 그래프 탐색
    for (Product product : products) {
        System.out.println("product.name = " + product.getName());
    }
}

아래 예제를 보자. 순서대로 저장한 후에 탐색해보면 저장해두었던 상품1이 조회된다.

member.getProducts()를 호출해서 상품 이름을 출력하면 다음 SQL이 실행된다.

SELECT * FROM MEMBER_PRODUCT MP
INNER JOIN PRODUCT P ON MP.PRODUCT_ID = P.PRODUCT_ID
WHERE MP.MEMBER_ID=?

실행된 SQL을 보면 연결 테이블인 MEMBER_PRODUCT와 상품 테이블을 조인해서 연관된 상품을 조회한다.

@ManyToMany 덕분에 복잡한 다대다 관계를 애플리케이션에서는 아주 단순하게 사용할 수 있다. 이제 이 관계를 양방향으로 만들어보자.

2. 다대다 : 양방향

아래 예제를 보면 다대다 매핑이므로 역방향 @ManyToMany를 사용한다. 그리고 양쪽 중 원하는 곳에 mappedBy로 연관관계의 주인을 지정한다(물론 mappedBy가 없는 곳이 연관관계의 주인이다).

@Entity
public class Product {
    
    @Id
    private String id;
    
    @ManyToMany(mappedBy = "products")  // 역방향 추가
    private List<Member> members;
    ...
}

다대다의 양방향 연관관계는 다음처럼 설정하면 된다.

member.getProducts().add(product);
members.getMembers().add(member);

양방향 연관관계는 연관관계 편의 메소드를 추가해서 관리하는 것이 편리하다. 다음처럼 회원 엔티티에 연관관계 편의 메소드를 추가하자.

public void addProduct(Product product) {
    ...
    products.add(product);
    product.getmembers().add(this);
}

연관관계 편의 메소드를 추가했으므로 다음처럼 간단히 양방향 연관관계를 설정하면 된다.

member.addProduct(product);

양방향 연관관계로 만들었으므로 product.getMembers()를 사용해서 역방향으로 객체 그래프를 탐색할 수 있다. 아래 예제를 보자.

public void findInverse() {

    Product product = em.find(Product.class, "productA");
    List<Member> members = product.getMembers();
    for (Member member : members) {
        System.out.println("member = " + member.getUsername());
    }
}

참고

  • 자바 ORM 표준 JPA 프로그래밍

좋은 웹페이지 즐겨찾기