240821 내일배움캠프 백엔드 Java 6기 TIL : Spring 숙련강의 Summary : Entity 연간관계

박대현·2024년 8월 21일
0

JPA 한 걸음 더 나아가기

Entity 연관관계

DB table간의 방향

  • DB에서는 어떤 테이블을 기준으로 하든 원하는 정보를 JOIN을 사용하여 조회할 수 있다.
    • 이처럼 DB 테이블간의 관계에서는 방향의 개념이 없습니다.(JOIN하면 어차피 양방향이라서 따질 필요가 없다고 이해함)

Entity간의 방향

  • DB에서는 JOIN으로 표현이 가능하지만, Entity에선 관계를 가진 상대Entity를 필드값으로 가지고 있지 않으면 표현할 방법이 없음
    • 여기서, '0대0 관계'와 별개로, 상대Entity를 필드값으로 가지고 있느냐 아니냐에 따라 단방향,양방향으로 나뉨
  • 외래 키 주인만이 외래 키 를 등록, 수정, 삭제할 수 있으며, 주인이 아닌 쪽은 오직 외래 키를 읽기만 가능합니다.
  • @JoinColumn() : 외래키의 주인에 해당하는 Entity에서 쓰는 애너테이션

단방향 관계

  • 외래 키의 주인만 상대 Entity 타입의 필드를 가지면서 @JoinColumn()을 활용하여 외래 키의 속성을 설정

양방향 관계

  • 외래 키의 주인만 상대 Entity 타입의 필드를 가지면서 @JoinColumn()을 활용하여 외래 키의 속성을 설정
  • 더하여, 상대 Entity는 외래 키의 주인 Entity 타입의 필드를 가지면서 mappedBy 옵션을 사용하여 속성 값으로 외래 키의 주인 Entity에 선언된 @JoinColumn()으로 설정되고 있는 필드명을 넣어주면 됩니다.

1대1 관계(@OneToOne)

연습하기 : 1:1 & Food Entity가 외래키의 주인 & 양방향

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

N대1 관계(@ManyToOne)

연습하기 : N대1 & Food Entity가 외래키의 주인 & 양방향

  • 양방향 참조를 위해 고객 Entity에서 Java 컬렉션(List)을 사용하여 음식 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<>();
}

1대N 관계(@OneToMany)

  • 외래 키를 관리하는 주인은 음식 Entity이지만 실제 외래 키는 고객 Entity가 가지고 있다.
    • 1 : N에서 N 관계의 테이블이 외래 키를 가질 수 있기 때문에, 외래 키는 N 관계인 users 테이블에 외래 키 컬럼을 만들어 추가
    • 하지만 외래 키의 주인인 음식 Entity를 통해 관리
      • 외래 키를 음식 Entity가 직접 가질 수 있다면 INSERT 발생 시 한번에 처리할 수 있지만 실제 DB에서 외래 키를 고객 테이블이 가지고 있기 때문에 추가적인 UPDATE가 발생된다는 단점이 존재
  • 양방향 관계 존재 X(@ManyToOne 애너테이션은 mappedBy 속성을 제공X)

연습하기

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

N대M 관계(@ManyToMany)

  • 애너테이션으로 생성되는 중간 테이블을 컨트롤하기 어렵기 때문에 추후에 중간 테이블의 변경이 발생할 경우 문제가 발생할 가능성 존재

연습하기

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

중간 테이블

  • 중간 테이블 orders를 직접 생성하여 관리하면 변경 발생 시 컨트롤하기 쉽기 때문에 확장성에 좋음
@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;
}

지연 로딩

  • 음식 : 고객 = N : 1 가정
@Test
@DisplayName("아보카도 피자 조회")
void test1() {
    Food food = foodRepository.findById(2L).orElseThrow(NullPointerException::new);

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

    System.out.println("아보카도 피자를 주문한 회원 정보 조회");
    System.out.println("food.getUser().getName() = " + food.getUser().getName());
}
  • getName()은 Food테이블만 있어도 됨에도 불구하고, 자동으로 JOIN문을 사용하여 User테이블 정보도 가져옴

지연 로딩과 즉시 로딩

  • 지연로딩 : 애너테이션이 Many로 끝나는 경우의 default
    • @OneToMany(fetch = FetchType.LAZY), @ManyToMany(fetch = FetchType.LAZY)
    • Many로 끝난다는 말은 Many에 해당하는 상대 Entity 정보가 여러개라는 의미이기 때문에, 지연로딩이 효율적
  • 즉시로딩 : 애너테이션이 One으로 끝나는 경우의 default
    • @OneToOne(fetch = FetchType.EAGER), @ManyToOne(fetch = FetchType.EAGER)
    • Many로 끝난다는 말은 Many에 해당하는 상대 Entity 정보가 단건이라는 의미이기 때문에, 즉시로딩이 효율적

영속성 컨텍스트와 지연로딩

  • 지연로딩된 entity의 정보를 조회하려할때는
    • 반드시 영속성 컨텍스트가 존재해야한다. == 트랜잭션이 걸려있는 상태여야한다

영속성 전이

  • 영속 상태의 Entity에서 수행되는 작업들이 연관된 Entity까지 전파되는 상황

CASCADE : PERSIST

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

    @OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST)
    private List<Food> foodList = new ArrayList<>();

		public void addFoodList(Food food) {
			  this.foodList.add(food);
			  food.setUser(this);// 외래 키(연관 관계) 설정
		}
}
  • 영속성 전이를 적용하여 해당 Entity를 저장할 때 연관된 Entity까지 자동으로 저장하기 위해서는, 자동으로 저장하려고 하는 연관된 Entity에 추가한 연관관계 애너테이션에 CASCADE의 PERSIST 옵션을 설정
@Test
@DisplayName("Robbie 음식 주문")
void test1() {
    // 고객 Robbie 가 후라이드 치킨과 양념 치킨을 주문합니다.
    User user = new User();
    user.setName("Robbie");

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

    user.addFoodList(food);

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

    user.addFoodList(food2);

    userRepository.save(user);
//    foodRepository.save(food);
//    foodRepository.save(food2); 영속성 전이로 생략가능
}

CASCADE : REMOVE

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

    @OneToMany(mappedBy = "user", cascade = {CascadeType.PERSIST, CascadeType.REMOVE}) //중복설정가능
    private List<Food> foodList = new ArrayList<>();

    public void addFoodList(Food food) {
        this.foodList.add(food);
        food.setUser(this);// 외래 키(연관 관계) 설정
    }
}
  • 해당 Entity 객체를 삭제 했을 때 연관된 Entity 객체들을 자동으로 삭제
@Test
@Transactional
@Rollback(value = false)
@DisplayName("Robbie 탈퇴")
void test3() {
    // 고객 Robbie 를 조회합니다.
    User user = userRepository.findByName("Robbie");
    System.out.println("user.getName() = " + user.getName());

    // Robbie 가 주문한 음식 조회
    for (Food food : user.getFoodList()) {
        System.out.println("food.getName() = " + food.getName());
    }

    // 주문한 음식 데이터 삭제
//    foodRepository.deleteAll(user.getFoodList()); 생략가능

    // Robbie 탈퇴
    userRepository.delete(user);
}

고아 Entity 삭제

orphanRemoval

  • Remove : 해당 Entity 객체를 삭제 했을 때 연관된 Entity 객체들을 자동으로 삭제
    • 연관된 Entity와의 관계를 제거하는건 자동으로 해당 Entity를 삭제해주진
      • 이를 orphanRemoval 옵션이 가능케해줌(opphanRemoval은 cascade기능도 포함되어있음)
@Entity
@Getter
@Setter
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST, orphanRemoval = true)
    private List<Food> foodList = new ArrayList<>();

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

0개의 댓글