Close
Close full mode
logo만렙 개발자 키우기

(4) 다대다

Git RepositoryEdit on Github
Last update: a year ago by nowwaterReading time: 4 min

관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.

그래서 보통 다대다 관계를 일대다, 다대일 관계로 풀어내는 연결 테이블을 사용한다.

image

따라서 중간에 연결 테이블을 추가해야 한다.

이 테이블을 통해 다대다 관계를 일대다, 다대일 관계로 풀어낼 수 있다.

image


그런데 객체는 테이블과 다르게 객체 2개로 다대다 관계를 만들 수 있다.

ex) 회원 객체는 컬렉션을 사용해 상품들을 참조, 반대로 상품들도 컬렉션을 사용해 회원들을 참조

@ManyToMany 를 사용하여 다대다 관계를 편리하게 매핑할 수 있다.

6.4.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"), // joinColumns : 현재 방향인 회원과 매핑할 조인 컬럼 정보를 지정, MEMBER_ID로 지정함.
inverseJoinColumns = @JoinColumn(name = "PRODUCT_ID")) // 반대 방향인 상품과 매핑할 조인 컬럼 정보를 지정. 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) 엔티티 없이 매핑을 완료할 수 있다.

저장

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().add(productA); // 연관관계 설정
em.persist(member1);
}

회원1과 상품A의 연관관계를 설정했으므로 회원1을 저장할 때 연결 테이블에도 값이 저장된다.

@ManyToMany 로 매핑을 했지만, 데이터베이스에서는 다대다 매핑이 안되기 때문에 연결 테이블이 자동으로 생성되고 거기에도 데이터가 쌓이는 듯하다!

다음과 같은 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());
}
}

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

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

탐색 시 연결 테이블인 MEMBER_PRODUCT와 상품 테이블을 조인해서 연관된 상품을 조회한다.


6.4.2 다대다: 양방향

역방향 추가

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

양방향 연관관계는 연관관계 편의 메소드를 추가해서 관리하는 것이 편리하다.

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

이후 member.addProduct(product) 처럼 간단히 양방향 연관관계 설정

역방향 탐색

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

6.4.3 다대다: 매핑의 한계와 극복, 연결 엔티티 사용

@ManyToMany 를 사용하면 연결 테이블을 자동으로 처리해주므로 도메인 모델이 단순해지고 여러 가지로 편리하다.

하지만 실무에서 사용하기에는 한계가 있다.

예를 들면 회원이 상품을 주문하면연결 테이블에 단순히 주문한 회원 아이디와 상품 아이디만 담고 끝나지 않는다.

보통은 연결 테이블에 주문 수량 컬럼이나 주문한 날짜 같은 컬럼이 더 필요하다.

image

따라서 위와 같은 구조에서 연결 테이블에 주문 수량(ORDERAMOUNT)과 주문 날짜(ORDERDATE) 컬럼을 추가한다.

이렇게 컬럼을 추가하면 더 이상 @ManyToMany를 사용할 수 없다.

=> 왜냐하면 주문 엔티티나 상품 엔티티에는 추가한 컬럼들을 매핑할 수 없기 때문이다. => 이유를 찾아봐야겠다.

결국 연결 테이블을 매핑하는 연결 엔티티를 만들고 이곳에 추가한 컬럼들을 매핑해야 한다.

그리고 엔티티 간의 관계도 테이블 관계처럼 다대다에서 일대다, 다대일 관계로 풀어야 한다.

image

연결 테이블에 주문 수량(ORDERAMOUNT)과 주문 날짜(ORDERDATE) 컬럼을 추가했다.

회원 코드

@Entity
public class Member {
@Id @Column(name = "MEMBER_ID")
private String id;
// 역방향
@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProducts;
...
}

회원과 회원상품은 양방향 관계

회원상품 엔티티 쪽이 외래 키를 갖고 있으므로 연관관계의 주인이다.

상품 코드

@Entity
public class Product {
@Id @Column(name = "PRODUCT_ID")
private String id;
private String name;
...
}

상품 엔티티에서 회원상품 엔티티로 객체 그래프 탐색 기능이 필요하지 않다고 판단해서 연관관계 X

회원상품 엔티티 코드

@id 와 외래 키를 매핑하는 @JoinColumn을 동시에 사용해서 기본 키 + 외래 키를 한 번에 매핑

그리고 @IdClass 를 사용해서 복합 기본 키를 매핑

@Entity
@IdClass(MemberProductId.class) // 복합 키를 사용하기 위한 식별자 클래스
public class MemberProduct {
@Id
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member; // MemberProductId.member와 연결
@Id
@ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product product; // MemberProductId.product 연결
private int orderAmount;
...
}

회원상품 식별자 클래스

public class MemberProductId implements Serializable {
private String member; // MemberProduct.member와 연결
private String product; // MemberProduct.product와 연결
// hashCode and equals
@Override
public boolean equals(Object o) {...}
@Override
public int hashCode() {...}
}

복합 기본 키

회원상품 엔티티는 기본 키가 MEMBER_IDPRODUCT_ID로 이루어진 복합 기본 키다.

JPA에서 복합 키를 사용하려면 별도의 식별자 클래스를 만드렁야 한다.

그리고 엔티티에 @IdClass를 사용해서 식별자 클래스를 지정해준다.

특징

  • 복합 키는 별도의 식별자 클래스로 만들어야 한다.
  • Serializable을 구현해야 한다.
  • equalshashCode 메소드를 구현해야 한다.
  • 기본 생성자가 있어야 한다.
  • 식별자 클래스는 public 이어야 한다.
  • @IdClass를 사용하는 방법 외에 @EmbeddedId를 사용하는 방법도 있다.

식별 관계

부모 테이블의 기본 키를 받아서 자신의 기본 키 + 외래 키로 사용하는 것

회원상품은

  1. 회원의 기본 키를 받아서 자신의 기본 키로 사용함과 동시에 회원과의 관계를 위한 외래 키로 사용

  2. 상품의 기본 키를 받아서 자신의 기본 키로 사용함과 동시에 상품과의 관계를 위한 외래 키로 사용

MemberProductId 식별자 클래스로 두 기본 키를 묶어서 복합 기본 키로 사용

저장

public void save() {
// 회원 저장
Member member1 = new Member();
member1.setId("member1");
member1.setUsername("회원1");
em.persist(member1);
// 상품 저장
Product productA = new Product();
productA.setId("productA");
pruductA.setName("상품1");
em.persist(productA);
// 회원상품 저장
MemberProduct memberProduct = new MemberProduct();
memberProduct.setMember(member1); // 주문 회원 - 연관관계 설정
memberProduct.setProduct(productA); // 주문 상품 - 연관관계 설정
memberProduct.setOrderAmount(2); // 주문 수량
em.persist(memberProduct);
}

데이터베이스 저장 시 연관된 회원의 식별자와 상품의 식별자를 가져와서 자신의 기본 키 값으로 사용

조회

복합 키는 항상 식별자 클래스를 만들어야 한다.

em.find()를 통해 생성한 식별자 클래스로 엔티티를 조회한다.

public void find() {
// 기본 키 값 생성
MemberProductId memberProductId = new MemberProductId();
memberProductId.setMember("member1");
memberProductId.setProduct("productA");
MemberProduct memberProduct = em.find(MemberProduct.class, memberProductId);
Member member = memberProduct.getMember();
Product product = memberProduct.getProduct();
System.out.println("member = " + member.getUsername());
System.out.println("product = " + product.getName());
System.out.println("orderAmount = " + memberProduct.getOrderAmount());
}

복합 키를 사용하면 ORM 매핑에서 처리할 일이 상당히 많아진다.

필요 : 복합 키를 위한 식별자 클래스, @IdClass 또는 @EmbeddedId 사용, equalshashCode 구현


6.4.4 다대다: 새로운 기본 키 사용

추천하는 기본 키 생성 전략

데이터베이스에서 자동으로 생성해주는 대리 키를 Long 값으로 사용하는 것!

장점

  • 간편하고 거의 영구적으로 쓸 수 있으며 비즈니스에 의존하지 않는다.
  • ORM 매핑 시 복합 키를 만들지 않아도 되므로 간단히 매핑을 완성할 수 있다.

회원상품 대신 주문(Order) 이라는 이름을 사용하는 것이 더 어울린다. => ORDERS 사용

image

주문 코드

대리 키를 사용함으로써 이전에 식별 관계에서의 복합 키 사용 보다 매핑이 단순하고 이해가 쉬워졌다.

@Entity
public class Order {
@Id @GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
@ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product product;
private int orderAmount;
...
}

회원 엔티티와 상품 엔티티

import java.util.ArrayList;
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID")
private String id;
private String username;
@OneToMany(mappedt = "member")
private List<Order> orders = new ArrayList<Order>();
...
}
@Entity
public class Product {
@Id
@Column(name = "PRODUCT_ID")
private String id;
private String name;
}

저장

public void save() {
// 회원 저장
Member member1 = new Member();
member1.setId("member1");
member1.setUsername("회원1");
em.persist(member1);
// 상품 저장
Product productA = new Product();
productA.setId("productA");
productA.setName("상품1");
em.persist(productA);
// 주문 저장
Order order = new Order();
order.setMember(member1); // 주문 회원 - 연관관계 설정
order.setProduct(productA); // 주문 상품 - 연관관계 설정
order.setOrderAmount(2); // 주문 수량량
m.persist(order);
}

조회

식별자 클래스를 사용하지 않아 코드가 단순해짐

public void find() {
Long orderId = 1L;
Order order = em.find(Order.class, orderId);
Member member= order.getMember();
Product product =order.getProduct();
System.out.println("member = " + member.getUsername());
System.out.println("product = " + product.getName());
System.out.println("orderAmount = " + order.getOrderAmount());
}

6.4.5 다대다 연관관계 정리

  • 식별 관계 : 받아온 식별자를 기본 키 + 외래 키로 사용한다.

    • 부모 테이블의 기본 키를 받아서 자식 테이블의 기본 키 + 외래 키로 사용하는 것
  • 비식별 관계 : 받아온 식별자는 외래 키로만 사용하고 새로운 식별자를 추가한다.

    • 단순히 외래 키로만 사용, 기본 키는 자동 생성 전략으로 대리 키 생성

    • 단순하고 편리하게 ORM 매핑 가능 -> 사용 추천

🚀 JPA — Previous
(3) 일대일
Next — 🚀 JPA
7장. 고급 매핑