
내일배움캠프 21일차 TIL : Spring - Entity 연관 관계
JPA에서 중요한 것은 객체와 관계형 데이터베이스 테이블이 어떻게 매핑되는지 이해하는 것
@Entity, @Colunm, @Id, @GenerateValue, @Enumerated등 객체와 데이터베이스 간의 일대일 대응 되는것
기본적인 어노테이션을 숙지해야 한다.
연관 관계 매핑은 비지니스 로직, 요구사항에 따라 개발자가 더 적절한 관계 설정 방법을 선택해야 한다.
연관 관계를 매핑할 때 3가지로 나눠 생각해야 한다.
데이터베이스 테이블은 외래 키 하나로 양쪽 테이블이 Join이 가능하다.
데이터베이스는 단방향과 양방향을 나눌 필요가 없다.
객체는 참조용 필드가 있는 객체만 다른 객체를 참조하는 것이 가능하다.
다시 정리해보자면
데이터베이스 테이블은 외래 키 하나로 양쪽 테이블간의 Join과 조회가 가능하다.
따라서 데이터베이스 테이블에서는 방향을 나눌 필요가 없다.
그렇지만 객체는 참조용 필드가 있어야만 다른 객체를 참조하는 것이 가능하다.
따라서 JPA로 객체를 이용해 데이터베이스를 다룰 때 참조용 필드가 한개라면 단방향
두 객체 모두 참조용 필드를 가지고 참조한다면 양방향 관계다.
다르게 보자면 양방향 관계는 두 객체가 서로 단방향 참조를 하나씩 가져서 단방향 관계 2개가 양방향 관계 처럼 사용되는 것을 말한다.
두 객체가 양방향 관계를 가질 때, 연관 관계의 주인을 정해야 한다.
위에서 양방향 관계는 단방향 관계 2개가 양방향 처럼 사용된다고 했다.
연관 관계의 주인을 지정하는 것은 두 단방향 관계중 외래키를 비롯한 데이터베이스 테이블의 제어 권한을 갖는 실질적인 관계가 어떤것인지 JPA에게 알려주는 것이다.
연관 관계의 주인은 연관 관계를 갖는 두 객체 사이에서 테이블 제어를 할 수 있지만 주인이 아닌 객체는 조회만 가능하다.
연관 관계의 주인을 정하는 곳은
외래키를 포함하는 객체가 연관 관계의 주인이 되고,
주인이 아닌 객체는 mappedBy속성을 사용해 주인을 지정해줘야 한다.
// 고객
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// mappedBy속성을 통해 주인을 지정하고,
@OneToMany(mappedBy = "user")
private List<Food> foodList = new ArrayList<>();
}
@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;
}
객체에서 양방향 연관 관계 관리 포인트가 두 개일 때는 테이블과 매핑을 담당하는 JPA가 혼란할 수 있다.
주인이 지정되지 않을 경우 어디서 외래 키를 수정할 지 결정하지 못하기 때문에
명확한 주인을 지정하고 명확한 수정 시점이 존재할 때 외래 키를 수정하기 위함이다.
데이터베이스를 기준으로 다중성을 결정한다.
- 1 : 1 (일 대 일 관계)
@OneToOne- 1 : N (일 대 다 관게)
@OneToMany- N : 1 (다 대 일 관계)
@ManyToOne- N : M (다 대 다 관계)
@ManyToMany
어노테이션 작성시 현재 클래스를 기준으로 적는다.
@OneToOne 1:1 관계를 맺어주는 역할이다.
@Entity
@Table(name = "food")
public class Food {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
// 1. 단방향 관계일 때 직접 주인을 지정해야 한다.
@OneToOne
@JoinColumn(name = "user_id")
private User user;
// 2. 양방향일 경우
@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;
// 1. 단방향일 경우 주인이 아닌 객체는 따로 해줄것이 없다.
// 2. 양방향일 경우
@OneToOne(mappedBy = "user")
private Food food;
}
위의 코드는 음식이 외래 키의 주인일 경우이다.
Entity에서 외래 키의 주인일 일반적으로 N(다)의 관계인 Entity이지만 1:1 관계에서는 주인을 직접 지정해야 한다.
@JoinColumn은 외래 키의 주인이 활용하는 어노테이션이다.
아래쪽 주석으로 단뱡향일 때와 양방향일 때의 주인 지정을 해줬다.
단방향일 경우 관계 주인이 직접 지정
양뱡향일 경우 주인이 아닌 객체에서 외래 키의 주인을 지정
코드랑 같이 적다보니 조금 복잡한거 같으니 여기서 한 번 다시 정리하겠다.
- 단방향이라면 외래 키의 주인만 상대 Entity 타입의 필드를 가지면서 @JoinColumn()을 활용하여 외래 키의 속성을 설정
- 양방향이라면 외래 키의 주인은 상대 Entity 타입의 필드를 가지면서 @JoinColumn()을 활용하여 외래 키의 속성을 설정
- 그리고 상대 Entity는 외래 키의 주인 Entity 타입의 필드를 가지면서 mappedBy 옵션을 사용하여 속성 값으로 외래 키의 주인 Entity에 선언된 @JoinColumn()으로 설정되고 있는 필드명을 넣어준다.
양뱡향의 경우를 다시 코드로 보겠다. (알파벳 순서를 따라가자)
@Entity
@Table(name = "food")
public class Food {
...
// 2. 양방향일 경우
@OneToOne
@JoinColumn(name = "user_id")
private User user;
// a. 상대 Entity 타입의 필드를 가지면서
// b. @JoinColumn()을 활용하여 외래 키의 속성 설정
}
@Entity
@Table(name = "users")
public class User {
...
// 2. 양방향일 경우
@OneToOne(mappedBy = "user") // c'. 그래서 mappedBy = "user"
private Food food;
// a'. 주인 Entity 타입의 필드를 가지면서
// c. @JoinColumn()으로 설정되고 있는 필드명을 넣는다.
// -> 여기서는 주인 Entity필드에 User라고 선언됨
}
1 : 1관계에서 한 번에 단방향과 양방향을 다뤘으니 여기부턴 하나씩 하겠다.
@OneToMany 1 대 N 관계를 맺어주는 역할
@Entity
@Table(name = "food")
public class Food {
...
@OneToMany
@JoinColumn(name = "food_id")
// 외래 키를 관리하는 주인 객체 이지만 실제 외래 키는 상대 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;
}
단방향 관계이기 때문에 주인이 아닌 객체에서 따로 mappedBy를 안해줬다.
여기서는 일 대 다 구조이기 때문에 자바컬렉션인 List를 사용하여 처리했다.
1 대 N 관계에서는 일반적으로 양방향 관계가 존재하지 않는다.
1 대 N 관계에서 양방향 관계를 맺으려면 음식 Entity를 외래 키의 주인으로 정해주기 위해 고객 Entity에서 mappedBy 옵션을 사용해야 하지만 @ManyToOne 애너테이션은 mappedBy 속성을 제공하지 않는다.
@ManyToOne N 대 1 관계를 맺어주는 역할
@Entity
@Table(name = "food")
public class Food {
...
@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 {
...
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
}
@Entity
@Table(name = "users")
public class User {
...
@OneToMany(mappedBy = "user")
private List<Food> foodList = new ArrayList<>();
}
음식 Entity가 N인 관계로 상대 Entity인 user Entity에서 List를 사용.
다대다 의 경우 중간 테이블이 숨겨저 복잡한 Join쿼리가 발생하는 경우가 생길 수 있다.
다대다로 자동생성된 중간테이블은 두 객체의 테이블의 외래 키만 저장되기 때문에 문제가 될 확률이 높다.
PA를 해보면 중간 테이블에 외래 키 외에 다른 정보가 들어가는 경우가 많기 때문에 다대다를 일대다, 다대일로 풀어서 만드는 것
(중간 테이블을 Entity로 만드는 것)이 추후 변경에도 유연하게 대처할 수 있다.
참고