이번 시간에는 M:N 관계의 처리에 대해 살펴보자.
M:N의 관계 처리
M:N의 관계
M:N 관계는 양측 모두가 여러 개의 관계를 가지는 경우이다. 예를 들어 게시글과 카테고리를 생각해보면 게시글 하나가 여러 카테고리에 속해있을 수 있고 하나의 카테고리에는 여러 개의 게시글이 있을 수 있다.
일반적으로 DB에서는 이런 상황을 처리하기 위해 중간에 연계 테이블을 만들어서 N:1 관계로 풀어서 처리하는 것이 일반적이다.
https://goodteacher.tistory.com/466/#comment18561464
@ManyToMany
JPA에서는 이를 처리하기 위해 @ManyToMany 애너테이션을 지원한다.
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface ManyToMany {
String mappedBy() default "";
CascadeType[] cascade() default {};
FetchType fetch() default FetchType.LAZY;
}
사용법도 매우 단순하다. 대상 엔티티 모두에게 @ManyToMany를 추가하고 주엔티티인 Post에 mappedBy 속성을 지정하면 된다.
@Entity
public class Category extends BaseEntity{
@Id
@GeneratedValue(strategy =
GenerationType.IDENTITY)
private Long cno;
private String name;
@ManyToMany
@Builder.Default
@ToString.Exclude
private List<Post> posts
= new ArrayList<>();
}
@Entity
public class Post extends BaseEntity {
@Id
@GeneratedValue(strategy =
GenerationType.IDENTITY)
private Long pno;
. . .
@ManyToMany(mappedBy = "posts")
@ToString.Exclude
@Builder.Default
private List<Category> categories
= new ArrayList<>();
}
결과 DDL
위와 같은 설정 결과 생성된 테이블의 구조는 다음과 같다.
더보기
Hibernate:
create table category (
cno bigint generated by default as identity,
name varchar(255),
primary key (cno)
)
Hibernate:
create table category_posts (
categories_cno bigint not null,
posts_pno bigint not null
)
Hibernate:
create table post (
created timestamp(6),
modified timestamp(6),
pno bigint generated by default as identity,
writer bigint,
content varchar(255),
title varchar(255),
primary key (pno)
)
Hibernate:
alter table if exists category_posts
add constraint FKmx347vmsshx3qxcxeu9ruthrk
foreign key (posts_pno)
references post
Hibernate:
alter table if exists category_posts
add constraint FK66viirlv9o5bys7a22r4jq1r9
foreign key (categories_cno)
references category
즉 자동으로 Category_Posts이라는 연계 테이블을 생성하는데 이 테이블은 Post, Category와 각각 1:N의 관계를 맺게 된다. 연계 테이블의 P.K는 Post와 Category에 대한 F.K를 합쳐서 복합키로 사용한다.
언뜻 보면 매우 간단해보이지만 이렇게 사용할 수 있는 경우는 연계 테이블이 단순히 복합키 만 가지는 경우이다. 즉 Category_Posts에 추가일, 수정일 등 추가적으로 필요한 컬럼들이 있다면 사용할 수 없다.
따라서 M:N의 관계에서는 @ManyToMany에 의해 자동으로 생성되는 연계 테이블을 사용하기 보다는 연계 테이블과 엔티티를 직접 생성해서 1:N의 관계를 명시적으로 주는 것을 권장한다.
package com.doding.board.post.model.entity;
import com.doding.board.common.entity.BaseEnttiy;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
public class CategoryPost extends BaseEnttiy {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long cpno;
@ManyToOne
@JoinColumn(name = "pno")
private Post post;
@ManyToOne
@JoinColumn(name = "cno")
private Category category;
}
@Entity
public class Category extends BaseEnttiy{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long cno;
@OneToMany(mappedBy = "category")
@Builder.Default
@ToString.Exclude
private List<CategoryPost> categoryPosts = new ArrayList<>();
}
@Entity
public class Post extends BaseEnttiy {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long pno;
@OneToMany(mappedBy = "post")
@ToString.Exclude
@Builder.Default
private List<CategoryPost> categoryPosts = new ArrayList<>();
}
@ManyToOne은 이전 포스트에서 많이 살펴봤기 때문에 추가적인 설명은 생략한다.