2022. 7. 3. 14:20ㆍLanguage`/JPA
당연하지만 다대일 관계의 반대는 항상 일대다 관계이고 일대다 관계의 반대는 항상 다대일이다
DB Table에서 {일(1) & 다(N)} 관계에서 FK는 항상 다(N)쪽에 존재한다. 따라서 객체 양방향 관계에서 연관관계의 주인은 항상 다(N)쪽이다
1. 다대일 단방향
다대일 단방향은 가장 많이 사용하는 연관관계이고 가장 대표적인 연관관계이다
당연히 다대일 연관관계의 반대는 일대다이다
Member(N)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "member")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
private String username;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
}
Team(1)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "team")
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "team_id")
private Long id;
private String name;
}
위와 같이 엔티티상에서 설계해주면 된다
- @~~~ToOne은 나중에 설명하겠지만 기본 fetchType이 EAGER이므로 JPA의 "N+1"문제가 발생할 수 있으므로 가급적이면 모든 @~~~ToOne관계의 fetchType은 LAZY로 설정해줘야 한다
위의 다대일 단방향 연관관계에서는 객체입장에서 Member → Team으로 가는 방향은 존재하지만 Team → Member로 가는 방향은 존재하지 않는다
- 기본적으로 모든 매핑은 "단방향"으로 설계하고 필요한 경우에만 양방향 관계를 추가해주면 좋다. 추가적으로 JPQL을 활용하게되면 양방향 매핑을 활용할 일이 많고, 양방향 매핑을 활용하면 데이터 정합성 문제가 발생하지 않도록 잘 설계해야 한다
2. 다대일 양방향
단방향에서 양방향 관계를 추가해보자
Member(N)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "member")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
private String username;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
public void joinTeam(Team team){
if(this.team != null){
this.team.getMembers().remove(this);
}
this.team = team;
team.getMembers().add(this);
}
}
Team(1)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "team")
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "team_id")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
달라진 점은 Team에 "List<Member>"가 들어오고 @OneToMany를 통해서 mappedBy를 지정했다는 점이다
Team의 입장에서는 Member가 N이므로 List를 통해서 여러 Member들을 담을 수 있도록 설계해야 한다
※ 연관관계 편의 메소드
// Member
public void joinTeam(Team team){
if(this.team != null){
this.team.getMembers().remove(this);
}
this.team = team;
team.getMembers().add(this);
}
// team
public void addMember(Member member){
if(member.getTeam() != null){
member.getTeam().getMembers().remove(member);
}
this.getMembers().add(member);
member.setTeam(this);
}
테스트를 위해서 양쪽에 다 편의 메소드를 생성해주었고 실제로 사용할 때는 둘 중 하나에만 작성해야 무한루프에 빠지는 위험에서 벗어날 수 있다
현재 위의 편의 메소드에는 컬렉션 중복을 방지해기 위해서 "중복 방지 로직"을 작성해보았다
하지만 이 중복 방지 로직이 없다면 어떻게 될까?
1. Member의 joinTeam에 중복 방지 로직이 없다면...
public void joinTeam(Team team){
this.team = team;
team.getMembers().add(this);
}
Member member = new Member("member1");
member.joinTeam(teamA);
member.joinTeam(teamB);
현재 member는 teamA에 join을 하였다가 나중에 teamB로 join을 했다고 가정하자
그러면 상식적으로 teamA의 List<Member> 컬렉션에는 member가 없는것이 정상적이다
이제 결과를 살펴보면
teamA에 member 존재? = true
Member{id=1, username='member1', team=Team{id=1, name='teamA'}}
====================================================================
teamB에 member 존재? = true
Member{id=1, username='member1', team=Team{id=2, name='teamB'}}
이렇게 중복 방지 로직이 없으면 member라는 하나의 사람이 team 2개에 전부 존재한다는 말이 안되는 상황이 벌어진다
따라서 중복 방지 로직이 필요하고 이를 적용한 결과를 살펴보자
public void joinTeam(Team team){
if(this.team != null){
this.team.getMembers().remove(this);
}
this.team = team;
team.getMembers().add(this);
}
Member member = new Member("member1");
member.joinTeam(teamA);
member.joinTeam(teamB);
<중복 방지 로직>
만약 member가 이미 team에 있다면 해당 team의 List<Member>에도 member가 있을 것이다.
따라서 해당 List에서 member를 remove시켜주고, 새로운 team에 적용하는 로직이다
teamA에 member 존재? = false
================================================================
teamB에 member 존재? = true
Member{id=1, username='member1', team=Team{id=2, name='teamB'}}
이제 원하는대로 로직이 실행된 것을 확인할 수 있다
양쪽 엔티티 어느곳이든 연관관계 편의 메소드를 작성할 수 있다
하지만 어차피 값을 설정해줘야 하는 곳은 "주인"쪽이고 따라서 "주인"쪽에 연관관계 편의 메소드를 작성하는 것이 더 편리하고 효율적이다