elevne's Study Note
JPA 연관관계 매핑 본문
엔티티는 대부분 다른 엔티티와 연관관계가 있다. 객체는 참조(주소)를 사용해서 관계를 맺고 테이블은 외래 키를 사용해서 관계를 맺게된다. 이 둘은 완전히 다른 특징을 가진다. ORM 에서 가장 어려운 부분이 바로 객체 연관관계와 테이블 연관관계를 매핑하는 일이라고 한다.
단방향 연관관계
연관관계 중 N:1 관계, 다대일 관계를 가장 먼저 이해해야 한다. 회원과 팀의 관계로 이를 알아볼 수 있다.
회원 객체는 Member.team 필드(멤버변수)로 팀 객체와 연관관계를 맺는다. 이는 단방향 관계다. Member.team 필드를 통해 팀을 알 수는 있지만 반대로 팀은 회원을 알 수 없다. 이와는 다르게 회원 테이블과 팀 테이블은 양방향 관계다. 회원 테이블의 TEAM_ID 외래 키를 통해서 회원과 팀을 조인할 수도 있고, 반대로 팀과 회원도 조인할 수 있다.
SELECT * FROM MEMBER M JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID;
참조를 통한 연관관계는 언제나 단방향이다. 객체 간 연간관계를 양방향으로 만들고싶다면 반대쪽에도 필드를 추가해서 참조를 보관해야 한다.
JPA 를 활용하여 위 구조를 코드로 작성해본다.
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Member {
@Id
@Column(name = "MEMBER_ID")
private String id;
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Team {
@Id
@Column(name = "TEAM_ID")
private String id;
private String name;
}
@ManyToOne 은 N:1 관계 매핑 정보이다. 연관관계를 매핑할 때 다중성을 나타내는 애노테이션을 필수로 사용해야 한다. @JoinColumn 은 외래 키를 매핑할 때 사용된다. name 속성에는 매핑할 외래 키 이름을 지정한다. (회원과 팀 테이블은 TEAM_ID 외래 키로 연관관계를 맺으므로 이 값을 지정)
연관관계가 있는 엔티티를 조회할 때는 member.getTeam() 과 같은 방법으로 객체 그래프 탐색을 할 수도 있고, 다음과 같이 JPQL 을 사용할 수도 있다. 회원을 대상으로 조회하는데 팀1 에 소속된 회원만 조회하려면 회원과 연관되 팀 엔티티를 검색 조건으로 사용해야 한다. SQL 은 연관된 테이블을 조인해서 검색조건을 사용하면 된다. JPQL 도 조인을 지원한다.
private void manyToOneMethod(EntityManager em) {
Team team1 = new Team("TEAM1", "팀1");
em.persist(team1);
Member member1 = new Member("MEMBER1", "회원1", team1);
em.persist(member1);
Member member2 = new Member("MEMBER2", "회원2", team1);
em.persist(member2);
String jpql = "select m from Member m join m.team t where t.name=:teamName";
List<Member> resultList = em.createQuery(jpql, Member.class)
.setParameter("teamName", "팀1").getResultList();
for (Member member : resultList) {
System.out.println("[QUERY] member.username = "+member.getUsername());
}
}
JPQL 의 from Member m join m.team t 부분을 보면 회원이 팀과 관계를 가지고 있는 필드를 통해 Member 와 Team 을 조인한다. 그리고 where 절을 보면 조인한 t.name 을 검색조건으로 사용해서 팀1 에 속한 회원만을 검색한다. (:teamName 과 같이 : 로 시작하는 것은 파라미터를 바인딩받는 문법이다)
양방향 연관관계
이번에는 반대 방향인 팀에서 회원으로 접근하는 관계를 추가해본다. 회원에서 팀으로 접근하고 반대인 팀에서도 회원으로 접근할 수 있도록 하는 것이다. 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 완료되었지만, 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가된다. 회원과 팀은 다대일 관계이지만 반대로 팀에서 회원은 일대다 관계이다. 일대다 관계는 여러 건과 연관관계를 맺을 수 있으므로 컬렉션을 사용해야 한다.
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Team {
@Id
@Column(name = "TEAM_ID")
private String id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
private void oneToManyMethod(EntityManager em) {
Team team1 = new Team("TEAM1", "팀1");
em.persist(team1);
Member member1 = new Member("MEMBER1", "회원1", team1);
team1.getMembers().add(member1);
em.persist(member1);
Member member2 = new Member("MEMBER2", "회원2", team1);
team1.getMembers().add(member2);
em.persist(member2);
Team team = em.find(Team.class, "TEAM1");
List<Member> members = team.getMembers();
System.out.println(team.getId());
System.out.println(members.size());
for (Member member : members) {
System.out.println("member.username = "+member.getUsername());
}
}
mappedBy 의 사용 이유에 대해서 알아보아야 한다. 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다. 엔티티를 단방향으로 매핑하면 참조를 하나만 사용하므로 이 참조로 외래 키를 관리하면 된다. 그런데 양방향으로 매핑하면 회원 -> 팀, 팀 -> 회원 두 곳에서 서로를 참조한다. 따라서 객체의 연관관계를 관리하는 포인트가 2 곳으로 늘어난다. 엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나인 차이가 발생하게 되는 것이다. 이를 해결하기 위해 JPA 는 두 객체 연관관계 중 하나를 정해서 테이블의 왜래 키를 관리하는데, 이를 연관관계의 주인이라고 한다. 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리할 수 있다. 반면 주인이 아닌 쪽은 읽기만 할 수 있다. 주인은 mappedBy 속성을 사용하지 않으며, 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다. (연관관계의 주인을 정한다는 것은 외래 키 관리자를 선택하는 것. 데이터베이스 테이블의 다대일, 일대다 관계에서는 항상 "다" 쪽이 외래 키를 가진다. "다" 쪽은 @ManyToOne 은 항상 연관관계의 주인이 되므로 mappedBy 를 설정할 수 없다. 따라서 @ManyToOne 에는 mappedBy 속성이 없다)
또 위 테스트 코드를 확인해보면 회원 1, 2 를 팀의 컬렉션에 담는 것을 확인할 수 있다. 이처럼 연관관계의 주인에도 값을 입력해주어야 한다. (그렇지 않으면 TEAM_ID 외래 키 값도 null 이 저장된다) 이러한 양방향 관계에서는 아래와 같이 Member 클래스의 setTeam 메소드를 수정해서 사용하는 편이 좋을 수 있다.
public void setTeam(Team team) {
if (this.team != null) {
this.team.getMembers().remove(this);
}
this.team = team;
team.getMembers().add(this);
}
Reference:
자바 ORM 표준 JPA 프로그래밍
'Backend > JPA 프로그래밍' 카테고리의 다른 글
JPA - Proxy, Cascade, Orphan (0) | 2023.07.11 |
---|---|
JPA 고급매핑 (0) | 2023.07.09 |
JPA N:M 관계 (0) | 2023.07.06 |
JPA Entity 매핑 (0) | 2023.07.04 |
JPA 영속성 관리 (0) | 2023.07.03 |