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
복사