Entity의 연관 관계 - @ManyToMany -
N 대 M 관계
N : M 관계를 맺어주는 역할을 한다.
상품 Entity와 고객 Entity가 N : M 관계라고 가정
단방향 관계
N : M 관계를 풀어내기 위해 중간 테이블(orders)을 생성하여 사용한다.
Product Entity
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@NoArgsConstructor
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
@ManyToMany
@JoinTable(name = "orders", //중간 테이블 생성
joinColumns = @JoinColumn(name = "product_id"), //현재 위치인 Product Entity 에서 중간 테이블로 조인할 컬럼 설정
inverseJoinColumns = @JoinColumn(name = "user_id")) //반대 위치인 User Entity 에서 중간 테이블로 조인할 컬럼 설정
private List<User> userList = new ArrayList<>();
public Product(String name, double price) {
this.name = name;
this.price = price;
}
}
User Entity
package com.practice.practice.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Getter
@Table(name = "users")
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
public User(String name) {
this.name = name;
}
}
현재 위치와 반대의 위치가 서로 N : M 으로서 리스트로 관리되면 문제가 생긴다.
그러므로 중간 테이블을 만들어준다.
생성되는 중간 테이블을 컨트롤 하기 어렵기 때문에 추후에
중간 테이블의 변경이 발생할 경우 문제가 발생할 가능성이 있다.
현업에서 자동적으로 쿼리를 날려 CREATE TABLE 또는 ALTER TABLE 하는 것은
좋아하지 않는다고 한다.
JPA로 자동적으로 생성할 경우 외래키가 난수로 나오기도 하여 관리가 힘들다.
직접 자기 자신이 만드는 것이 좋다.
Product Entity에 추가할 코드
public void addUserList(User user) {
this.userList.add(user);
}
테스트 코드
package com.practice.practice.relation;
import com.practice.practice.entity.Product;
import com.practice.practice.entity.User;
import com.practice.practice.repository.ProductRepository;
import com.practice.practice.repository.UserRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;
@SpringBootTest
@Transactional
public class ManyToManyTest {
@Autowired
ProductRepository productRepository;
@Autowired
UserRepository userRepository;
@Test
@Rollback(value = false)
@DisplayName("N대M 단방향 테스트")
void test1() {
Product product = new Product("곰인형", 15000);
productRepository.save(product);
User user = new User("John");
userRepository.save(user);
User user2 = new User("Kate");
userRepository.save(user2);
product.addUserList(user);
product.addUserList(user2);
}
}
결과
Hibernate:
create table orders (
product_id bigint not null,
user_id bigint not null
) engine=InnoDB
Hibernate:
create table product (
id bigint not null auto_increment,
name varchar(255),
price float(53) not null,
primary key (id)
) engine=InnoDB
Hibernate:
create table users (
id bigint not null auto_increment,
name varchar(255),
primary key (id)
) engine=InnoDB
Hibernate:
alter table orders
add constraint FK32ql8ubntj5uh44ph9659tiih
foreign key (user_id)
references users (id)
Hibernate:
alter table orders
add constraint FK787ibr3guwp6xobrpbofnv7le
foreign key (product_id)
references product (id)
중간테이블인 orders가
ALTER TABLE을 두 번 하여 외래키를 각각의
테이블로부터 받아왔다.
Hibernate:
/* insert com.practice.practice.entity.Product
*/ insert
into
product (name, price)
values
(?, ?)
Hibernate:
/* insert com.practice.practice.entity.User
*/ insert
into
users (name)
values
(?)
Hibernate:
/* insert com.practice.practice.entity.User
*/ insert
into
users (name)
values
(?)
Hibernate:
/* insert collection
row com.practice.practice.entity.Product.userList */ insert
into
orders (product_id, user_id)
values
(?, ?)
Hibernate:
/* insert collection
row com.practice.practice.entity.Product.userList */ insert
into
orders (product_id, user_id)
values
(?, ?)
중간테이블에서 update가 아닌
insert가 실행되었다.
양방향 관계
Product Entity
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@NoArgsConstructor
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
@ManyToMany
@JoinTable(name = "orders", //중간 테이블 생성
joinColumns = @JoinColumn(name = "product_id"), //현재 위치인 Product Entity 에서 중간 테이블로 조인할 컬럼 설정
inverseJoinColumns = @JoinColumn(name = "user_id")) //반대 위치인 User Entity 에서 중간 테이블로 조인할 컬럼 설정
private List<User> userList = new ArrayList<>();
public Product(String name, double price) {
this.name = name;
this.price = price;
}
}
User Entity
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@Table(name = "users")
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany(mappedBy = "userList")
private List<Product> productList = new ArrayList<>();
public User(String name) {
this.name = name;
}
}
외래키의 주인은 Product이고
반대편의 userList와 매칭이 된다는 것을
mappedBy를 통해 알려준다.
테스트 전 User Entity에 추가할 코드
public void addProductList(Product product) {
this.productList.add(product);
}
테스트 코드
@Test
@Rollback(value = false)
@DisplayName("N대M 양방향 테스트")
void test2() {
Product product = new Product("곰인형", 15000);
productRepository.save(product);
User user = new User("John");
userRepository.save(user);
User user2 = new User("Kate");
userRepository.save(user2);
// User Entity 를 통해 Food Entity 를 참조 해보겠습니다.
user.addProductList(product);
user2.addProductList(product);
}
결과
Hibernate:
/* insert com.practice.practice.entity.Product
*/ insert
into
product (name, price)
values
(?, ?)
Hibernate:
/* insert com.practice.practice.entity.User
*/ insert
into
users (name)
values
(?)
Hibernate:
/* insert com.practice.practice.entity.User
*/ insert
into
users (name)
values
(?)
앞전의 테이블을 그대로 사용하였기 때문에
CREATE와 ALTER는 수행하지 않았고
INSERT가 3번 수행되었다.
orders table에는 INSERT가 안되었다.
확인해 보자.
User Entity에서 수정할 코드
public void addProductList(Product product) {
this.productList.add(product);
product.addUserList(this);
}
결과
Hibernate:
/* insert com.practice.practice.entity.Product
*/ insert
into
product (name, price)
values
(?, ?)
Hibernate:
/* insert com.practice.practice.entity.User
*/ insert
into
users (name)
values
(?)
Hibernate:
/* insert com.practice.practice.entity.User
*/ insert
into
users (name)
values
(?)
Hibernate:
/* insert collection
row com.practice.practice.entity.Product.userList */ insert
into
orders (product_id, user_id)
values
(?, ?)
Hibernate:
/* insert collection
row com.practice.practice.entity.Product.userList */ insert
into
orders (product_id, user_id)
values
(?, ?)
orders table에 INSERT가 확인가능하다.
조회 테스트 1
@Test
@DisplayName("N대M 조회 : product 기준 user 정보 조회")
void test3() {
Product product = productRepository.findById(1L).orElseThrow(NullPointerException::new);
// 음식 정보 조회
System.out.println("product.getName() = " + product.getName());
// 음식을 주문한 고객 정보 조회
List<User> userList = product.getUserList();
for (User user : userList) {
System.out.println("user.getName() = " + user.getName());
}
}
결과 1
Hibernate:
select
p1_0.id,
p1_0.name,
p1_0.price
from
product p1_0
where
p1_0.id=?
product.getName() = 곰인형
Hibernate:
select
u1_0.product_id,
u1_1.id,
u1_1.name
from
orders u1_0
join
users u1_1
on u1_1.id=u1_0.user_id
where
u1_0.product_id=?
user.getName() = John
user.getName() = Kate
from orders u1_o
이 있는 것을 보아
orders table이 있다는 것을 알고 있다.
join users u1_1.id=u1_o.user_id
에서
orders table
의 user_id
와
users table의 user_id
가 조인이 되고 있다.
where u1_0.prodcut_id=?
에서 유저 1의 product_id
를 WHERE 조건문으로 주었다.
따라서 "곰인형"을 주문한
"John"과 "Kate"가 잘 조회되고 있다.
조회테스트 2
@Test
@DisplayName("N대M 조회 : User 기준 product 정보 조회")
void test4() {
User user = userRepository.findById(1L).orElseThrow(NullPointerException::new);
// 고객 정보 조회
System.out.println("user.getName() = " + user.getName());
// 해당 고객이 주문한 음식 정보 조회
List<Product> productList = user.getProductList();
for (Product product : productList) {
System.out.println("product.getName() = " + product.getName());
System.out.println("product.getPrice() = " + product.getPrice());
}
}
결과 2
Hibernate:
select
u1_0.id,
u1_0.name
from
users u1_0
where
u1_0.id=?
user.getName() = John
Hibernate:
select
p1_0.user_id,
p1_1.id,
p1_1.name,
p1_1.price
from
orders p1_0
join
product p1_1
on p1_1.id=p1_0.product_id
where
p1_0.user_id=?
product.getName() = 곰인형
product.getPrice() = 15000.0
위와 마찬가지로
product table과 orders table의 product_id
가
JOIN 되고 있다.
orders table에 있는 product_id
를 조회하여
"곰인형"과 "가격"이 조회되고 있다.
중간 테이블을 직접 생성
중간 테이블 orders를 직접 생성하여 관리하면
변경 발생 시 컨트롤하기 쉽기 때문에
확장성 및 유지보수에 유리하다.
Order Entity(중간 테이블)
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Getter
@Table(name = "orders")
@NoArgsConstructor
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long Id;
@ManyToOne
@JoinColumn(name = "product_id")
private Product product;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
}
주문 : 상품의 관계
N : 1 관계
주문 : 고객의 관계
N : 1 관계
외래 키는 orders table이 관리하는 형태이다.
Product Entity
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@NoArgsConstructor
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
@OneToMany(mappedBy = "product")
private List<Order> orderList = new ArrayList<>();
public Product(String name, double price) {
this.name = name;
this.price = price;
}
}
상품 : 주문의 관계
1 : N 관계
User Entity
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@Table(name = "users")
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "user")
private List<Order> orderList = new ArrayList<>();
public User(String name) {
this.name = name;
}
}
고객 : 주문의 관계
1 : N 관계
스프링 부트로 run 해보기
Hibernate:
create table orders (
id bigint not null auto_increment,
product_id bigint,
user_id bigint,
primary key (id)
) engine=InnoDB
Hibernate:
create table product (
id bigint not null auto_increment,
name varchar(255),
price float(53) not null,
primary key (id)
) engine=InnoDB
Hibernate:
create table users (
id bigint not null auto_increment,
name varchar(255),
primary key (id)
) engine=InnoDB
Hibernate:
alter table orders
add constraint FK787ibr3guwp6xobrpbofnv7le
foreign key (product_id)
references product (id)
Hibernate:
alter table orders
add constraint FK32ql8ubntj5uh44ph9659tiih
foreign key (user_id)
references users (id)
orders table, product table, user table이 생성되었고
product table과 user table로부터 외래키를 받아왔다.
이전과는 다른 점은 orders table이 직접 생성해 주어
oeders table이 pk도 가지고 있다.
따라서 orders table에 추가적인 pk설정을 할 필요가 없다.
가장 큰 장점은 확장성이다.
extends 등을 하여 확장에도 좋다.
내용 참고 - 스파르타 코딩클럽 -
'자바 탐구' 카테고리의 다른 글
스프링) SpringSecurity - 2) SecurityConfig 설정 (0) | 2023.05.26 |
---|---|
스프링) SpringSecurity - 1) Security, Mustache 환경 설정 (0) | 2023.05.26 |
JPA) Entity의 연관 관계 - @OneToMany - (0) | 2023.05.03 |
JPA) Entity의 연관 관계 - @ManyToOne - (0) | 2023.05.03 |
JPA) Entity의 연관 관계 - @OneToOne - (0) | 2023.05.02 |