velog에서 이전한 글 입니다.

Entity 연관관계

공통

외래 키의 주인만이 외래 키를 등록,수정,삭제 할 수 있다.
외래 키의 주인은 테이블에 외래 키 컬럼이 생성되는 Entity를 말한다.
주인이 아닌 쪽은 오직 외래 키 읽기만 가능하다.

외래 키의 주인은 @JoinColumn(name = 컬럼명)를 사용하고
(양방향이라면) 반대편은 (mappedBy = 외래 키의 주인에서 '나'를 가르키는 필드명)을 사용한다.

'나'를 가르키는 컬럼명이 아니라 필드명이다. JoinColumn의 컬럼명이 아니라 필드명이란 거

 

필드명 = 자바 객체 필드

컬럼명 = db 컬럼

단방향,양방향

하나를 예시로 보자

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

    @ManyToOne
    @JoinColumn(name = "customer_id")
    private Customer customer;
}

public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

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

    public void addFoodList(Food food) {
        this.foodList.add(food);
        food.setCustomer(this);
    }
}

위 코드에서 Customer에 @OneToMany 필드는 필수가 아니다.

 

외래키의 주인이 아닌 쪽에서 연관 관계 객채를 참조하고 싶다면 양방향을 설정하고 참조할 필요가 없다면 단방향만 해도 무관하다.

 

양방향에서 주의점

    (1)
    user.getFoodList().add(food);
    (2)
    user.addFoodList(food);

    userRepository.save(user);

(1)과 같이 외래 키의 주인이 아닌 쪽에서 연관관계 객체를 추가해도 외래 키 값은 들어가지 않는다. 그래서 (2)와 같은 메서드 작성이 필요하다. 결국 외래 키의 주인에서만 등록/수정/삭제 가 가능하다는 것

 

양방향에선 외래 키의 주인에 연관관계 객체를 추가할 때도 생각해볼게 있다.

Food food = new Food();
...
food.setCustomer(customer);
foodRepository.save(food);

위의 코드는 db는 문제없으나 객체 관점에서 반대쪽은 entity의 foodList에는 food가 추가되지 않은 상태가 된다.

(Food entity)
       public void setCustomer(Customer customer){
        this.customer = customer;
        customer.getFoodList().add(this);
    }

그래서 위와 같이 customer 객체에도 food를 추가하도록 처리해줄 수 있다.
food를 save하고 아래에 customer를 참조하는 추가 로직이 있다면 필요한 처리이다.

N(FK주인):1과 1(FK주인):N

일반적으로 N쪽이 외래키의 주인이다. 그럼 1쪽이 외래키의 주인이라면?

  • 1(주인)대N의 관계여도 1의 테이블에 FK컬럼이 생길 이유는 없다. 그래서 DB에는 N의 테이블에 FK컬럼이 생기지만 관리는 1(FK주인)쪽이 하게된다. 그래서 추가 UPDATE가 발생한다.

'UPDATE가 발생한다'는 부분을 살펴보자

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

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

Food가 N이고 외래 키의 주인인 단방향 관계이다.

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

    Food food = new Food();
    food.setName("후라이드 치킨");
    food.setPrice(15000);
    food.setUser(user); // 외래 키(연관 관계) 설정

    userRepository.save(user);
    foodRepository.save(food);

위의 작업에서 sql은 select를 제외하고 insert가 2번 나간다.

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

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

Food가 1이고 외래 키의 주인인 단방향 관계이다.

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

    Food food = new Food();
    food.setName("후라이드 치킨");
    food.setPrice(15000);
    food.getUserList().add(user); // 외래 키(연관 관계) 설정

    userRepository.save(user);
    foodRepository.save(food);

위의 작업에서 sql은 select를 제외하고 insert가 2번 update가 1번 나간다.

 

왜 그럴까?
FK주인만이 FK를 등록,수정,삭제할 수 있다. 그런데 FK주인이 관리하는 FK가 다른 테이블에 존재한다. 그래서 추가 작업이 발생하는 것이다.
User를 insert할 때 FK도 넣고 싶지만 User는 외래 키의 주인이 아니므로 외래 키를 등록할 수 없고 insert가 동작한 후 외래 키의 주인인 Food가 user에 update쿼리를 날려 등록(수정)한다고 볼 수 있다.

 

추가로 save와 동시에 fk가 update되는 것이 아니다.
그래서 save의 순서를 바꿔도 insert순서만 바뀌고 update는 마지막에 동작한다.
1(FK주인):N 연관 관계에서 외래 키의 등록은 영속성 컨텍스트의 쓰기 지연 큐에 마지막에 들어가도록 돼 있는 듯하다.

추가로

Entity option의 지연 로딩, 영속성 전이, 고아 entity삭제를 알아보자.

2023.07.13 - [spring] - 스프링) JPA Entity option 지연 로딩/영속성 전이/고아 entity삭제 (23-07-12)