다오의 개발일지

TIL-22 스프링 연관관계 N:M 관계 1번째 방법 본문

WTIL

TIL-22 스프링 연관관계 N:M 관계 1번째 방법

다오__ 2023. 7. 13. 22:08

JPA 연관관계 맵핑에서 N:M 관계인 경우 2가지 방법으로 만들 수 있다.

 

1번째 방법의 단방향, 양방향, 테스트를 먼저 해보자

N:M 관계의 단방향

예로 users테이블과 food 테이블이 있다고 하자 food테이블을 외래키의 주인으로 둔다.

이런 경우 주문(orders)이라는 테이블을 만들어 N:M을 1:N 1:N상태로 만든다.

 

@Entity
@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<>();
}

위의 코드블럭은 food entity이다. food entity는 외래키의 주인이므로 여기서 orders테이블을 만들 수가 있다.

 

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

위의 코드블럭은 users entity이다 단방향이므로 users테이블에는 추가로 할 작업이 없다.

 

하지만.. 이렇게 작성하는 경우 생성되는 중간 테이블을 컨트롤하기 어렵기 때문에

추후에 중간 테이블의 변경이 발생할 경우 문제가 발생할 가능성이 있다.

 

다른 또 하나의 방법을 알아보기 전에 양방향도 한번 알아보자

 

food entity클래스는 위의 코드와 똑같으므로 생략.

@Entity
@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<>();
}

위의 코드블럭은 users entity이다 양방향이므로 mappedBy가 외래키의 주인 필드명으로 설정되어있다. 

 

반대 방향인 고객 Entity에 @ManyToMany 로 음식 Entity를 연결하고 mappedBy 옵션을 설정하여 외래 키의 주인을 설정하면 양방향 관계 맺음이 가능하다.

 

 

 

단방향 테스트

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

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

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

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

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

    // 자동으로 중간 테이블 orders 가 create 되고 insert 됨을 확인할 수 있습니다.
}

 

위의 테스트는 유저 2명이 음식 1개를 주문하는 코드이다. 자동으로 orders테이블이 만들어지고 insert된다.

 

양방향 테스트

@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 값이 들어가 있지 않은 것을 확인하실 수 있습니다.
}

위의 코드블럭은 N:M양방향 테스트중 외래키의 주인이 아닌 user 테이블에서 음식을 추가하는 경우이다.

외래키의 주인이 아닌경우에는 수정 및 삭제가 불가능하다 오직 읽기만 가능하다.

 

@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);
}

외래키의 주인이 아니지만 food를 저장하고싶을 때는 addFoodList()란 메서드를 정의하여 추가할 수 있다.

 

user entity 내부

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

 

 

 

@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 의 정보를 조회할 수는 없습니다.
}

user 객체에 food 정보를 넣어주지 않았기 때문에 아래부분의 for문은 출력이 되지 않는다.

 

 

 

@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 entity 내부에 addUserList() 메서드를 추가해 user 정보를 추가한다.

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

아까랑 다르게 for문은 온전히 출력이 된다.

 

 

조회 테스트

@Test
@DisplayName("N대M 조회 : Food 기준 user 정보 조회")
void test6() {
    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.getName() = " + user.getName());
    }
}

@Test
@DisplayName("N대M 조회 : User 기준 food 정보 조회")
void test7() {
    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.getName() = " + food.getName());
        System.out.println("food.getPrice() = " + food.getPrice());
    }
}