1. 단방향 연관관계
연관관계 중 다대일(N:1) 단방향 관계를 가장 먼저 이해해야 한다.
객체 연관관계와 테이블 연관관계의 가장 큰 차이
- 참조를 통한 연관관계는 항상 단방향이다.
- 객체 간 연관관계를 양방향으로 만들고 싶으면 반대쪽에도 필드를 추가해서 참조를 보관해야 한다.
=> 서로 다른 단방향 관계 2개
- 객체 간 연관관계를 양방향으로 만들고 싶으면 반대쪽에도 필드를 추가해서 참조를 보관해야 한다.
- 객체는 참조(주소)로 연관관계를 맺는다.
- ex)
a.getB().getC()
- 테이블은 외래 키로 연관관계를 맺는다.
- ex)
A JOIN B or B JOIN A
1.1 순수한 객체 연관관계
객체 그래프 탐색
: 참조를 사용해서 연관관계를 탐색한다.
Team findTeam = member1.getTeam();
1.2 테이블 연관관계
조인
: 데이터베이스는 외래 키를 사용해서 연관관계를 탐색할 수 있다.
SELECT T.*
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
WHERE M.MEMBER_ID = 'member1'
1.3 객체 관계 매핑
회원과 팀, 다대일 관계로 가정
// 매핑한 회원 엔티티
@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
}
// 매핑한 팀 엔티티
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
private String id;
private String name;
// Getter, Setter..
}
1. @ManyToOne
- 다대일(N:1) 관계라는 매핑 정보
- 연관관계 매핑시 이렇게 다중성을 나타내는 어노테이션을 필수로 사용
2. JoinColumn(name="TEAM_ID")
- 조인 컬럼은 외래 키를 매핑할 때 사용
- name 속성에 매핑할 외래 키 이름을 지정
- 생략이 가능하다.
1.4 @JoinColumn
외래 키 매핑 시 사용
- 기본 전략 : 필드명_참조 테이블의 컬럼명
ex) team_TEAM_ID
1.5 @ManyToOne
다대일 관계에서 사용
속성 | 기능 | 기본값 |
---|---|---|
optional | false로 설정하면 연관된 엔티티가 항상 있어야 한다. | true |
fetch | 글로벌 페치 전략 설정. 자세한건 8장 | @ManyToOne = FetchType.EAGER @OneToMany = FetchType.LAZY |
cascade | 영속성 전이 기능 사용. 자세한건 8장 |
2. 연관관계 사용
연관관계를 등록, 수정, 삭제할 수 있다.
2.1 저장
public void testSave() {
//팀1 저장
Team team1 = new Team("team1", "팀1");
em.persist(team1);
//회원1 저장
Member member1 = new Member(100L, "회원1");
member1.setTeam(team1); //연관관계 설정 member1 -> team1
em.persist(member1);
//회원2 저장
Member member2 = new Member(101L, "회원2");
member2.setTeam(team1); //연관관계 설정 member2 -> team1
em.persist(member2);
}
JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다.
MEMBER_ID | NAME | TEAM_ID | TEAM_NAME |
---|---|---|---|
member1 | 회원1 | team1 | 팀 1 |
member2 | 회원2 | team1 | 팀1 |
2.2 조회
엔티티를 조회하는 방법은 2가지
- 객체 그래프 탐색(객체 연관관게를 사용한 조회)
- 객체지향 쿼리 사용(JPQL)
2.2.1 객체 그래프 탐색
Member member = em.find(Member.class, 100L);
Team team = member.getTeam(); // 객체 그래프 탐색 : 연관된 엔티티 조회
System.out.println("팀 이름 = " + team.getName());
2.2.2 객체 지향 쿼리
public static void testJPQL(EntityManager em) {
String jpql1 = "select m from Member m join m.team t where " +
"t.name = :teamName";
List<Member> resultList = em.createQuery(jpql1, Member.class)
.setParameter("teamName", "팀1")
.getResultList();
for (Member member : resultList) {
System.out.println("[query] member.username = " +
member.getUsername());
}
}
// 결과ㅣ [query] member.username=회원1
// 결과ㅣ [query] member.username=회원2
// 실행되는 쿼리
SELECT M.* FROM MEMBER MEMBER
INNER JOIN
TEAM TEAM ON MEMBER.TEAM_ID = TEAM1_.ID
WHERE
TEAM1_.NAME='팀1'
2.3 수정
2.3.1 연관관계를 수정하는 코드
private static void updateRelation(EntityManager em) {
// 새로운 팀2
Team team2 = new Team("team2", "팀2");
em.persist(team2);
//회원1에 새로운 팀2 설정
Member member = em.find(Member.class, 100L);
member.setTeam(team2);
}
수정은
em.update()
같은 메소드가 없다.
불러온 엔티티의 값만 변경하면 트랜잭션 커밋 시 플러시가 발생하여 변경감지 기능 작동
=> 변경사항을 데이터베이스에 자동으로 반영
2.4 제거
2.4.1 연관관계를 삭제하는 코드
private static void deleteRelation(EntityManager em) {
Member member1 = em.find(Member.class, "member1");
member1.setTeam(null); //연관관계 제거
}
// 실제 SQL
UPDATE MEMBER
SET
TEAM_ID = NULL, ...
WHERE
ID = 'member1'
2.5 연관된 엔티티 삭제
연관된 엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 한다.
그렇지 않으면 외래 키 제약조건으로 인해 데이터베이스에서 오류가 발생한다.
member1.setTeam(null); // 회원1 연관관계 제거
member2.setTeam(null); // 회원2 연관관계 제거
em.remove(team); // 팀 삭제
3. 양방향 연관관계
팀에서 회원으로 접근 가능하고, 회원에서 팀으로 접근할 수 있다.
- 회원과 팀은 다대일 관계
- 회원 -> 팀 (Member.team)
- 팀에서 회원은 일대다 관계 -> 일대다 관계는 여러 건과 연관관계를 맺을 수 있으므로 컬렉션을 사용
- 팀 -> 회원 (Team.members)
데이터베이스 테이블은 외래 키 하나로 양방향 조회 가능 -> 처음부터 양방향 관계
3.1 양방향 연관관계 매핑
3.1.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
}
3.1.2 매핑이 추가된 팀 엔티티
@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 ...
}
3.1.3 mappedBy
- 양방향 매핑 시 사용한다.
- 반대쪽 매핑의 필드 이름을 값으로 준다.
반대쪽 매핑이 Member.team
이므로 team
을 값으로 준다.
- 자세한 내용은
5.4 연관관계의 주인
에서 상세히 설명
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
}
4. 연관관계 주인
엄밀히 얘기하면 객체에는 양방향 연관관계라는 것이 없다.
서로 다른 단방향 연관관계 2개를 애플리케이션 로직으로 잘 묶어서 양방향인 것처럼 보이게 할 뿐이다.
반면, 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다.
이렇게 엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나다.
따라서 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 한다.
이것을
연관관계의 주인
이라고 한다.
4.1 양방향 매핑의 규칙: 연관관계의 주인
- 연관관계의 주인만이 외래키를 관리(등록, 수정, 삭제) 할 수 있고, 데이터베이스 연관관계와 매핑된다.
- 주인이 아닌 쪽은 읽기만 할 수 있다.
연관관계의 주인을 정한다는 것 = 외래 키 관리자를 선택하는 것
4.1.1 mappedBy 속성
주인은 mappedBy 속성
을 사용하지 않는다.
주인이 아니면 mappedBy 속성
을 사용해서 속성의 값으로 연관관계의 주인을 지정
4.2 연관관계의 주인은 외래 키가 있는 곳
- 연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 한다.
- Team 엔티티는 mappedBy를 통해 주인이 아님을 설정
class Team {
@OneToMany(mappedBy = "team") // 연관관계 주인인 Member.team
private List<Member> members = new ArrayList<Member>();
}
4.2.1 중요
- 연관관계의 주인만 데이터베이스 연관관계와 매핑, 외래 키를 관리.
- 주인이 아닌 반대편은 읽기만 가능, 외래 키를 변경하지 못한다!!
- 항상 "다(N)" 쪽이 외래 키를 가진다.
@ManyToOne
은 항상 연관관계의 주인이 된다. =>mappedBy
속성이 없다.
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);
}
Member.team 필드를 통해서 회원 과 팀의 연관관계를 설정하고 저장하였다.
주인이 아닌 곳에서 입력된 값은 외래키에 영향을 주지 않는다.
team1.getMembers().add(member1); //무시
team1.getMembers().add(member2); //무시
member1.setTeam(team1); //연관관계 설정(연관관계의 주인)
member2.setTeam(team1); //연관관계 설정(연관관계의 주인)
Member.team
은 연관관계의 주인으로, 엔티티 매니저는 이곳에 입력된 값으로 외래 키를 관리한다.
6. 양방향 연관관계 주의점
양방향 연관관계 설정 후 가장 흔히 하는 실수는
연관관계의 주인에는 값을 입력하지 않고 주인이 아닌 곳에서 값을 입력하는 것이다.데이터베이스에 외래 키 값이 정상적으로 저장되지 않으면 이것부터 의심해보자
6.1 양방향 연관관계 주의점
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);
}
회원을 조회한 결과
MEMBER_ID | USERNAME | TEAM_ID |
---|---|---|
member1 | 회원1 | null |
member2 | 회원2 | null |
6.2 순수한 객체까지 고려한 양방향 연관관계
사실 객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다.
양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있다.
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);
Member.team
: 연관관계의 주인, 이 값으로 외래 키를 관리한다.Team.members
: 연관관계의 주인이 아니다. 따라서 저장 시에 사용되지 않는다.
결론: 객체의 양방향 연관관계는 양쪽 모두 관계를 맺어주자.
6.3 연관관계 편의 메소드
양방향 연관관계는 결국 양쪽 다 신경 써야 한다.
member1.setTeam(team1); // 연관관계 설정 member1 -> team1
team1.getMembers().add(member1); // 연관관계 설정 team1 -> member1
이렇게 각각 호출하다 보면 실수로 둘 중 하나만 호출해서 양방향이 깨질 수 있다.
두 코드를 하나인 것처럼 사용하는 것이 안전한다.
=> 연관관계 편의 메소드
: 한 번에 양방향 관계를 설정하는 메소드
public class Member {
private Team team;
public void setTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
}
6.4 연관관계 편의 메소드 작성 시 주의사항
연관관계를 변경 시 기존의 정보를 삭제하는 코드를 추가해줘야 한다.
따라서 다음 코드가 올바른 코드이다.
public class Member {
private Team team;
public void setTeam(Team team) {
// 기존 팀과 관계를 제거
if(this.team != null){
this.team.getMembers().remove(this);
}
this.team = team;
team.getMembers().add(this);
}
}
7. 정리
7.1 양방향 매핑
1. 연관관계의 주인을 정해줘야 한다.
- 항상 "다(N)" 쪽이 주인이 된다. ->
@ManyToOne
어노테이션이 붙는 곳이 주인 - 주인 쪽에서는
@JoinColumn(name = ~)
으로 매핑되는 엔티티의 ID 컬럼명을 넘겨준다. - 주인이 아닌 곳은
mappedBy
로 표시해준다.
2. 연관관계의 주인이 아니면 읽기만 가능하다.
3. 연관관계의 주인만이 데이터를 변경시킬 수 있다.
4. 순수한 객체까지 고려하여 연관관계를 매핑해준다.
- 주인이 아닌 쪽에서도 데이터를 추가해준다.
- 객체 관점에서 양쪽에 데이터를 추가해주지 않으면 에러가 발생할 수 있기 때문
5. 양방향의 장점은 반대방향으로도 객체 그래프 탐색 기능이 추가되었다는 것
- 이전까지는 단방향 참조로 객체 그래프 탐색이 가능했음
6. 양방향 연관관계 매핑 시 연관관계 편의 메소드를 작성하여 사용한다.
- 실수할 가능성을 줄이기 위해 양쪽 객체에 추가해주는 작업을 하나의 메소드로 관리한다.
- 다(N) 쪽 연관관계 편의 메소드 로직에서 만약 기존 관계가 있다면 제거해주는 로직을 작성한다.
7. 다대일 연관관계에서 꼭 양방향일 필요는 없다.
- 만약 한 쪽에서만 참조할 경우 단방향 다대일 연관관계를 사용하면 된다.
- 굳이 양방향을 쓰지 않아도 된다.
- 양쪽에 다른 엔티티의 참조를 가지면 양방향
- 한쪽에만 다른 엔티티의 참조를 가지면 단방향
- 단방향 다대일의 경우, 다(N) 쪽에 다른 엔티티를 가지며
@ManyToOne
을 가지지만 (1) 쪽에서는 다(N) 쪽 엔티티를 갖지 않는다. (양방향이 아니니까)
다(N) 쪽에서 @ManyToOne
이 붙고 @JoinColumn
을 한다고 해서 일(1) 쪽에서 항상 @OneToMany
를 붙여주는건 아님!!! (여태 잘못 알고있었다...)
7.2 연관관계의 주인을 정하는 기준
연관관계의 주인은 외래 키의 위치와 관련해서 정해야지, 비즈니스 중요도로 접근하면 안된다.
ex) 자동차의 차체와 바퀴에서 바퀴가 외래 키가 있는 다쪽이다.
내 생각을 정리하자면,
외래 키가 있다는 말은 엔티티 내부에 다른 엔티티를 참조하는 객체를 포함하고 있고,
이것은 사실 단방향 매핑을 의미하며 다른 엔티티(테이블)의 외래 키를 가지고 있다는 것으로 해석된다.그래서 다대일 관계에서 보면, 다(N) 쪽은 엔티티 자체를 필드로 갖지만, 일(1) 쪽은 컬렉션으로 다쪽의 엔티티를 보관한다. 따라서 컬렉션을 필드로 갖는 일(1) 쪽은 연관관계에서 주인이 될 수 없고,
단지 mappedBy로 컬렉션 내부 엔티티들의 조회만 가능한 것이다따라서 다쪽이 항상 외래 키(참조 엔티티 필드) 를 갖기 때문에 연관관계에서 주인이 되고,
간단히 정리하자면@ManyToOne
이 붙는 쪽이 주인이 된다고 생각할 수 있을 것 같다.
'Read Book > JPA 프로그래밍' 카테고리의 다른 글
7장. 고급 매핑 (0) | 2023.02.20 |
---|---|
6장. 다양한 연관관계 매핑 (0) | 2023.02.20 |
4장. 엔티티 매핑 (0) | 2023.02.20 |
3장. 영속성 관리 (0) | 2023.02.20 |
2장. JPA 시작 (0) | 2023.02.20 |