- 참조: 자바 ORM 표준 JPA 프로그래밍
- M : N 연결 엔티티
이제 1 : 1, 1 : N, M : N 의 단방향과 양방향을 모두 알아보았으니 도메인 설계만 잘하면 JPA 로 모든 맵핑을 할 수 있을까? 세상 문제가 그렇게 간단하면 좋겠지만 M : N 관계에서 더 살펴볼 사항이 있다.
앞의 글 다양한 연관관계 M : N 의 ER Diagram 과 UML 을 다시 한번 생각해보자. ER Diagram 에 USER 와 PRODUCT 테이블이 있었고, 두 테이블이 서로에 대해 M : N 의 관계라면 외래키를 이용해서 표현할 방법이 없으므로 USER_PRODUCT 조인 테이블도 추가했었다.
반면 객체(UML Digram)에서는 User 와 Product 엔티티를 선언하고 서로 Collection 으로 참조하였으며, JPA 의 @ManyToMany 어노테이션과 @JoinTable 을 이용하여 M : N 관계를 표현하였다.
잠시 우리가 상품을 주문하는 쿠팡이나 네이버 등 회원과 상품이 존재하는 웹 어플리케이션을 생각해보자. 회원인 우리는 상품을 주문한다. 그리고 난 후 나중에 사이트를 재접속하여 주문한 건을 조회하면 어떠한 상품을 몇개 그리고 언제 주문했는지를 알 수 있다.
이 상황을 우리가 앞에서 설계했던 DB 설계에 반영한다면 USER_PRODUCT 테이블에 주문수량과 주문일자 컬럼을 추가할것이다. 여기서부터 문제가 발생한다. DB 설계는 컬럼만 추가하면 되지만 객체에서는 User 과 Product 2 개의 엔티티와 @ManyToMany 맵핑만으로 표현할 수 없게 된다.
Product 에 추가하면 되지 않을까? 라고 생각할 수 있겠다. 하지만 상품은 회원이 주문한 당시의 형상을 반영한 상품이 아닌 상품 자체의 고유한 성질만 나타내야 한다. 상품에 주문수량과 주문일자를 넣게되면 더이상 Product 는 고유한 상품의 엔티티가 아닌 주문정보가 반영된 상품형상의 엔티티가 된다. 만약 상품의 재고가 별로 없어서 해당 상품에 대한 주문을 막아야 한다거나, 제품의 이름을 변경하는 등과 같은 기능을 제공해야 한다면 하면 난감한 상황이 펼쳐진다.
기존의 ER Diagram 처럼 USER_PRODUCT 에 ORDER_AMOUNT 와 ORDER_DATE 컬럼을 추가시켜주었다.
UML 에는 위와 같이 연결 엔티티를 추가해주고 해당 컬럼들에 해당하는 Field 들도 추가하였다. User 는 여러 상품을 주문할 수 있으므로 UserProduct 와 1 : N 의 관계이다. UserProduct 와 Product 는 1 주문에는 1 개의 상품에 대한 수량과 주문일자가 있고, Product 는 여러 주문에서 상품이 주문되므로 N : 1 관계가 된다.
이 설계를 기반으로 JPA 코드를 작성해보자. User 엔티티의 코드는 아래와 같이 작성하였다.
@Entity
public class User {
@Id
@Column(name = "USER_ID")
private String id;
private String userName;
@OneToMany(mappedBy = "user")
private List<UserProduct> userProducts = new ArrayList<>();
User 와 UserProduct 는 1 : N 이므로 @OneToMany 다중성을 사용한다. 그리고 양방향이며 외래키는 N 에 해당하는 UserProduct 엔티티에서 관리하므로 mappedBy 로 UserProduct 에서 User 를 참조하는 user 필드명을 기입한다.
Product 코드는 아래와 같이 작성하였다. UserProduct 에서 Product 단방향 참조이므로 Product 에서는 연관관계 관련해서 작성할 코드는 없다.
@Entity
public class Product {
@Id
@Column(name = "PRODUCT_ID")
private String id;
private String name;
이제 핵심에 해당하는 UserProduct 를 작성해보자.
@Entity
@IdClass(UserProductId.class)
public class UserProduct {
@Id
@ManyToOne
@JoinColumn(name = "USER_ID")
private User user;
@Id
@ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product product;
private int orderAmount;
@Temporal(TemporalType.TIMESTAMP)
private Date orderDate;
USER_ID 와 PRODUCT_ID 가 기본키에 해당하기 때문에 User 와 Product 형 참조 필드에도 @Id 를 붙여준다. 동시에 이 두 컬럼들은 외래키이기도 하므로 @JoinColumn 으로 설정한다. 다중성도 잊지 않고 N : 1 에 해당하는 @ManyToOne 을 사용한다.
위 내용까지는 앞에서 배운 내용이므로 별도 설명이 필요없다. 그런데 갑자기 @IdClass 라는 어노테이션이 등장한다. USER_PRODUCT 에서의 키는 복합 키인데 이를 사용해야할 때 별도의 식별자 클래스를 만들어야 하고 이때 @IdClass 를 이용하여 식별자 클래스를 지정한다. 식별자 클래스는 몇 가지 규칙이 있는데 아래 규약을 지켜야 한다.
- 복합키는 별도의 식별자여야 한다.
- Serializable 구현
- equals와 hashCode 메소드 구현
- 기본 생성자
- public 접근제어자
- @IdClass 혹은 @EmbeddedId 사용
위 규약을 지킨 UserProductId 식별자 클래스의 풀버전 소스는 아래와 같다. hashCode 와 equals 는 IDE 라면 자동생성 기능을 제공하므로 당황하지 않아도 된다. hashCode 와 equals 구현에 대한 중요성과 왜 hashCode 가 저런 모양인지 알고 싶다면 이펙티브자바의 해당 챕터를 참조하면 된다.
public class UserProductId implements Serializable{
/**
*
*/
private static final long serialVersionUID = 5953921082025893508L;
private String user;
private String product;
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
public String getProduct() {
return product;
}
public void setProduct(String product) {
this.product = product;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((product == null) ? 0 : product.hashCode());
result = prime * result + ((user == null) ? 0 : user.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
UserProductId other = (UserProductId) obj;
if (product == null) {
if (other.product != null)
return false;
} else if (!product.equals(other.product))
return false;
if (user == null) {
if (other.user != null)
return false;
} else if (!user.equals(other.user))
return false;
return true;
}
}
이제 잘 작동하는지 테스트코드를 작성해보자. 제품과 회원을 영속 시킨 뒤 이를 관계 엔티티인 UserProduct 에 할당하고 영속시킨다.
public static void save(EntityManager em) {
EntityTransaction tx = em.getTransaction();
tx.begin();
Product product = new Product();
product.setId("PRODUCT#1");
product.setName("Product Name#1");
em.persist(product);
User user = new User();
user.setId("USER#1");
user.setUserName("User Name#1");
em.persist(user);
UserProduct userProduct = new UserProduct();
userProduct.setUser(user);
userProduct.setProduct(product);
userProduct.setOrderAmount(2);
userProduct.setOrderDate(new Date());
em.persist(userProduct);
tx.commit();
em.close();
}
그 후 find 메소드는 아래와 같이 작성한다. 여태까지 em.find 메소드에서는 엔티티 클래스형과 2 번째 인자인 PK 에는 String 형이나 Long 형을 사용하였다. 그런데 UserProduct 는 복합 키 이므로 이를 검색할 때 복합 키 식별자 클래스의 객체를 넘겨준다.
public static void find(EntityManager em) {
EntityTransaction tx = em.getTransaction();
tx.begin();
UserProductId userProductId = new UserProductId();
userProductId.setUser("USER#1");
userProductId.setProduct("PRODUCT#1");
UserProduct userProduct = em.find(UserProduct.class, userProductId);
System.out.println("UserProduct Info=============");
System.out.println(userProduct);
tx.commit();
em.close();
}
결과를 조회하면 아래와 같이 회원상품에 대한 정보가 잘 나타난다.
Hibernate:
select
userproduc0_.product_id as product_1_8_0_,
userproduc0_.user_id as user_id2_8_0_,
userproduc0_.order_amount as order_am3_8_0_,
userproduc0_.order_date as order_da4_8_0_,
product1_.product_id as product_1_5_1_,
product1_.name as name2_5_1_,
user2_.user_id as user_id1_7_2_,
user2_.user_name as user_nam2_7_2_
from
user_product userproduc0_
inner join
product product1_
on userproduc0_.product_id=product1_.product_id
inner join
user user2_
on userproduc0_.user_id=user2_.user_id
where
userproduc0_.product_id=?
and userproduc0_.user_id=?
UserProduct Info=============
UserProduct [user=com.example.demo.test.User@43ab9ae9, product=com.example.demo.test.Product@70c205bf, orderAmount=2, orderDate=2021-07-10 00:26:09.671]
H2 Console 결과
'Framework and Tool > JPA' 카테고리의 다른 글
JPA - 다양한 연관관계 - 요구사항 분석과 맵핑 2 (0) | 2021.07.10 |
---|---|
JPA - 다양한 연관관계 - M : N 비식별관계 (0) | 2021.07.10 |
JPA - 다양한 연관관계 - M : N (0) | 2021.07.08 |
JPA - 다양한 연관관계 - 1 : 1 (0) | 2021.07.06 |
JPA - 다양한 연관관계 - N : 1 과 1 : N (0) | 2021.07.03 |
댓글