elevne's Study Note
JPA N:M 관계 본문
관계형 데이터베이스에서는 정규화된 테이블 2 개로 다대다 관계를 표현할 수 없다. 그래서 보통 다대다 관계를 일대다, 다대일 관계로 풀어내는 연결테이블을 사용한다. 쇼핑 플랫폼을 예시로 생각해볼 수 있다. 회원들은 상품을 주문하고, 상품들은 회원들에 의해 주문된다. 둘은 다대다 관계다. 회원 테이블과 상품 테이블 둘만으로는 이 관계를 표현할 수 없다. 중간에 연결 테이블을 추가해주어야 한다.
그런데 객체는 테이블과 다르게 객체 2 개로 다대다 관계를 만들 수 있다. 예를 들어 회원 객체는 컬렉션을 사용해서 상품들을 참조하면 되고, 반대로 상품들도 컬렉션을 사용해서 회원들을 참조하면 된다. @ManyToMany 를 사용하여 다대다 관계를 편리하게 매핑할 수 있다.
@Entity
@Getter
@Setter
public class User {
@Id @Column(name = "USER_ID")
private String id;
private String username;
@ManyToMany
@JoinTable(name = "USER_PRODUCT", joinColumns = @JoinColumn(name = "MEMBER_ID"),
inverseJoinColumns = @JoinColumn(name = "PRODUCT_ID"))
private List<Product> products = new ArrayList<>();
}
@Entity
@Getter
@Setter
public class Product {
@Id @Column(name = "PRODUCT_ID")
private String id;
private String name;
}
위와 같이 작성해 사용할 수 있다. 회원 엔티티와 상품 엔티티를 @ManyToMany 로 매핑한 것이다. 여기서 중요한 점은 @ManyToMany 와 @JoinTable 을 사용해서 연결테이블을 바로 매핑한 것이다. 따라서 회원과 상품을 연결하는 USER_PRODUCT 엔티티 없이 매핑을 완료할 수 있는 것이다. @JoinTable.name 으로 연결테이블을 지정, @JoinTable.joinColumns 로 현재 방향인 회원과 매핑할 조인 컬럼 정보를 지정, @JoinTable.inverseJoinColumns 로 반대 방향인 상품과 매핑할 조인 컬럼 정보를 지정한다.
@Test
public void manyToManyTest() {
EntityManager em = entityManagerFactory.createEntityManager();
EntityManager em2 = entityManagerFactory.createEntityManager();
EntityTransaction tx = em.getTransaction();
try {
tx.begin();
Product productA = new Product();
productA.setId("productA");
productA.setName("상품A");
em.persist(productA);
User member1 = new User();
member1.setId("member1");
member1.setUsername("회원1");
member1.getProducts().add(productA);
em.persist(member1);
tx.commit();
User user = em2.find(User.class, member1.getId());
List<Product> products = user.getProducts();
for (Product product : products) {
System.out.println(product.getName());
}
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
}
다대다 양방향 매핑을 해주기 위해서는 역방향에 @ManyToMany 를 사용하면 된다. 원하는 곳에 mappedBy 로 연관관계의 주인을 지정한다. (위 경우에서는 Product 에 @ManyToMany(mappedBy = "products") private List<User> users; 를 추가해주면 된다) 그러면 Product 에서도 객체 그래프 탐색이 가능해진다.
그런데 위 방식에는 한계가 있다. @ManyToMany 를 사용하면 연결 테이블을 자동으로 처리해주므로 도메인 모델이 단순해지고 여러가지로 편리하기는 하나, 실무에서는 연결테이블에 단순히 주문한 회원아이디와 상품아이디만 담고 끝나지 않는다. 보통은 연결테이블에 주문 수량 컬럼이나 주문한 날짜 같은 컬럼이 더 필요하다. 연결테이블에 컬럼이 추가되면 @ManyToMany 를 사용할 수 없다. 결국 매핑하는 연결 엔티티를 만들고 이곳에 추가할 컬럼들을 매핑해야 한다. 그리고 엔티티 간의 관계도 테이블 관계처럼 다대다에서 일대다, 다대일 관계로 풀어야한다.
@Entity
@Data
public class Member2 {
@Id @Column(name = "MEMBER_ID")
private String id;
private String username;
@OneToMany(mappedBy = "member2")
private List<MemberProduct2> memberProduct2s;
}
@Entity
@Data
public class Product2 {
@Id @Column(name = "PRODUCT_ID")
private String id;
private String name;
}
@Entity
@Data
@IdClass(MemberProduct2Id.class)
public class MemberProduct2 {
@Id @ManyToOne @JoinColumn(name = "MEMBER_ID")
private Member2 member2;
@Id @ManyToOne @JoinColumn(name = "PRODUCT_ID")
private Product2 product2;
private int orderAmount;
}
@EqualsAndHashCode
@Data
public class MemberProduct2Id implements Serializable {
private String member2;
private String product2;
}
MemberProduct2 클래스에는 기본 키를 매핑하는 @Id, 외래 키를 매핑하는 @JoinColumn 을 동시에 사용해서 기본 키 + 외래 키를 한 번에 매핑한다. 그리고 @IdClass 를 사용해서 복합키를 매핑한다. 복합키는 위와 같이 별도의 식별자 클래스로 만들어야 하며, Serializable 을 구현해야 한다. equals 와 hashCode 를 구현해야 하며, 또 기본 생성자가 필요하다. (이 외에도 @EmbeddedId 를 사용하는 방법도 있긴 하다)
@Test
public void manyToManyTest2() {
EntityManager em = entityManagerFactory.createEntityManager();
EntityTransaction tx = em.getTransaction();
try {
tx.begin();
Member2 member1 = new Member2();
member1.setId("member1");
member1.setUsername("회원1");
em.persist(member1);
Product2 productA = new Product2();
productA.setId("productA");
productA.setName("상품1");
em.persist(productA);
MemberProduct2 memberProduct = new MemberProduct2();
memberProduct.setMember2(member1);
memberProduct.setProduct2(productA);
memberProduct.setOrderAmount(3);
em.persist(memberProduct);
tx.commit();
MemberProduct2Id memberProduct2Id = new MemberProduct2Id();
memberProduct2Id.setMember2("member1");
memberProduct2Id.setProduct2("productA");
MemberProduct2 memberProduct2 = em.find(MemberProduct2.class, memberProduct2Id);
Member2 member = memberProduct2.getMember2();
Product2 product = memberProduct2.getProduct2();
System.out.println(member.getUsername());
System.out.println(product.getName());
System.out.println(memberProduct2.getOrderAmount());
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
}
이 다음으로는 복합 키를 사용하지 않고 간단히 다대다 관계를 구성하는 방법에 대해 알아본다. 새로운 기본 키를 사용하는 것이다. 데이터베이스에서 자동으로 생성해주는 대리키를 Long 값으로 사용하여, 간편하게 사용할 수 있다. ORM 매핑 시 복합키를 만들지 않아도 된다.
@Entity
@Data
public class MemberProduct2 {
@Id @Column(name = "ORDER_ID") @GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@ManyToOne @JoinColumn(name = "MEMBER_ID")
private Member2 member2;
@ManyToOne @JoinColumn(name = "PRODUCT_ID")
private Product2 product2;
private int orderAmount;
}
다대다 관계를 일대다 다대일 관계로 풀어낼 때에는 연결 테이블을 만들 때 식별관계(받아온 식별자를 기본 키 + 외래 키로 사용) 로 할 것인지 비식별관계(받아온 식별자는 외래키로만 사용하고 새로운 식별자 추가)로 할 것인지를 결정해야 한다. 객체 입장에서 보면 비식별관계처럼 복합 키를 위한 식별자 클래스를 만들지 않는 방식이 단순하고 편리하므로 비식별관계를 사용하는 것이 추천된다고 한다.
Reference:
자바 ORM 표준 JPA 프로그래밍
'Backend > JPA 프로그래밍' 카테고리의 다른 글
JPA - Proxy, Cascade, Orphan (0) | 2023.07.11 |
---|---|
JPA 고급매핑 (0) | 2023.07.09 |
JPA 연관관계 매핑 (0) | 2023.07.05 |
JPA Entity 매핑 (0) | 2023.07.04 |
JPA 영속성 관리 (0) | 2023.07.03 |