본문 바로가기

내일배움캠프

Spring 숙련주차 Part3

1. N:1관계

N:1 단방향 관계:

 

테이블 예시

테이블 예시처럼 외래키의 주인은 음식 테이블이지만 실제로 외래키가 저장되는 곳은 고객 테이블이다.

1 : N에서 N 관계의 테이블이 외래 키를 가질 수 있기 때문에 외래 키는 N 관계인 users 테이블에 외래 키 컬럼을 만들어 추가하지만 외래 키의 주인인 음식 Entity를 통해 관리한다.

 

음식 엔티티(외래키 주인):

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

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;
}

고객 엔티티:

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

 

N:1 양방향 관계:

테이블 예시

양방향 참조를 위해 고객 Entity에서 Java 컬렉션을 사용하여 음식 Entity 참조함

 

음식 엔티티(외래 키 주인):

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

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;
}

고객 엔티티:

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

    @OneToMany(mappedBy = "user")
    private List<Food> foodList = new ArrayList<>();
}

 

외래키 저장 예시:

@Test
@Rollback(value = false)
@DisplayName("N대1 양방향 테스트 : 외래 키 저장 실패")
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);

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

실행 결과

외래 키 주인이 아닌 entity에서 외래 키를 저장하지 않도록 유의해야 한다.

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

외래 키 주인이 아닌 entity에서 저장이 되게 하려면 해당 entity에 다음과 같은 코드를 추가해야 한다.

 

외래 키 조회 예시:

@Test
@DisplayName("N대1 조회 : Food 기준 user 정보 조회")
void test5() {
    Food food = foodRepository.findById(1L).orElseThrow(NullPointerException::new);
    // 음식 정보 조회
    System.out.println("food.getName() = " + food.getName());

    // 음식을 주문한 고객 정보 조회
    System.out.println("food.getUser().getName() = " + food.getUser().getName());
}

실행되는 hibernate

@Test
@DisplayName("N대1 조회 : User 기준 food 정보 조회")
void test6() {
    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());
    }
}

실행되는 hibernate

이렇게 어느 테이블을 기준으로 정보를 조회하느냐에 따라 실행되는 sql이 달라진다.

 

2. 1:N 관계

1:N 단방향 관계:

테이블 예시

외래 키를 관리하는 주인은 음식 Entity이지만 실제 외래 키는 고객 Entity에 있다. 

1:N에서 N 관계의 테이블이 외래 키를 가질 수 있기 때문에 외래 키는 N 관계인 users 테이블에 외래 키 컬럼을 만들어 추가하지만 외래 키의 주인인 음식 Entity를 통해 관리한다.

음식 엔티티(외래 키 주인):

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

    @OneToMany
    @JoinColumn(name = "food_id") // users 테이블에 food_id 컬럼
    private List<User> userList = new ArrayList<>();
}

고객 엔티티:

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

 

외래키 저장 예시:

@Test
@Rollback(value = false)
@DisplayName("1대N 단방향 테스트")
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);

    // 추가적인 UPDATE 쿼리 발생을 확인할 수 있습니다.
}

실제 외래키 위치와 외래키 주인의 위치가 서로 다르기 때문에 추가적으로 update쿼리를 수행하는 문제가 있다.

 

1:N관계에 양방향 관계는 따로 존재하지 않는다. @ManyToOne 애너테이션은 mappedBy옵션이 없어서 양방향 관계를 만들 수 없다.

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

    @ManyToOne
    @JoinColumn(name = "food_id", insertable = false, updatable = false)
    private Food food;
}

N 관계의 Entity인 고객 Entity에서 @JoinColum의 insertable 과 updatable 옵션을 false로 설정하여 양쪽으로 JOIN 설정을 하면 양방향처럼 만들 수는 있다.

3. N:M 관계

N:M 단방향 관계:

테이블 예시

N : M 관계를 풀어내기 위해 중간 테이블(orders)을 생성하여 사용한다. 물론 중간 테이블 엔티티를 별도로 만드는 게 아니라 외래 키의 주인쪽 엔티티에 어떤 형태의 중간 테이블을 만들지 명시한다.

음식(외래키 주인):

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

고객:

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

 

외래키 저장 코드:

@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 됨을 확인할 수 있습니다.
}

N:M 양방향 관계:

테이블 예시

음식(외래키 주인):

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

고객:

@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); // 외래 키(연관 관계) 설정
    }
}

외래 키 저장코드:

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

users

orders

food

외래 키 주인이 아닌 엔티티에서 외래키를 저장하려 하면 이렇게 중간 테이블에 정상적으로 레코드가 생성되지 않는다.

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

만약 외래키를 조작하려 한다면 다른 관계들 다룰 때 처럼 외래 키를 조작하는 메소드를 추가해야 한다.

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

데이터를 조회만 하는거라면 외래키의 주인이든 아니든 상관없다.

 

중간 테이블:

중간 테이블과 관련된 엔티티를 직접 생성해서 N:M관계를 구현할 수도 있다. 이러면 테이블의 구조가 바뀌어도 컨트롤하기 쉽기 때문에 확장성에 좋다.

테이블 예시

음식:

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

    @OneToMany(mappedBy = "food")
    private List<Order> orderList = new ArrayList<>();
}

고객:

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

    @OneToMany(mappedBy = "user")
    private List<Order> orderList = new ArrayList<>();
}

주문(외래키 주인):

@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "food_id")
    private Food food;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;
}

 

'내일배움캠프' 카테고리의 다른 글

행렬 간 덧셈  (0) 2024.05.28
Java의 정규표현식  (0) 2024.05.27
Spring 숙련주차 Part2  (0) 2024.05.24
Spring 숙련주차 Part1  (0) 2024.05.22
Spring 입문주차 2주차  (0) 2024.05.21