Everyday Dev System

@ManyToMany 양방향 관계 이해하기 본문

내배캠 주요 학습/Spring 숙련

@ManyToMany 양방향 관계 이해하기

chaeyoung- 2023. 6. 21. 15:13
중간에 생긴 orders 테이블에 PK도 없고 직접 생성한 것이 아니라 자동으로 생긴 테이블이므로
추가 변경 사항이 발생할 경우 관리하기가 힘들다는 단점이 있다.

위의 단점을 해결할 수 있는 방법

다음 링크 참조:

 

 

 

단방향 이전 글 참조: 

2023.06.21 - [내배캠 주요 학습/Spring 심화] - @ManyToMany 단방향 관계 이해하기

 

 

N : M 관계 양방향 전체 Code :

package com.sparta.jpaadvance.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@Setter
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @ManyToMany
    @JoinTable(name = "orders", // 중간 테이블 생성
            joinColumns = @JoinColumn(name = "food_id"), // 현재 위치인 Food Entity 에서 중간 테이블로 조인할 컬럼 설정
            inverseJoinColumns = @JoinColumn(name = "user_id")) // 반대 위치인 User Entity 에서 중간 테이블로 조인할 컬럼 설정
    private List<User> userList = new ArrayList<>();

    public void addUserList(User user) {
        this.userList.add(user); // 외래 키(연관 관계) 설정
        user.getFoodList().add(this);
    }
}
package com.sparta.jpaadvance.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@Setter
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @ManyToMany(mappedBy = "userList")
    private List<Food> foodList = new ArrayList<>();

    public void addFoodList(Food food) {
        this.foodList.add(food);
        food.getUserList().add(this); // 외래 키(연관 관계) 설정
    }
}
package com.sparta.jpaadvance.relation;

import com.sparta.jpaadvance.entity.Food;
import com.sparta.jpaadvance.entity.User;
import com.sparta.jpaadvance.repository.FoodRepository;
import com.sparta.jpaadvance.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;

@Transactional
@SpringBootTest
public class ManyToManyTest {

    @Autowired
    UserRepository userRepository;
    @Autowired
    FoodRepository foodRepository;

    @Test
    @Rollback(value = false)
    @DisplayName("N대M 양방향 테스트 : 외래 키 저장 실패")
    void test2() {

        Food food = new Food();
        food.setName("후라이드 치킨");
        food.setPrice(15000);

        Food food2 = new Food();
        food2.setName("양념 치킨");
        food2.setPrice(20000);

        // 외래 키의 주인이 아닌 User 에서 Food 를 저장해보겠습니다.
        User user = new User();
        user.setName("Robbie");
        user.getFoodList().add(food);
        user.getFoodList().add(food2);

        userRepository.save(user);
        foodRepository.save(food);
        foodRepository.save(food2);

        // 확인해 보시면 orders 테이블에 food_id, user_id 값이 들어가 있지 않은 것을 확인하실 수 있습니다.
    }

    @Test
    @Rollback(value = false)
    @DisplayName("N대M 양방향 테스트 : 외래 키 저장 실패 -> 성공")
    void test3() {

        Food food = new Food();
        food.setName("후라이드 치킨");
        food.setPrice(15000);

        Food food2 = new Food();
        food2.setName("양념 치킨");
        food2.setPrice(20000);

        // 외래 키의 주인이 아닌 User 에서 Food 를 쉽게 저장하기 위해 addFoodList() 메서드를 생성해서 사용합니다.
        // 외래 키(연관 관계) 설정을 위해 Food 에서 userList 를 호출해 user 객체 자신을 add 합니다.
        User user = new User();
        user.setName("Robbie");
        user.addFoodList(food);
        user.addFoodList(food2);


        userRepository.save(user);
        foodRepository.save(food);
        foodRepository.save(food2);
    }

    @Test
    @Rollback(value = false)
    @DisplayName("N대M 양방향 테스트")
    void test4() {

        User user = new User();
        user.setName("Robbie");

        User user2 = new User();
        user2.setName("Robbert");


        Food food = new Food();
        food.setName("아보카도 피자");
        food.setPrice(50000);
        food.getUserList().add(user); // 외래 키(연관 관계) 설정
        food.getUserList().add(user2); // 외래 키(연관 관계) 설정

        Food food2 = new Food();
        food2.setName("고구마 피자");
        food2.setPrice(30000);
        food2.getUserList().add(user); // 외래 키(연관 관계) 설정

        userRepository.save(user);
        userRepository.save(user2);
        foodRepository.save(food);
        foodRepository.save(food2);

        // User 를 통해 food 의 정보 조회
        System.out.println("user.getName() = " + user.getName());

        List<Food> foodList = user.getFoodList();
        for (Food f : foodList) {
            System.out.println("f.getName() = " + f.getName());
            System.out.println("f.getPrice() = " + f.getPrice());
        }

        // 외래 키의 주인이 아닌 User 객체에 Food 의 정보를 넣어주지 않아도 DB 저장에는 문제가 없지만
        // 이처럼 User 를 사용하여 food 의 정보를 조회할 수는 없습니다.
    }

    @Test
    @Rollback(value = false)
    @DisplayName("N대M 양방향 테스트 : 객체와 양방향의 장점 활용")
    void test5() {

        User user = new User();
        user.setName("Robbie");

        User user2 = new User();
        user2.setName("Robbert");


        // addUserList() 메서드를 생성해 user 정보를 추가하고
        // 해당 메서드에 객체 활용을 위해 user 객체에 food 정보를 추가하는 코드를 추가합니다. user.getFoodList().add(this);
        Food food = new Food();
        food.setName("아보카도 피자");
        food.setPrice(50000);
        food.addUserList(user);
        food.addUserList(user2);

        Food food2 = new Food();
        food2.setName("고구마 피자");
        food2.setPrice(30000);
        food2.addUserList(user);


        userRepository.save(user);
        userRepository.save(user2);
        foodRepository.save(food);
        foodRepository.save(food2);

        // User 를 통해 food 의 정보 조회
        System.out.println("user.getName() = " + user.getName());

        List<Food> foodList = user.getFoodList();
        for (Food f : foodList) {
            System.out.println("f.getName() = " + f.getName());
            System.out.println("f.getPrice() = " + f.getPrice());
        }
    }
}

 

 

 

 

중요한 코드 :

Food.java 코드는 단방향 코드와 같으나, addUserList(User user) 메서드가 추가되었다.

@ManyToMany(mappedBy = "userList")
User.java에서는 양방향이므로 FK 주인 Entity를 위 코드를 통해 설정한다.

private List<Food> foodList = new ArrayList<>();
private List<User> userList = new ArrayList<>();
N : M 관계이므로 Food.java와 User.java 모두 상대 타입의  List를 필드로 갖습니다.

Food.java

    @ManyToMany
    @JoinTable(name = "orders", // 중간 테이블 생성
            joinColumns = @JoinColumn(name = "food_id"), // 현재 위치인 Food Entity 에서 중간 테이블로 조인할 컬럼 설정
            inverseJoinColumns = @JoinColumn(name = "user_id")) // 반대 위치인 User Entity 에서 중간 테이블로 조인할 컬럼 설정
    private List<User> userList = new ArrayList<>();

    public void addUserList(User user) {
        this.userList.add(user); // 외래 키(연관 관계) 설정
        user.getFoodList().add(this);
    }

 

User.java

    @ManyToMany(mappedBy = "userList")
    private List<Food> foodList = new ArrayList<>();

    public void addFoodList(Food food) {
        this.foodList.add(food);
        food.getUserList().add(this); // 외래 키(연관 관계) 설정
    }

 

 

 

저장 테스트 코드 : 

    @Test
    @Rollback(value = false)
    @DisplayName("N대M 양방향 테스트 : 외래 키 저장 실패")
    void test2() {

        Food food = new Food();
        food.setName("후라이드 치킨");
        food.setPrice(15000);

        Food food2 = new Food();
        food2.setName("양념 치킨");
        food2.setPrice(20000);

        // 외래 키의 주인이 아닌 User 에서 Food 를 저장해보겠습니다.
        User user = new User();
        user.setName("Robbie");
        user.getFoodList().add(food);
        user.getFoodList().add(food2);

        userRepository.save(user);
        foodRepository.save(food);
        foodRepository.save(food2);

        // 확인해 보시면 orders 테이블에 food_id, user_id 값이 들어가 있지 않은 것을 확인하실 수 있습니다.
    }

해당 코드는 orders Table에 insert가 실행되지 않습니다.

 

 

해결 방법 : 

아래 테스트 코드로 하면 orders Table에 insert가 잘 실행된다.

    @Test
    @Rollback(value = false)
    @DisplayName("N대M 양방향 테스트 : 외래 키 저장 실패 -> 성공")
    void test3() {

        Food food = new Food();
        food.setName("후라이드 치킨");
        food.setPrice(15000);

        Food food2 = new Food();
        food2.setName("양념 치킨");
        food2.setPrice(20000);

        // 외래 키의 주인이 아닌 User 에서 Food 를 쉽게 저장하기 위해 addFoodList() 메서드를 생성해서 사용합니다.
        // 외래 키(연관 관계) 설정을 위해 Food 에서 userList 를 호출해 user 객체 자신을 add 합니다.
        User user = new User();
        user.setName("Robbie");
        user.addFoodList(food);
        user.addFoodList(food2);


        userRepository.save(user);
        foodRepository.save(food);
        foodRepository.save(food2);
    }
Hibernate: 
    /* insert for
        com.sparta.jpaadvance.entity.User */insert 
    into
        users (name) 
    values
        (?)
Hibernate: 
    /* insert for
        com.sparta.jpaadvance.entity.Food */insert 
    into
        food (name,price) 
    values
        (?,?)
Hibernate: 
    /* insert for
        com.sparta.jpaadvance.entity.Food */insert 
    into
        food (name,price) 
    values
        (?,?)
Hibernate: 
    /* insert for
        com.sparta.jpaadvance.entity.Food.userList */insert 
    into
        orders (food_id,user_id) 
    values
        (?,?)
Hibernate: 
    /* insert for
        com.sparta.jpaadvance.entity.Food.userList */insert 
    into
        orders (food_id,user_id) 
    values
        (?,?)

 

 

 

 

조회하기 :

 

1. Food 기준 User 정보 조회

    @Test
    @DisplayName("N대M 조회 : Food 기준 user 정보 조회")
    void tests() {
        Food food = foodRepository.findById(1L).orElseThrow(NullPointerException::new);

        System.out.println("food.getName() = " + food.getName());

        List<User> userList = food.getUserList();
        for(User user : userList){
            System.out.println("user.getId() = " + user.getId());
            System.out.println("user.getName() = " + user.getName());
        }
    }

food 객체를 생성후 DB에서 해당 id에 따른 food 데이터를 가져온다.

가져온 food 데이터의 user 데이터를 가져온다.

List의 형태로 가져온 user 데이터를 for()문을 통해 출력한다.

Hibernate: 
    select
        f1_0.id,
        f1_0.name,
        f1_0.price 
    from
        food f1_0 
    where
        f1_0.id=?
food.getName() = 아보카도 피자
Hibernate: 
    select
        u1_0.food_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.food_id=?
user.getId() = 1
user.getName() = Robbie
user.getId() = 2
user.getName() = Robbert

 

2. User 기준 Food 정보 조회

    @Test
    @DisplayName("N대M 조회 : user 기준 Food 정보 조회")
    void tests() {
        User user = userRepository.findById(1L).orElseThrow(NullPointerException::new);

        System.out.println("user.getName() = " + user.getName());

        List<Food> foodList = user.getFoodList();
        for(Food food : foodList){
            System.out.println("food.getId() = " + food.getId());
            System.out.println("food.getName() = " + food.getName());
            System.out.println("food.getPrice() = " + food.getPrice());
        }
    }

user 객체를 생성후 DB에서 해당 id에 따른 user 데이터를 가져온다.

가져온 user 데이터의 food 데이터를 가져온다.

List의 형태로 가져온 food 데이터를 for()문을 통해 출력한다.

Hibernate: 
    select
        u1_0.id,
        u1_0.name 
    from
        users u1_0 
    where
        u1_0.id=?
user.getName() = Robbie
Hibernate: 
    select
        f1_0.user_id,
        f1_1.id,
        f1_1.name,
        f1_1.price 
    from
        orders f1_0 
    join
        food f1_1 
            on f1_1.id=f1_0.food_id 
    where
        f1_0.user_id=?
food.getId() = 1
food.getName() = 아보카도 피자
food.getPrice() = 50000.0
food.getId() = 2
food.getName() = 고구마 피자
food.getPrice() = 30000.0