////
Search

5. 연관관계 매핑 기초

태그
생성 일시
2023/04/14 09:51

1. 단방향 연관관계

연관관계 중에선 다대일(N:1) 단방향 관계를 가장 먼저 이해해야 한다.
회원과 팀의 다대일 관계 예시
회원과 팀이 있다.
회원은 하나의 팀에만 소속될 수 있다.
회원과 팀은 다대일 관계다.
객체 연관관계
회원 객체는 Member.team 필드로 팀 객체와 연관관계를 맺는다.
회원은 Team에 접근 가능하지만 반대 방향은 접근하는 필드가 없다.
테이블 연관관계
회원 테이블은 TEAM_ID 외래 키로 팀 테이블과 연관관계를 맺는다.
회원 테이블과 팀 테이블은 양방향 관계로 TEAM_ID를 통해서 서로 조인이 가능하다.
객체 연관관계와 테이블 연관관계의 가장 큰 차이
참조를 통한 연관관계는 언제나 단방향으로 양방향 관계를 만드록 싶다면 반대쪽에도 필드를 추가해 참조를 보관해야 한다.
서로 참조하는 것을 양방향 연관관계라 하지만 정확히는 양방향이 아닌 서로 다른 단방향 관계 2개다.
반면 테이블은 외래 키 하나로 양방향으로 조인 가능하다.
객체 연관관계 VS 테이블 연관관계 정리
객체는 참조로 연관관계를 맺는다.
테이블은 외래키로 연관관계를 맺는다.

1.1. 순수한 객체 연관관계

@Entity public class Member { @Id @Column(name = "MEMBER_ID") private Long id; private String username; //연관 관계 매핑 @ManyToOne @JoinColumn(name="TEAM_ID") private Team team; //연관관계 설정 public void setTeam(Team team) { this.team = team; } //Getter, Setter ... } public class Team { private String id; private String name; //Getter, Setter ... }
Java
복사
//팀 저장 Team team = new Team(); team.setName("wooteco"); em.persist(team); //회원 저장 Member member = new Member(); member.setName("conas"); member.setTeamId(team.getId()); em.persist(member);
Java
복사

1.2. 테이블 연관관계

CREATE TABLE MEMBER { MEMBER_ID VARCHAR(255) NOT NULL, TEAM_ID VARCHAR(255), USERNAME VARCHAR(255), PRIMARY KEY (MEMBER_ID) } CREATE TABLE TEAM { TEAM_ID VARCHAR(255) NOT NULL, NAME VARCHAR(255), PRIMARY KEY (TEAM_ID) } //회원 테이블의 TEAM_ID에 외래 키 제약조건 설정 ALTER TABLE MEMBER ADD CONSTRAINT FK_MEMBER_TEAM FOREIGN KEY (TEAM_ID) REFERENCES TEAM
SQL
복사
테이블 DDL
INSERT INTO TEAM(TEAM_ID, NAME) VALUES('team1', '팀1'); INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME) VALUES('member1', 'team1', '회원1'); INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME) VALUES('member2', 'team1', '회원2');
SQL
복사
회원1과 회원2을 팀1에 소속
SELECT T.* FROM MEMBER M JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID WHERE M.MEMBER_ID = 'member1'
SQL
복사
회원1이 소속된 팀 조회

1.3. 객체 관계 매핑

@Entity public class Member { @Id @Column(name = "MEMBER_ID") prviate String id; private String username; //연관관계 매핑 @ManyToOne @JoinColumn(name="TEAM_ID") private Team team; //연관관계 설정 public void setTeam(Team team) { this.team = team; } //Getter, Setter ... }
Java
복사
매핑한 회원 엔티티
@Entity public class Team { @Id @Column(name = "TEAM_ID") private String id; private String name; //Getter, Setter ... }
Java
복사
매핑한 팀 엔티티
객체 연관관계 : 회원 객체의 Memeber.team 필드 사용
테이블 연관관계 : 회원 테이블의 MEMBER.TEAM_ID 외래 키 컬럼 사용
@ManyToOne 이름 그대로 다대일 관계라는 매핑 정보로 다중성을 나타내는 어노테이션을 필수로 사용해야 한다.
@JoinColumn(name="TEAM_ID") 조인 컬럼은 외래 키를 매핑할 때 이용하며 name 속성에 매핑할 왜래 키 이름을 지정한다.
이 어노테이션은 생략할 수 있다.

1.4. @JoinColumn

속성
설명
기본값
name
매핑할 외래 키 이름
필드명 + _ + 참조하는 테이블의기본 키 컬럼명
referencedColumnName
외래 키가 참조하는대상 테이블의 컬럼명
참조하는 테이블의 기본 키 컬럼명
foreignKey(DDL)
외래 키 제약조건 직접 지정힐 수 있고 테이블 생성할 때만 사용한다.
unique nullable insertable columnDefinition table
@Column의 속성과 같음

1.5. @ManyToOne

속성
설명
기본값
optional
false로 설정 시 연관된 엔티티가 항상 존재해야 한다.
true
fetch
글로벌 패치 전략을 설정한다.
- @ManyToOne=FetchType.EAGER - @OneToMnay=FetchType.LAZY
cascade
속성 전이 기능을 사용한다.
targetEntity
- 연관된 엔티티의 타입 정보 설정한다. - 거의 사용하지 않는다. - 컬렉션 사용해도 제네릭으로 타입 정보 알 수 있다.

2. 연관관계 사용

2.1. 저장

public void testSave() { //팀1 저장 Team team1 = new Team("team1", "팀1"); em.persist(team1); //회원1 저장 Member member1 = new Member("member1", "회원1"); member1.setTeam(team1); //연관관계 설정 member1 → team1 em.persist(member1); //회원2 저장 Member member2 = new Member("member2", "회원2"); member2.setTeam(team1); //연관관계 설정 member2 → team1 em.persist(member2); }
Java
복사
회원과 팀을 저장하는 코드
JPA는 참조한 팀의 식별자를 외래 키로 사용해 적절한 등록 쿼리를 생성한다.

2.2. 조회

연관관계가 있는 엔티티를 조회하는 방법은 크게 2가지다.
객체 그래프 탐색
Member member = em.find(Member.class, "member1"); Team team = member.getTeam(); //객체 그래프 탐색 System.out.println("팀 이름 = " + team.getName();
Java
복사
객체 그래프 탐색 방법
객체지향 쿼리 사용 (JPQL)
private static void queryLogicJoin(EntityManager em) { //회원과 팀 간 관계가 존재하는 필드(m.team)를 통해 Member와 Team 조인 String jpql = "select m from Member m join m.team t where " + "t.name=:teamName"; //조인한 t.name을 검색조건으로 사용 //파라미터 바인딩 List<Member> resultList = em.createQuery(jpql, Member.class) .setParameter("teamName", "팀1") //팀1에 속한 회원만 검색 .getResultList(); for (Member member : resultList) { System.out.println("[query] member.username=" + member.getUsername()); } } //결과 : [query] member.username=회원1 //결과 : [query] member.username=회원2
Java
복사
JPQL 조인 검색
JPQL은 조인을 지원한다.

2.3. 수정

private static void updateRelation(EntityManger em) { //새로운 팀2 Team team2 = new Team("team2", "팀2"); em.persist(team2); //회원1에 새로운 팀2 설정 Member member = em.find(Member.class, "member1"); member.setTeam(team2); }
Java
복사
수정은 불러온 엔티티의 값만 변경하면 커밋할 때 플러시가일어나며 변경감지 기능이 작동한다.
연관관계 수정도 참조하는 대상만 변경하면 나머지는 JPA가 자동으로 처리한다.

2.4. 연관관계 제거

private static void deleteRelation(EntityManager em) { Member member1 = em.find(Member.class, "member1"); member1.setTeam(null); //연관관계 제거 }
Java
복사

2.5. 연관된 엔티티 제거

연관된 엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 한다.
왜래 키 제약조건으로 인해, DB에서 오류가 발생한다.
member1.setTeam(null); //회원1 연관관계 제거 member2.setTeam(null); //회원2 연관관계 제거 em.remove(team); //팀 삭제
Java
복사

3. 양방향 연관관계

객체의 관계
회원 → 팀
팀 → 회원
테이블 관계는 왜래 키 하나로 양방향으로 조회할 수 있다.

3.1. 양방향 연관관계 매핑

@Entity public class Member { @Id @Column(name = "MEMBER_ID") private String id; private String username; @ManyToOne @JoinColumn(name = "TEAM_ID") private Team team; //연관관계 설정 public void setTeam(Team team) { this.team = team; } //Getter, Setter }
Java
복사
매핑한 회원 엔티티
@Entity public class Team { @Id @Column(name = "TEAM_ID") private String id; private String name; //추가 @OneToMany(mappedBy = "team") private List<Member> members = new ArrayList<Member>(); // Getter, Setter ... }
Java
복사
매핑한 팀 엔티티
팀과 회원은 일대다 관계다 때문에 팀 엔티티에 컬렉션인 List<Member> members를 추가한다.

3.2. 일대다 컬렉션 조회

public void biDirection() { Team team = em.find(Team.class, "team1"); List<Member> members = team.getMembers(); // 팀 -> 회원, 객체그래프 탐색 for (Member member : members) { System.out.println("member.username = " + member.getUsername()); } } //결과 //member.username = 회원1 //member.username = 회원2 }
Java
복사
그래프 탐색을 사용해 조회한 회원들을 출려

4. 연관관계의 주인

왜 연관관계 설정시 mappedBy가 필요할까?
외래 키 하나로 두 테이블의 연관관계를 관리한다.
엔티티를 단방향으로 매핑하면 참조를 하나만 사용하므로 이 참조로 외래키를 관리하면 된다.
엔티티를 양방향으로 설정하면 객체의 참조는 둘인데 왜래 키는 하나다.
따라서 둘 사이의 차이가 발생한다.
이런 차이로 인해 JPA에서는 두 객체 연관관계 중 하나를 정해 테이블의 왜래 키를 관리해야 하는데 이것을 연관관계 주인이라 한다.

4.1. 양방향 매핑의 규칙: 연관관계의 주인

연관관계의 주인만이 데이타베이스 연관관계와 매핑된다.
연관관계의 주인만이 외래키를 관리(등록, 수정, 삭제)할 수 있다.
주인이 아닌 쪽은 읽기만 할 수 있다.
연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것이다.

4.2. 연관관계의 주인은 왜래 키가 있는 곳

연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 한다.
Team 엔티티는 mappedBy를 통해 주인이 아님을 설정.
class Team { @OneToMany(mappedBy = "team") // 연관관계 주인인 Member.team private List<Member> members = new ArrayList<Member>(); }
Java
복사

5. 양방향 연관관계 저장

public void testSave() { //팀1 저장 Team team1 = new Team("team1", "팀1"); em.persist(team1); //회원1 저장 Member member1 = new Member("member1", "회원1"); member1.setTeam(team1); //연관관계 설정 member1 -> team1 em.persist(member1); //회원2 저장 Member member2 = new Member("member2", "회원2"); member2.setTeam(team1); //연관관계 설정 member2 -> team1 em.persist(member2); }
Java
복사
양방향 연관관계 저장
단방향 연관 관계에서 살펴본 회원과 팀을 저장하는 코드와 완전히 같다.
team1.getMembers().add(member1); //무시 team1.getMembers().add(member2); //무시 member1.setTeam(team1); //연관관계 설정(연관관계의 주인) member2.setTeam(team1); //연관관계 설정(연관관계의 주인)
Java
복사
주인이 아닌 곳에서 입력된 값은 왜래 키에 영향을 주지 않는다.

6. 양방향 연관관계의 주의점

양방향 연관관계를 설정하고 가장 흔히 하는 실수는 연관관계의 주인에는 값을 입력하지 않고 주인이 아닌 곳에서만 값을 입력하는 것
public void testSaveNonOwner() { //회원1 저장 Member member1 = new Member("member1", "회원1"); em.persist(member1); //회원2 저장 Member member2 = new Member("member2", "회원2"); em.persist(member2); Team team1 = new Team("team1", "팀1"); //주인이 아닌 곳에 연관관계 설정 team1.getMembers().add(member1); team2.getMembers().add(member2); em.persist(team1); }
Java
복사
실제 데이터베이스에서는 값이 들어가지 않는다.

6.1. 순수한 객체까지 고려한 양방향 연관관계

사실 객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다.
양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있다.
public void testORM_양방향() { //팀1 저장 Team team1 = new Team("team1", "팀1"); em.persist(team1); Member member1 = new Member("member1", "회원1"); // 양방향 연관관계 설정 member1.setTeam(team1); // 연관관계 설정 member1 -> team1 team1.getMembers().add(member1); // 연관관계 설정 team1 -> member1 em.persist(member1); Member member2 = new Member("member2", "회원2"); // 양방향 연관관계 설정 member2.setTeam(team1); // 연관관계 설정 member2 -> team1 team1.getMembers().add(member2); // 연관관계 설정 team1 -> member2 em.persist(member2); }
Java
복사