· 오늘 공부한 것
Entity 연관 관계
- 1 대 1 관계
- N 대 1 관계
- 1 대 N 관계
- N 대 M 관계
지연 로딩과 즉시로딩 (LAZY, EAGER)
영속성 전이 (cascade)
고아 Entity 삭제 (orphanRemoval)
· 공부한 내용, 궁금한 내용, 부족한 내용
entity를 작성을 전에 했을 때는 1개를 가지고 진행했는데 이번에는 entity끼리의 관계설정하는 방법을 학습했다. entity관계 설정 시 단방향인지 양방향인지에 따라서 참조하여 조회를 할 수 있고 없고를 정할 수 있다. 하지만 DB 테이블 간의 관계에서는 이러한 방향의 개념은 없다. FK로 연결되어 있으면 JOIN으로 어느 테이블에서든지 참조해서 가져올 수 있다. 그렇기에 지금 하는 것은 entity끼리의 관계이기 때문에 DB테이블 관계와 약간은 분리해서 생각해야 할 부분들이 있다. 아래가 정리한 부분이다.
- DB 테이블에서는 테이블 사이의 연관관계를 FK(외래 키)로 맺을 수 있고 방향 상관없이 조회가 가능합니다.
- Entity에서는 상대 Entity를 참조하여 Entity 사이의 연관관계를 맺을 수 있습니다.
- 하지만 상대 Entity를 참조하지 않고 있다면 상대 Entity를 조회할 수 있는 방법이 없습니다.
- 따라서 Entity에서는 DB 테이블에는 없는 방향의 개념이 존재합니다.
1 : 1 관계
@OneToOne 애너테이션을 사용한다.
단방향 관계
보통 FK의 주인은 N(다)의 관계인 Entity이지만 1 대 1 관계에서는 FK의 주인을 직접 지정해야 한다.
외래 키 주인만이 외래 키 를 등록, 수정, 삭제할 수 있으며, 주인이 아닌 쪽은 오직 외래 키를 읽기만 가능합니다.
@JoinColumn() 은 FK 주인이 활용하는 애너테이션이다.
- 칼럼명, null 여부, unique 여부 등을 지정할 수 있다.
음식 Entity가 FK의 주인인 경우
@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;
}
양방향 관계
- FK의 주인을 지정해 줄 때 mappedBy 옵션을 사용한다.
- mappedBy의 속성값은 FK의 주인인 상대 Entity의 필드명을 의미한다.
- FK의 주인 Entity에서 @JoinColumn() 애너테이션을 사용하지 않아도 defalut 옵션이 적용되어 생략이 가능하다.
- 그러나 1 대 N 관계에서 외래 키의 주인 Entity가 @JoinColumn() 애너테이션을 생략한다면 JPA가 외래 키를 저장할 컬럼을 파악할 수가 없어서 의도하지 않은 중간 테이블이 생성된다.
- 그러니 생략하지 말고 활용을 해주자!
- 양방향 관계에서 mappedBy 옵션도 위와 같은 문제가 생길 수 있기 때문에 설정해주는게 좋다.
음식 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 애너테이션을 사용한다.
단방향 관계
음식 Entity가 N의 관계로 외래 키의 주인
@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
@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 애너테이션을 사용한다.
단방향 관계
FK를 관리하는 주인은 음식 Entity이지만 실제 FK는 고객 Entity가 가지고 있다.
- 1 : N에서 N 관계의 테이블이 FK를 가질 수 있기 때문에 FK는 N관계인 고객 테이블에 FK칼럼을 만들어서 추가하지만 FK의 주인인 음식 Entity를 통해 관리한다.
- 그렇기에 실제 DB에서 FK를 고객 테이블이 가지고 있기 때문에 추가적인 UPDATE가 발생된다는 단점이 존재한다.
@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;
}
양방향 관계
- 1 대 N 관계에서는 일반적으로 양방향 관계가 존재하지 않는다.
- 양방향 관계를 맺으려면 음식 Entity를 FK의 주인으로 정해주기 위해 고객 Entity에서 mappedBy 옵션을 사용해야 하지만 @ManyToOne 애너테이션은 mappedBy 속성을 제공하지 않는다.
- N관계의 Entity인 고객 Entity에서 @JoinColumn의 insertable과 updatable 옵션을 false로 설정하여 양쪽으로 JOIN 설정을 하면 양방향처럼 설정할 수는 있다. (굳이?)
@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 대 M 관계
@ManyToMany 애너테이션을 사용한다.
단방향 관계
- 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;
}
양방향 관계
- 반대 방향인 고객 Entity에 @ManyToMany로 음식 Entity를 연결하고 mappedBy 옵션을 설정하여 FK의 주인을 설정하면 양방향 관계 맺음이 가능하다.
@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<>();
}
중간테이블을 직접 생성해서 N : M 관계 설정하기
- 중간 테이블 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;
}
지연로딩과 즉시로딩
JPA는 연관관계가 설정된 Entity의 정보를 바로 가져올지 필요할 때 가져올지 정할 수 있다.
- 즉, 가져오는 방법을 정하게 되는데 JPA에서는 Fetch Type이라 부릅니다.
- Fetch Type의 종류에는 2가지가 있는데 하나는 LAZY, 다른 하나는 EAGER입니다.
- LAZY는 지연 로딩으로 필요한 시점에 정보를 가져옵니다.
- EAGER는 즉시 로딩으로 이름의 뜻처럼 조회할 때 연관된 모든 Entity의 정보를 즉시 가져옵니다.
기본적으로 @OneToMany 애너테이션은 defalut값이 LAZY로 지정되어 있고 @ManyToOne 애너테이션은 EAGER로 되어 있다.
다른 애너테이션도 default 값이 있는데 이를 구분하는 방법은 아래와 같다.
- 애너테이션 이름에서 뒤쪽에 Many가 붙어있으면 설정된 해당 필드가 Java 컬렉션 타입일 것입니다.
- 즉, 해당 Entity의 정보가 여러 개 들어있을 수 있다는 것을 의미합니다.
- 따라서 효율적으로 정보를 조회하기 위해 지연 로딩이 default로 설정되어 있습니다.
- 반대로 이름 뒤쪽이 One일 경우 해당 Entity 정보가 한 개만 들어오기 때문에 즉시 정보를 가져와도 무리가 없어 즉시 로딩이 default로 설정되어 있습니다.
JPA에서 영속성 콘텍스트의 기느은 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());
}
@Test
@Transactional
@DisplayName("Robbie 고객 조회")
void test2() {
User user = userRepository.findByName("Robbie");
System.out.println("user.getName() = " + user.getName());
System.out.println("Robbie가 주문한 음식 이름 조회");
for (Food food : user.getFoodList()) {
System.out.println(food.getName());
}
}
위에 코드는 실행 시 left join까지 실행하여 한 번에 연관된 모든 Entity의 정보를 가져오는데 아래에 있는 코드는 user.getFoodList()가 호출될 때 select문을 사용하여 정보를 가져온다. 그리고 지연로딩이기 때문에 @Transactional 애너테이션이 사용된 모습이다.
영속성 전이
- 영속 상태의 Entity에서 수행되는 작업들이 연관된 Entity까지 전파되는 상황을 뜻한다.
- 이를 적용하여 해당 Entity를 저장할 때 연관된 Entity까지 자동으로 저장하기 위해서는 자동으로 저장하려고 하는 연관된 Entity에 추가한 연관관계 애너테이션에 CASCADE의 PERSIST 옵션을 설정하면 된다.
옵션을 사용하지 않으면 아래코드와 같이 save() 메서드를 여러 번 사용하게 된다.
@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);
}
@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);// 외래 키(연관 관계) 설정
}
}
하지만 User entity에 cascade PERSIST 옵션을 추가하게 되면 userRepository.save(user); 를 한 번만 쓰게 되면 영속성 전이로 food도 저장이 된다.
반대로 이렇게 연관된 Entity를 쉽게 삭제하는 방법인 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);// 외래 키(연관 관계) 설정
}
}
cascade에 중괄호({})를 사용하면 여러 개의 옵션을 사용할 수 있다.
cascade의 REMOVE 옵션을 하고 안하고의 차이는 아래와 같다.
@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);
}
원래는 user를 삭제하기 전에 연관된 Entity Food에서 해당 user가 주문한 음식 데이터를 삭제하는 작업이 필요하다. 하지만 위에서 REMOVE옵션을 걸어줬기 때문에 foodRepository.deleteAll(user.getFoodList()); 와 같은 작업 없이 userRepository.delete(user); 만 하면 연관된 데이터도 삭제가 된다.
고아 Entity 삭제
위에서 알아본 REMOVE옵션의 경우 해당 Entity 객체를 삭제했을 때 연관된 Entity객체들을 자동으로 삭제할 수 있었다. 하지만 연관된 Entity와 관계를 제거했다고 해서 자동으로 해당 Entity가 삭제되지는 않는다. 그래서 사용하는 옵션이 orphanRemoval이다.
@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);// 외래 키(연관 관계) 설정
}
}
@OneToMany 애너테이션에 orphanRemoval = true 옵션을 준 모습이다.
@Test
@Transactional
@Rollback(value = false)
@DisplayName("연관관계 제거")
void test1() {
// 고객 Robbie 를 조회합니다.
User user = userRepository.findByName("Robbie");
System.out.println("user.getName() = " + user.getName());
// 연관된 음식 Entity 제거 : 후라이드 치킨
Food chicken = null;
for (Food food : user.getFoodList()) {
if(food.getName().equals("후라이드 치킨")) {
chicken = food;
}
}
if(chicken != null) {
user.getFoodList().remove(chicken);
}
// 연관관계 제거 확인
for (Food food : user.getFoodList()) {
System.out.println("food.getName() = " + food.getName());
}
}
만약 orphanRemoval 옵션을 주지 않고 위에 코드를 실행하게 되면 Delete SQL이 수행되지 않는다. 하지만 옵션을 주면 Delete가 수행되는 것을 확인할 수 있었다.
그런데 여기서 REMOVE, orphanRemoval 옵션을 사용할 때 주의할 부분이 있다. 그것은 삭제하려고 하는 연관된 Entity를 다른 곳에서 참조하고 있는지 아닌지를 꼭 확인해야 하는 것이다.
- A와 B에 참조되고 있던 C를 B를 삭제하면서 같이 삭제하게 되면 A는 참조하고 있던 C가 사라졌기 때문에 문제가 발생할 수 있습니다.
- 따라서 orphanRemoval 같은 경우 @ManyToOne 같은 애너테이션에서는 사용할 수 없습니다.
- ManyToOne이 설정된 Entity는 해당 Entity 객체를 참조하는 다른 Entity 객체들이 있을 수 있기 때문에 속성으로 orphanRemoval를 가지고 있지 않습니다.
· 오늘 서칭 한 자료
강의자료 대체
· 느낀 점
- 전에 node.js로 서버 만들 때 express 사용해서 구현했는데 관계 설정이 어느 정도 기억이 났다.
- 방식은 당연히 다르지만 관계설정을 하는 개념이 같았고 특히 다대다 관계에서 중간테이블을 생성해서 관리하기 편하게 해주는 방식이 같았다.
- entity 작성이 코드 초반작업이기 때문에 관계설정까지 확실히 해두지 않으면 DB에서 무슨 작업을 할 때 어려움이 생길 수 있다고 생각했다.