(4) 다대다
관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.
그래서 보통 다대다 관계를 일대다, 다대일 관계로 풀어내는 연결 테이블을 사용한다.
따라서 중간에 연결 테이블을 추가해야 한다.
이 테이블을 통해 다대다 관계를 일대다, 다대일 관계로 풀어낼 수 있다.
그런데 객체는 테이블과 다르게 객체 2개로 다대다 관계를 만들 수 있다.
ex) 회원 객체는 컬렉션을 사용해 상품들을 참조, 반대로 상품들도 컬렉션을 사용해 회원들을 참조
@ManyToMany
를 사용하여 다대다 관계를 편리하게 매핑할 수 있다.
6.4.1 다대다: 단방향
다대다 단방향 회원
@Entitypublic 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>();...}
다대다 단방향 상품
@Entitypublic 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 MPINNER JOINPRODUCT P ON MP.PRODUCT_ID = P.PRODUCT_IDWHERE MP.MEMBER_ID = ?
탐색 시 연결 테이블인 MEMBER_PRODUCT
와 상품 테이블을 조인해서 연관된 상품을 조회한다.
6.4.2 다대다: 양방향
역방향 추가
@Entitypublic class Product {@Idprivate 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
를 사용하면 연결 테이블을 자동으로 처리해주므로 도메인 모델이 단순해지고 여러 가지로 편리하다.
하지만 실무에서 사용하기에는 한계가 있다.
예를 들면 회원이 상품을 주문하면연결 테이블에 단순히 주문한 회원 아이디와 상품 아이디만 담고 끝나지 않는다.
보통은 연결 테이블에 주문 수량 컬럼이나 주문한 날짜 같은 컬럼이 더 필요하다.
따라서 위와 같은 구조에서 연결 테이블에 주문 수량(ORDERAMOUNT)과 주문 날짜(ORDERDATE) 컬럼을 추가한다.
이렇게 컬럼을 추가하면 더 이상 @ManyToMany
를 사용할 수 없다.
=> 왜냐하면 주문 엔티티나 상품 엔티티에는 추가한 컬럼들을 매핑할 수 없기 때문이다. => 이유를 찾아봐야겠다.
결국 연결 테이블을 매핑하는 연결 엔티티를 만들고 이곳에 추가한 컬럼들을 매핑해야 한다.
그리고 엔티티 간의 관계도 테이블 관계처럼 다대다에서 일대다, 다대일 관계로 풀어야 한다.
연결 테이블에 주문 수량(ORDERAMOUNT)과 주문 날짜(ORDERDATE) 컬럼을 추가했다.
회원 코드
@Entitypublic class Member {@Id @Column(name = "MEMBER_ID")private String id;// 역방향@OneToMany(mappedBy = "member")private List<MemberProduct> memberProducts;...}
회원과 회원상품은 양방향 관계
회원상품 엔티티 쪽이 외래 키를 갖고 있으므로 연관관계의 주인이다.
상품 코드
@Entitypublic 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@Overridepublic boolean equals(Object o) {...}@Overridepublic int hashCode() {...}}
복합 기본 키
회원상품 엔티티는 기본 키가 MEMBER_ID
와 PRODUCT_ID
로 이루어진 복합 기본 키다.
JPA에서 복합 키를 사용하려면 별도의 식별자 클래스를 만드렁야 한다.
그리고 엔티티에 @IdClass
를 사용해서 식별자 클래스를 지정해준다.
특징
- 복합 키는 별도의 식별자 클래스로 만들어야 한다.
Serializable
을 구현해야 한다.
equals
와hashCode
메소드를 구현해야 한다.
- 기본 생성자가 있어야 한다.
- 식별자 클래스는
public
이어야 한다.
@IdClass
를 사용하는 방법 외에@EmbeddedId
를 사용하는 방법도 있다.
식별 관계
부모 테이블의 기본 키를 받아서 자신의 기본 키 + 외래 키로 사용하는 것
회원상품은
회원의 기본 키를 받아서 자신의 기본 키로 사용함과 동시에 회원과의 관계를 위한 외래 키로 사용
상품의 기본 키를 받아서 자신의 기본 키로 사용함과 동시에 상품과의 관계를 위한 외래 키로 사용
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
사용,equals
와hashCode
구현
6.4.4 다대다: 새로운 기본 키 사용
추천하는 기본 키 생성 전략
데이터베이스에서 자동으로 생성해주는 대리 키를 Long
값으로 사용하는 것!
장점
- 간편하고 거의 영구적으로 쓸 수 있으며 비즈니스에 의존하지 않는다.
- ORM 매핑 시 복합 키를 만들지 않아도 되므로 간단히 매핑을 완성할 수 있다.
회원상품 대신 주문(Order) 이라는 이름을 사용하는 것이 더 어울린다. => ORDERS 사용
주문 코드
대리 키를 사용함으로써 이전에 식별 관계에서의 복합 키 사용 보다 매핑이 단순하고 이해가 쉬워졌다.
@Entitypublic 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;@Entitypublic class Member {@Id@Column(name = "MEMBER_ID")private String id;private String username;@OneToMany(mappedt = "member")private List<Order> orders = new ArrayList<Order>();...}@Entitypublic 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 매핑 가능 -> 사용 추천