Spring 숙련 - 4주차 (Entity 연관 관계)

ayboori·2023년 7월 11일
0

Spring

목록 보기
10/24

@Transactional의 경우 2개의 save 수행 시 한 개만 오류가 발생해도 모두 Rollback한다. 필요에 따라 달거나 달지 않아도 된다.

N 대 1 관계

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

실제 DB에는 List 형식으로 반영되는 것은 아니다!

// 외래 키의 주인
@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; // 이 필드 명이 User의 mappedBy에 사용됨
}
@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<>();
}

mappedBy : 외래키의 주인을 설정. 외래키 주인이 나를 참조할 때 사용하는 필드 명을 적는다!

1 대 N 관계

@OneToMany
외래키를 관리하는 주인 (여기서 음식) / 실제 외래키 (여기서 고객)를 서로 다른 Entity가 가지고 있다.

@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가 외래키를 가지고 있다면 setUser를 통해 쉽게 세팅할 수 있지만, Food 테이블 자체에는 user 칼럼이 없다! 그래서 userList를 통해서 외래키를 관리해야 한다.

외래 키를 음식 Entity가 직접 가질 수 있다면 INSERT 발생 시 한번에 처리할 수 있지만 실제 DB에서 외래 키를 고객 테이블이 가지고 있기 때문에 추가적인 UPDATE가 발생된다는 단점이 존재합니다.

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

1 대 N관계에서는 양방향 관계가 존재하지 않는다!

  • 1 대 N 관계에서 양방향 관계를 맺으려면 음식 Entity를 외래 키의 주인으로 정해주기 위해 고객 Entity에서 mappedBy 옵션을 사용해야 하지만 @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 설정을 하면 양방향처럼 설정할 수는 있습니다.

    주인이 아닌 쪽에서는 읽기만 할 수 있다고 표현하는 방법이 있다!

N 대 M 관계

ManyToMany를 사용하고, 중간 테이블 생성 시 @joinTable 사용

양쪽의 id를 가지고 있는 중간 테이블을 생성하여 사용한다.

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

joinColumns : 현재 Entity
inverseJoinColumns : 상대 Entity

중간 테이블의 경우 직접 만든 게 아니기 때문에 PK가 없어서 직접 다루기 힘들다.

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

지연 로딩

JPA 에서는 연관 관계에 있는 Entity의 정보를 바로 가져올지, 필요할 때 가져올지 정할 수 있다.

FetchType.LAZY

  • 지연 로딩 > 필요한 시점에 정보를 읽어온다
  • OneToMany, ManyToMany의 default
  • 에러를 방지하는 데에 더 도움이 된다
    • 애너테이션 이름에서 뒤쪽에 Many가 붙어있으면 설정된 해당 필드가 Java 컬렉션 타입
     @ManyToMany(mappedBy = "userList") // 외래키 주인의 필드
    private List<Food> foodList = new ArrayList<>();
  • 즉, 해당 Entity의 정보가 여러 개 들어있을 수 있다는 것을 의미합니다.
    • 따라서 효율적으로 정보를 조회하기 위해 지연 로딩이 default로 설정되어있습니다.

FetchType.EAGER

  • 즉시 로딩 > 연관된 Entity의 정보를 모두 읽어온다
  • ManyToOne의 default

One의 경우 객체 타입을 가져오므로 즉시 가져와도 무방 > 즉시 로딩

@ManyToOne(fetch = FetchType.LAZY) 등으로 변경이 가능하다.

지연 로딩도 영속성 컨텍스트의 기능 중 하나

  • 따라서 지연 로딩된 Entity의 정보를 조회하려고 할 때는 반드시 영속성 컨텍스트가 존재해야합니다.
  • ‘영속성 컨텍스트가 존재해야한다’ = ‘트랜잭션이 적용되어있어야 한다’
    @Transactional

영속성 전이

save를 일일히 해주다니 너무 귀찮다..!

한 Entity 저장할 때 연관된 Entity까지 모두 저장할 수 없을까?

    @OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST)
    private List<Food> foodList = new ArrayList<>();
  • 전이 지정 전 저장
        user.addFoodList(food2);

        // save를 일일히 해주다니 너무 귀찮다..!
        // 연관된 Entity까지 모두 저장할 수 없을까? >> User 수정하기
        userRepository.save(user);
        foodRepository.save(food);
        foodRepository.save(food2);
  • 전이 지정 후 저장
        user.addFoodList(food2);

        userRepository.save(user);

연관된 애들을 일일히 remove 하다니 귀찮다!

    @OneToMany(mappedBy = "user", cascade = {CascadeType.PERSIST,CascadeType.REMOVE})
    private List<Food> foodList = new ArrayList<>();
  • 전이 지정 전 삭제
    @Test
    @Transactional // 지연 로딩 사용하기 위해서 - OneToMany
    @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()); // 여러개의 List 먼저 모두 지워야

        // Robbie 탈퇴
        userRepository.delete(user); // 탈퇴 가능
    }
  • 전이 지정 후 삭제
@Test
@Transactional
@Rollback(value = false)
@DisplayName("영속성 전이 삭제")
void test4() {
    // 고객 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());
    }

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

연관된 데이터까지 모두 삭제 가능하다


고아 Entity 삭제

  • CASCADE의 REMOVE 옵션을 적용하면 해당 Entity 객체를 삭제 했을 때 연관된 Entity 객체들을 자동으로 삭제할 수 있었습니다.
    • 하지만 REMOVE 옵션 같은 경우 연관된 Entity와 관계를 제거했다고 해서 자동으로 해당 Entity가 삭제 되지는 않습니다.

💁‍♂️ 그렇다면 Cascade.REMOVE 와 **orphanRemoval 차이점은 무엇인가?**

Cascade.REMOVE의 경우 에 해당하는 부모 엔티티를 em.remove를 통해 직접 삭제할 때,그 아래에 있는 에 해당하는 자식 엔티티들이 삭제되는 것입니다.

orphanRemoval=true는 위 케이스도 포함하며,에 해당하는 부모 엔티티의 리스트에서 요소를 삭제하기만 해도 해당 에 해당하는 자식 엔티티가 delete되는 기능까지 포함하고 있다고 이해하시면 됩니다.

즉, orphanRemoval=true 는 리스트 요소로써의 영속성 전이도 해준다는 뜻

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

Cascade = CascadeType.REMOVE의 기능도 가지고 있다.

⚠️ 주의!

  • orphanRemoval이나 REMOVE 옵션을 사용할 때 삭제하려고 하는 연관된 Entity를 다른 곳에서 참조하고 있는지 아닌지를 꼭 확인해야합니다.
    • A와 B에 참조되고 있던 C를 B를 삭제하면서 같이 삭제하게 되면 A는 참조하고 있던 C가 사라졌기 때문에 문제가 발생할 수 있습니다.
    • 따라서 orphanRemoval 같은 경우 @ManyToOne 같은 애너테이션에서는 사용할 수 없습니다.
      • ManyToOne이 설정된 Entity는 해당 Entity 객체를 참조하는 다른 Entity 객체들이 있을 수 있기 때문에 속성으로 orphanRemoval를 가지고 있지 않습니다.
profile
프로 개발자가 되기 위해 뚜벅뚜벅.. 뚜벅초

0개의 댓글