DB 연관 관계
관계형 데이터베이스는 테이블끼리 관계를 맺을 수 있다.
관계는 논리적으로 연관이 있는 두 테이블 사이의 연결을 설정한다.
테이블 구조를 정제하고 중복 데이터를 최소화하는 것을 도와준다.
- 다중성
- 다대일 (N : 1) (@ManyToOne)
- 일대다 (1 : N) (@OneToMany)
- 일대일 (1 : 1) (@OneToOne)
- 다대다 (N : N) (@ManyToMany)
- 단방향, 양방향
- 테이블은 외래 키 하나로 조인을 사용해서 양방향으로 쿼리가 가능하므로 사실상 방향이라는 개념이 없다
- 반면에 객체는 참조용 필드를 가지고 있는 객체만 연관된 객체를 조회할 수 있다.
- 연관관계 주인
- JPA는 두 객체 연관관계 중 하나를 정해서 데이터 베이스 외래 키를 관리하는데 이것을 연관관계의 주인이라함
주인이 아닌 방향은 외래 키를 변경할 수 없고 읽기만 가능 - 연관관계의 주인이 아니면 mappedBy 속성을 사용하고 연관관계 주인 필드 이름을 값으로 입력해야함
- JPA는 두 객체 연관관계 중 하나를 정해서 데이터 베이스 외래 키를 관리하는데 이것을 연관관계의 주인이라함
- 연관관계의 필요성
- 객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것
다대일 [N : 1] - @ManyToOne
다대일 단방향
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
...
}
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
...
}
회원은 Member.team으로 팀 엔티티를 참조할 수 있지만, 반대로 팀에는 회원을 참조하는 필드가 없다.
따라서 회원과 팀은 다대일 단방향 연관관계
@ManyToOne
@JoinColumn(name = "TEAM_ID")
다대일 연관관계이므로 @ManyToOne을 사용하였다.
@JoinColumn을 사용하여 Member.team 필드를 TEAM_ID 외래 키와 매핑하였다.
다대일 양방향
Team에만 참조가 하나 늘어남
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
...
}
@OneToMany(mappedBy = "team")
위의 어노테이션을 통해, 연관관계의 주인, 즉 외래키를 관리하는 필드(Member의 team 필드)를 명시
일대다 [1 : N] : @OneToMany
일대다 단방향
Team 엔티티의 Team.members로 MEMBER 테이블의 TEAM_ID 외래 키를 관리
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
...
}
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>();
...
}
@OneToMany
mappedBy 속성이 없어졌다
Team이 연관관계의 주인이므로 mappedBy 속성을 지워주었다.
또한 @JoinColumn을 명시해 주었다.
@JoinColumn을 사용하지 않으면 JPA는 연결 테이블을 중간에 두고 연관관계를 관리하는 조인 테이블 전략을 기본으로 사용하여 매핑하기 때문에, 꼭 @JoinColumn을 붙여주어야 한다.
일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하는 것을 권장
일대다 단방향 매핑을 사용하면, 자신이 매핑된 테이블이 아닌 다른 테이블의 외래 키를 관리하게 되므로 성능 문제를 발생시킬 뿐더러 관리도 어렵게 만든다.
따라서 일대다 단방향 매핑보다는, 다대일 양방향 관계를 사용하도록 하는것이 좋다.
일대다 양방향
사실 일대다 양방향 매핑은 존재하지 않다. (일대다 양방향과 다대일 양방향은 사실 똑같은 말)
정확히는 일대다 양방향 매핑에서 @OneToMany는 데이터베이스의 특성상 연관관계의 주인이 될 수 없다.
왜냐하면 일대다 관계에서는 항상 다 쪽에 외래 키가 존재하기 때문
이런 이유로 @ManyToOne에는 mappedBy 속성이 없다.
그러나 정말 일대다 양방향 매핑을 하고 싶다면, 방법이 아예 없는것은 아니다.
일대다 단방향 매핑 반대편에, 같은 외래키를 사용하는 다대일 단방향 매핑을 읽기 전용으로 하나 추가해주면 된다.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID", insertable = false, updatable = false) //읽기 전용
private Team team;
...
}
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>();
...
}
위처럼 각 엔티티에 @JoinColumn을 명시하여 두 엔티티가 같은 외래키를 관리하도록 만들어준 후, 다(N)쪽 엔티티를 읽기 전용으로 하나 만들어주었다.
같은 외래키를 사용해야만 가능한 방법
일대다 단방향 매핑의 단점을 그대로 가지므로, 될 수 있으면 다대일 양방향 매핑을 사용하도록 하는것이 좋다.
일대일 [1 : 1] : @OneToOne
일대일 관계는 양쪽이 서로 하나의 관계만을 가진다.
일대일 관계의 반대도 일대일 관계이며, 일대일 관계는 두 테이블 중 어느곳에서든 외래 키를 가질 수 있다.
일대일 관계일 경우 고려해야 할 요소가 하나 더 늘어난다.
주 테이블에 외래 키 - 단방향
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
}
@Entity
public class Locker {
@Id @GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
}
일대일 관계이므로 @OneToOne을 사용해 매핑하였고, 데이터베이스에는 LOCKER_ID(FK)에 유니크 제약조건을 추가
주 테이블에 외래 키 - 양방향
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@OneToOne
@JoinColumn(name = "locker_ID")
private Locker locker;
}
@Entity
public class Locker {
@Id @GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
@OneToOne(mappedBy = "locker")
private Member memeber;
}
양방향 연관관계이므로, mappedBy를 사용하여 연관관계의 주인을 정해주었다.
대상 테이블에 외래 키 - 단방향
일대일 관계 중 대상 테이블의 외래 키가 있는 단방향 관계는 지원하지 않는다.
대상 테이블에 외래 키 - 양방향
사실 이 관계는 주 테이블에 외래 키 - 양방향과 동일하다.
단지 주 테이블만 바뀌었을 뿐이다.
@Entity
public class Locker {
@Id @GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
@OneToOne
@JoinColumn(name = "MEMBER_ID")
private Member memeber;
}
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@OneToOne(mappedBy = "member")
private Locker locker;
}
프록시를 사용할 때 외래 키를 직접 관리하지 않는 일대일 관계는 지연 로딩으로 설정하더라도 즉시 로딩
예를 들어 위의 예제에서 Locker.member는 지연 로딩할 수 있지만, Member.locker는 지연 로딩으로 설정하더라도 즉시 로딩됩니다.
이것은 프록시의 한계 때문에 발생하는 문제이다.
일대일 연관관계 정리
주 테이블에 외래 키
- 주 객체가 대상 객체의 참조를 가지는 것 처럼, 주 테이블에 외래 키를 두고 대상 테이블을 찾음
- 객체지향 개발자 선호
- JPA 매핑이 편리
- 장점 : 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능(성능상으로 좋다)
- 단점 : 값이 없으면 외래 키에 null이 허용
대상 테이블에 외래 키
- DB 개발자 선호
- 장점 : 주 테이블과 대상 테이블의 관계를 1대1에서 1대다 관계로 변경할 때 테이블 구조가 유지
- 단점 : 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩
다대다[N : N] : @ManyToMany
다대다 관계 대신 연결 엔티티를 사용한다
두 테이블을 연결하는 연결 테이블을 만들고, 그것을 연결 엔티티로 승격시켜준다.
즉 @ManyToMany -> @OneToMany와 @ManyToOne으로 나눠지는 것
Member와 Product의 다대다 관계를 Order 엔티티를 추가하며 1대다, 다대1 관계로 풀어냄
양방향 매핑 TIP
- 단방향 매핑만으로도 이미 연관관계 매핑은 완료된 상태
- 단방향 매핑으로 먼저 설계를 끝낸 후 필요한 경우에만 양방향을 추가 (이는 DB 테이블에 영향을 주지 않는다..)
참고
'데이터베이스' 카테고리의 다른 글
Redis는 언제 어떻게 사용할까 (0) | 2023.01.19 |
---|
댓글