2022. 7. 3. 16:43ㆍLanguage`/JPA
관계형 데이터베이스에서는 애초에 "정규화된 테이블" 2개로 다대다 관계를 표현할 수 없다
따라서 다대다 관계를 {일대다 - 다대일}로 풀어주는 "연결 테이블"을 사용한다
Member & Product는 다대다 관계이므로 "Member_Product"라는 연결 테이블을 통해서 {일대다 - 다대일}로 풀어버렸다
하지만 객체에서는 객체 2개로 "다대다 관계"를 만들어낼 수 있다
각 객체마다 상대방 객체를 컬렉션화 시켜서 관리하면 된다
1. 다대다 단방향
Member (N - 주인)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "member")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
private String username;
@ManyToMany
@JoinTable(
name = "MEMBER_PRODUCT_TABLE",
joinColumns = @JoinColumn(name = "member_id"),
inverseJoinColumns = @JoinColumn(name = "product_id")
)
private List<Product> productList = new ArrayList<>();
}
Product (M - 주인 X)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "product")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "product_id")
private Long id;
private String name;
}
이렇게 두 엔티티간의 JoinTable(member_product_table)이 생성됨을 확인할 수 있다
여기서 이 JoinTable은 두 엔티티간의 다대다 관계를 {일대다 - 다대일}로 풀어내기 위한 별도의 테이블이므로 사용할때는 이 JoinTable을 신경쓰지 않아도 된다
Product product = new Product("Product-A");
em.persist(product);
Member user = new Member("memberA");
user.getProductList().add(product);
em.persist(user);
JoinTable에도 각 FK값이 잘 들어감을 확인하였다
※ @JoinTable
주요 속성 | 기능 |
name | 연결 테이블의 이름을 지정 |
joinColumns | @JoinTable이 존재하는 Entity상에서 어느 컬럼이 PK이고 어떤 것을 가져다가 쓸지 |
inverseJoinColumns() | @JoinTable이 없는 반대편 Entity상에서 어느 컬럼이 PK이고 어떤 것을 가져다가 쓸지 |
2. 다대다 양방향
다대다 양방향은 그냥 주인이 아닌 테이블상에서 @ManyToMany를 매핑해주고 내부 속성에서 mappedBy를 설정해주면 된다
Member (N - 주인)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "member")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
private String username;
@ManyToMany
@JoinTable(
name = "MEMBER_PRODUCT_TABLE",
joinColumns = @JoinColumn(name = "member_id"),
inverseJoinColumns = @JoinColumn(name = "product_id")
)
private List<Product> productList = new ArrayList<>();
public void orderProduct(Product product){
this.getProductList().add(product);
product.getMemberList().add(this);
}
}
Product (M - 주인 X)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "product")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "product_id")
private Long id;
private String name;
@ManyToMany(mappedBy = "productList")
private List<Member> memberList = new ArrayList<>();
}
이전에 일대다 양방향 연관관계에서도 말했듯이 "양방향 연관관계"는 연관관계 편의 메소드를 만들어서 활용하는게 두 객체에 넣는 것을 까먹지도 않고 편리하다
- 편리한 만큼 중복 방지 로직도 잘 고려해서 작성해야 한다
다대다 매핑의 한계 & 극복
@ManyToMany와 @JoinTable을 사용하면 알아서 연결 테이블을 처리해주기 때문에 굉장히 유용할거라고 생각이 든다
하지만 이러한 매핑을 실무에서 사용하기에는 분명한 한계가 존재한다
왜냐하면 위와 같이 만들게되면 연결 테이블을 단순히 양쪽의 PK를 참조하고 있는 모습이고 또 다른 컬럼을 추가할 수 없다
그래서 요구사항이 달라지게되면 테이블의 구조 자체(JoinTable)를 변경해야 하는 문제가 존재한다
예를 들어서 회원이 상품을 "주문"한다면 주문이라는 연결 테이블에는 일반적으로 {주문 날짜 / 주문 수량 / ...} 이러한 컬럼들이 필요하다
그러나 단순히 @ManyToMany + @JoinTable을 활용해서 "주문"이라는 연결 테이블(@JoinTable)을 만든다면 추가적인 컬럼을 넣을 수 없다
>> 연결 테이블을 위한 "Entity"를 따로 만들어주자
1. 비식별 관계 연결 테이블
비식별 관계란 참조하는 FK를 본인의 PK로 사용하지 않는 관계이다
Member (1)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "member")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
private String username;
@OneToMany(mappedBy = "member")
private List<Order> orderList = new ArrayList<>();
}
Product (1)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "product")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "product_id")
private Long id;
private String name;
}
Order (N)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "order_id")
private Long id;
private Integer orderAmount;
private LocalDateTime orderDate;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id")
private Product product;
public void orderMember(Member member){
if(member.getOrderList().contains(this)){
member.getOrderList().remove(this);
}
this.member = member;
member.getOrderList().add(this);
}
public void orderProduct(Product product){
this.product = product;
}
}
Product와 Order간에는 왜 양방향 매핑을 하지 않았냐면 Product Table상에서는 어느 Product가 Order되었는지 굳이 양방향 매핑을 통해서 알 필요가 없기 때문이다
왜냐하면 어떤 Product에 대해서 Order을 확인하고 싶으면 그냥 Order Table상에서 Product Table과 Join을 하면 파악할 수 있기 때문이다
Product product = new Product("Product - A");
Member member = new Member("memberA");
em.persist(product);
em.persist(member);
Order order = new Order(5, LocalDateTime.now());
order.orderMember(member);
order.orderProduct(product);
em.persist(order);
2. 식별 관계 연결 테이블
식별 관계란 참조하는 FK를 본인의 PK에 더해서 "복합키"로 활용하는 관계이다
JPA에서 복합키를 사용하려면 "별도의 식별자 클래스 : @IdClass/@EmbeddedId"를 만들어야 한다
식별자 클래스의 특징은 다음과 같다
1. 복합키는 별도의 식별자 클래스로 만들어야 한다 (식별자 클래스 속성명 == 엔티티의 식별자 속성명)
2. Serializable을 구현해야 한다
3. equals & hashcode를 Override해야 한다
4. 기본 생성자가 있어야 한다
5. 식별자 클래스는 public이어야 한다
- @IdClass를 활용하는 복합키는 복합키를 사용할 엔티티 위에 @IdClass(식별자 클래스)를 적어주기
- @EmbeddedId를 활용하는 복합키는 복합키 위에 @EmbeddedId를 적어주기
※ (1) @IdClass
Order (N - 복합키 활용 클래스)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "orders")
@IdClass(OrderId.class)
public class Order {
@Id
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member memberId;
@Id
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id")
private Product productId;
private Integer orderAmount;
private LocalDateTime orderDate;
}
OrderId (복합키 클래스)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OrderId implements Serializable {
private Long memberId;
private Long productId;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OrderId orderId = (OrderId) o;
return Objects.equals(getMemberId(), orderId.getMemberId()) && Objects.equals(getProductId(), orderId.getProductId());
}
@Override
public int hashCode() {
return Objects.hash(getMemberId(), getProductId());
}
}
여기서 "반드시" 복합키 클래스의 PK 필드명이랑 복합키를 사용할 엔티티의 PK 필드명은 일치해야 한다
Orders Table의 복합키 설정이 완료된 것을 확인할 수 있다
Product product = new Product("Product-A");
Member member = new Member("memberA");
em.persist(product);
em.persist(member);
Order order = new Order(member, product, 5, LocalDateTime.now());
em.persist(order);
※ (2) @EmbeddedId
Order (N - 복합키 활용 클래스)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "orders")
public class Order {
@EmbeddedId
private OrderId id;
private Integer orderAmount;
private LocalDateTime orderDate;
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("member_id")
@JoinColumn(name = "member_id")
private Member memberId;
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("product_id")
@JoinColumn(name = "product_id")
private Product productId;
}
OrderId (복합키 클래스)
@Getter
@Embeddable
public class OrderId implements Serializable {
@Column(name = "member_id")
private Long memberId;
@Column(name = "product_id")
private Long productId;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OrderId orderId = (OrderId) o;
return Objects.equals(getMemberId(), orderId.getMemberId()) && Objects.equals(getProductId(), orderId.getProductId());
}
@Override
public int hashCode() {
return Objects.hash(getMemberId(), getProductId());
}
}
Product product = new Product("Product-A");
Member member = new Member("memberA");
em.persist(product);
em.persist(member);
OrderId orderId = new OrderId(member.getId(), product.getId());
Order order = new Order(orderId, 5, LocalDateTime.now());
em.persist(order);