Spring 입문 4-2 (Entity Relationship 1)

SJ.CHO·2024년 10월 11일

Entity Relationship

  • DB에서 정보를 가진 Table들은 연관관계를 지님.

  • 두 테이블에서 주문을 받을때 저장이 되야하는 곳은 어디인가?

  • 고객 테이블

    • 고객과 음식은 1:N 의 관계를 가진다.
    • 해당 형태로 저장이 된다면 불필요한 중복Row가 발생.
  • 음식 테이블

    • 음식과 고객은 1:N의 관계를 가짐.
    • 위에서와 동일한 문제가 발생한다.

  • 하나의 ROW에서 관리를 하면 되지않을까? 라는 생각이 들수도 있지만. 현실적인 상황에선 불가능에 가깝다.

중간관계를 지니는 테이블 생성

  • 고객정보와 음식정보를 합치는 중간 테이블인 주문 테이블을 생성하여 연관관계를 해결.

    • 고객 : 음식 = 1 : N 관계
    • 음식 : 고객 = 1 : N 관계
    • 고객 : 음식 = N : M 관계
      을 가질때 해결하기 위해 중간테이블을 사용한다.
  • 고객 1명은 주문을 여러번 할 수 있다.

    • 고객 : 주문 = 1 : N
  • 음식 1개는 주문이 여러번 될 수 있다.

    • 음식 : 주문 = 1 : N

데이터의 방향성

DB에서의 방향성

  • 단방향 : 하나의 테이블에서만 참조가 가능.

  • 양방향 : 서로 연관관계를 가지는 테이블끼리 참조가 가능.

  • DB에서는 JOIN을 통해 서로의 데이터를 확인하고 검증이 가능하기에 방향성이 존재하지않는다.

    • 2개의 쿼리문은 모두 같은 결과가 나옴.

JPA 에서의 방향성

  • 양방향 N:1 관계
@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<>();
}
  • 2개의 Entity가 서로의 객체에대한 정보를 지닐수 있는 공간을 가지고있음.

  • DB는 JOIN을 통해 서로의 정보를 조회가 가능하지만.
    JPA 내에서는 실질적인 정보객체를 지니지않는다면 그 정보를 참조할수있는 방법이 존재하지않음.

  • DB내에 실제 컬럼으로서는 존재하지않지만 Entity를 참조하기위해 사용한다.

  • List를 가지는 이유는 N의 관계를 표현하기위해서.

    (해당사진의 user_id 중첩에 대한 방법)

  • 단방향 관계

@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;
}
  • 음식 Entity는 유저를 참조 가능 하지만 유저 Entity는 음식을 참조할 방법이 없음.

정리

  • DB에서는 연관관계를 FK 를 통해 지어주고 JOIN을 통해 방향성의 상관없이 조회가 가능하다.
  • JPA에서는 상대정보를 참조할수가 없다면 아예 조회가 불가능하기에 방향성의 개념이 생성된다.

1:1 관계

  • 1:1 관계에서는 외래키의 주인을 직접 지정해줘야 함
  • 외래키의 주인 만이 외래키를 등록,수정,삭제 가 가능하며 아닌 쪽은 조회만 가능
  • @JoinColumn() 을 통해 외래키의 주인 지정가능
  • @OneToOne 어노테이션을 통해 관계지정

단방향

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

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

양방향

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

    @OneToOne
    @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;

    @OneToOne(mappedBy = "user")
    private Food food;
}
  • 양방향 관계에서는 mappedBy 옵션을 통해 설정
  • mappedBy의 속성값은 외래 키의 주인인 상대 Entity의 필드명을 의미.
  • 반드시 외래키의 주인이 가지고있는 필드명을 맵핑해야한다. (클래스명이 아니다!)

주의할점

  • 외래 키의 주인 Entity에서 @JoinColumn()을 생략이가능
  • 다만 1:N관계에서 주인 Entity가 @JoinColumn()을 생략한다면 JPA는 외래키가 저장될 컬럼파악이 불가능하기에 의도치않은 중간테이블이 생성된다. (웬만하면 사용하자)
  • 양방향 관계에서 mappedBy 옵션을 생략할 경우 JPA는 외래키의 주인 Entity 파악이 불가능하다. (양방향 관계에서는 반드시 설정하자)
    @Test
    @Rollback(value = false) // 테스트에서는 @Transactional 에 의해 자동 rollback 됨으로 false 설정해준다.
    @DisplayName("1대1 단방향 테스트")
    void test1() {

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

        // 외래 키의 주인인 Food Entity user 필드에 user 객체를 추가해 줍니다.
        Food food = new Food();
        food.setName("후라이드 치킨");
        food.setPrice(15000);
        food.setUser(user); // 외래 키(연관 관계) 설정

        userRepository.save(user);
        foodRepository.save(food);
    }
  • 외래키의 주인인 Food 가 User를 저장하고있기에 성공
@Test
@DisplayName("1대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());
}

@Test
@DisplayName("1대1 조회 : User 기준 food 정보 조회")
void test6() {
    User user = userRepository.findById(1L).orElseThrow(NullPointerException::new);
    // 고객 정보 조회
    System.out.println("user.getName() = " + user.getName());

    // 해당 고객이 주문한 음식 정보 조회
    Food food = user.getFood();
    System.out.println("food.getName() = " + food.getName());
    System.out.println("food.getPrice() = " + food.getPrice());
}

N:1 관계

  • N 의 관계에선 일반적으로 다수의 관계인 Entity가 외래키의 주인을 가짐
  • @ManyToOne 을 통해 관계를 맺어준다.

단방향

@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;
}
  • 일반적인 1:1 관계와 크게 다르지않은 모습.

양방향 관계

@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<>();
}
  • N의 관계를가지는 Food의 대해 Java Collenction 을 통해 객체'들'을 저장 할 수 있도록 지정.
@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());
}

@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());
    }
}
  • 1개의 단건조회의 경우 기존의 개쳋의 정보를 가져오든 Get메소드를 통해 조회.
  • N개의 객체를 가져오기위해 List를 통한 반복문 조회등으로 가져올수있다.

1:N 관계

  • @OneToMany 를 활용하여 관계를 표현.
  • 일반적으론 N쪽이 외래키의 주인이지만 1쪽이 외래키의 주인이어야할 경우 사용.

단방향

  • 관계도에선 음식이 외래키의 주인의 형태를 가지지만 실제 외래키는 고객이 가지고 있음.
@Entity
@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
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
}
  • 기존의 방식처럼 외래키를 음식이 가진다면 Insert 한번에 해결이 가능하지만 DB상에선 외래키를 고객이 지니고 있기에 추가적인 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;
}
  • @JoinColum의 insertable 과 updatable 옵션을 false로 설정하여 양방향형태처럼 구현은 가능.
    @Test
    @DisplayName("1대N 조회 테스트")
    void test2() {
        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());
        }
    }
profile
70살까지 개발하고싶은 개발자

0개의 댓글