연관관계 매핑 기초
엔티티들은 대부분 다른 엔티티와 연관관계가 있다.
객체는 참조(주소)를 사용해서 관계를 맺고, 테이블은 외래 키를 사용해서 관계를 맺는다.
핵심 키워드
방향 : [단방향, 양방향] 단방향 : 한쪽만 참조, 양방향 : 양쪽 모두 서로 참조
다중성 : [다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M)]
연관관계의 주인 : 객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 한다.
단방향 연관관계
다대일 단방향 연관관계
- 회원과 팀이 있다.
- 회원은 하나의 팀에만 소속될 수 있다.
- 회원과 팀은 다대일 관계다.
객체 연관관계
- 회원 객체는 Member.team 필드(멤벼변수)로 팀 객체와 연관관계를 맺는다.
테이블 연관관계
- 회원 테이블과 팀 테이블은 양방향 관계다. 회원 테이블의 TEAM_ID 외래키를 통해 회원과 팀을 조인할 수 있고, 반대로 팀과 회원도 조인할 수 있다.
객체에서 단방향 연관관계
class A {
B b;
}
class B {}
객체에서 양방향 연관관계
class A {
B b;
}
class B {
A a;
}
객체 연관관계 vs 테이블 연관관계
- 객체는 참조(주소)로 연관관계를 맺는다.
- 테이블은 외래 키로 연관관계를 맺는다.
JPA를 사용해서 매핑하기
@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 ...
}
@ManyToOne : 이름 그대로 다대일(N:1) 관계라는 매핑 정보다. 회원과 팀은 다대일 관계다. 연관관계를 매핑할 때 이렇게 다중성을 나타내는 어노테이션을 필수로 사용해야 한다.
@JoinColumn(name = "TEAM_ID") : 조인 컬럼은 외래 키를 매핑할 때 사용한다. name 속성에는 매핑할 외래 키 이름을 지정한다. 회원과 팀 테이블은 TEAM_ID 외래 키로 연관관계를 맺으므로 이 값을 지정하면 된다.
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
private String id;
private String name;
// Getter, Setter
}
@JoinColumn
속성 | 기능 | 기본값 |
name | 매핑할 외래 키 이름 | 필드명 + _ + 참조하는 테이블의 기본 키 컬럼명 |
referencedColumnName | 외래 키가 참조하는 대상 테이블의 컬럼명 | 참조하는 테이블의 기본 키 컬럼명 |
foreignKey(DDL) | 외래 키 제약조건을 직접 지정할 수 있다. 이 속성은 테이블을 생성할 때만 사용한다. | |
unique nullable insertable updatable columnDefinition table |
@Column의 속성과 같다. |
@JoinColumn을 생략하면 외래 키를 찾을 때 기본 전략을 사용한다.
기본 전략 : 필드명 + _ + 참조하는 테이블의 컬럼명
필드명(team) + _ + 참조하는 테이블의 컬럼명(TEAM_ID) = team_TEAM_ID 외래 키를 사용한다.
@ManyToOne
다대일 관계에서 사용한다.
속성 | 기능 | 기본값 |
optional | false로 설정하면 연관된 엔티티가 항상 있어야 한다. | true |
fetch | 글로벌 페치 전략을 설정한다. | @ManyToOne=FetchType.LAZY @ManyToOne=FetchType.EAGER |
cascade | 영속성 전이 기능을 사용한다. |
@OneToMany
private List<Member> member; // 제너릭으로 타입 정보를 알 수 있다.
@OneToMany(targetEntity=Member.class)
private List member; // 제너릭이 없으면 타입 정보를 알 수 없다.
연관관계 사용
연관관계 등록, 수정, 삭제, 조회
(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);
}
JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다.
JPA는 참조한 팀의 식별자(Team.id)를 외래 키로 사용해서 적절한 등록 쿼리를 생성한다.
INSERT INTO TEAM(TEAM_ID, NAME) VALUES('team1','팀1')
INSERT INTO MEMBER (MEMBER_ID, NAME, TEAM_ID) VALUES ('member1','회원1','team1')
INSERT INTO MEMBER (MEMBER_ID, NAME, TEAM_ID) VALUES ('member2','회원2','team1')
회원 테이블의 외래키 값으로 참조한 팀의 식별자 값인 team1이 입력된 것을 확인할 수 있다.
(2) 조회
연관관계가 있는 엔티티를 조회하는 방법은 크게 2가지이다.
- 객체 그래프 탐색
- 객체지향 쿼리 사용
객체 그래프 탐색
Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); // 객체 그래프 탐색
System.out.println("팀 이름 = " + team.getName());
객체 지향 쿼리 사용 (JPQL)
private static void queryLogicJoin(EntityManage em){
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());
}
}
select m from Member m join m.team t where " + "t.name=:teamName
실행되는 SQL
SELECT M.* FROM MEMBER MEMBER
INNER JOIN
TEAM TEAM ON MEMBER.TEAM_ID = TEAM1_.ID
WHERE
TEAM1_.NAME = '팀1'
(3) 수정
private static void updateRelation(EntityManage em){
// 새로운 팀2
Team team2 = new Team("team2","팀2");
em.persist(team2);
// 회원1에 새로운 팀2 설정
Member member = em.find(Member.class,"member1");
member.setTeam(team2);
}
실행되는 수정 SQL
UPDATE MEMBER
SET
TEAM_ID = 'team2', ...
WHERE
ID = 'member1'
수정은 em.update()와 같은 메소드가 없다. 단순히 불러온 엔티티의 값만 변경해두면 트랜잭션을 커밋할 때 플러시가 일어나면서 변경 감지 기능이 작동한다. 그리고 변경사항을 데이터베이스에 자동으로 반영한다.
(4) 제거
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'
연관된 엔티티 삭제
연관된 엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 한다.
그렇지 않으면 외래 키 제약조건으로 인해, 데이터베이스에서 오류가 발생한다.
member1.setTeam(null);
member2.setTeam(null);
em.remove(team);
양방향 연관관계
팀에서 회원으로 접근하는 관계를 추가
팀에서 회원은 일대다 관계다.
일대다 관계는 여러 건과 연관관계를 맺을 수 있으므로 컬렉션을 사용해야 한다.
Team.members를 List 컬렉션으로 추가
JPA는 List를 포함해서 Collection, Set, Map 같은 다양한 컬렉션을 지원한다.
양방향 연관관계 매핑
회원 엔티티
@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...
}
팀 엔티티
@Entity
pubilc class Team {
@Id
@Column(name = "TEAM_ID")
private String id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
//Getter, Setter...
}
팀과 회원은 일대다 관계다. 팀 엔티티 컬렉션인 List<Member> members를 추가했다.
일대다 관계를 매핑하기 위해 @OneToMany 매핑 정보를 사용했다.
mappedBy 속성은 양방향 매핑일때 사용하는데 반대쪽 매핑의 필드 이름을 값으로 주면 된다.
반대쪽 매핑이 Member.team이므로 team을 값으로 주었다.
일대다 컬렉션 조회
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());
}
}
연관관계의 주인
엄밀히 이야기하면 객체에는 양방향 연관관계라는 것이 없다.
서로 다른 단방향 연관관계 2개를 애플리케이션 로직으로 잘 묶어서 양방향인 것처럼 보이게 할 뿐이다.
엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나다.
따라서 둘 사이에 차이가 발생한다. JPA에서는 두 객체 연관관계 중 하나늘 정해서 테이블의 외래키를 관리해야 하는데, 이것을 연관관계의 주인이라 한다.
양방향 매핑의 규칙 : 연관관계의 주인
연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록,수정,삭제)할 수 있다.
반면에 주인이 아닌 쪽은 읽기만 할 수 있다.
연관관계를 주인으로 정할지는 mappedBy 속성을 사용하면 된다.
- 주인은 mappedBy 속성을 사용하지 않는다.
- 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.
어떤걸 연관관게의 주인으로 정해야할까?
연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것이다.
연관관계의 주인은 외래 키가 있는곳
연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 한다. 여기서는 회원테이블이 외래 키를 가지고 있으므로 Member.team이 주이 된다. 주인이 아닌 Team.members에는 mappedBy="team" 속성을 사용해서 주인이 아님을 설정한다.
여기서 mappedBy의 값으로 사용된 team은 연관관계의 주인인 Member 엔티티의 team 필드를 말한다.
class Team {
@OneToMany(mappedBy="team") // 연관관계의 주인인 Member.team
private List<Member> members = new ArrayList<>();
}
연관관계의 주인만 데이터베이스 연관관계와 매핑되고 외래 키를 관리할 수 있다.
주인이 아닌 반대편은 읽기만 가능하고 외래 키를 변경하지는 못한다.
데이터베이스 테이블의 다대일, 일대다 관계에서는 항상 다 쪽이 왜리 키를 가진다. 다 쪽인 @ManyToOne은 항상
연관관계의 주인이 되므로 mappedBy를 설정할 수 없다. 따라서 @ManyToOne에는 mappedBy 속성이 없다.
양방향 연관관계 저장
public void testSave(){
Team team1 = new Team("team1","팀1");
em.persist(team1);
Member member1 = new Member("member1","회원1");
member1.setTeam(team1);
em.persist(member1);
Member member2 = new Member("member2","회원2");
member2.setTeam(team1);
em.persist(member2);
}
팀1을 저장하고, 회원1, 회원2에 연관관계의 주인인 Member.team 필드를 통해서 회원과 팀의 연관관계를 설정하고 저장했다.
테이블 조회
SELECT * FROM MEMBER;
TEAM_ID 외래 키에 팀의 기본 키 값이 저장되어 있다.
양방향 연관관게는 연관관계의 주인이 외래 키를 관리한다.
따라서 주인이 아닌 방향은 값을 설정하지 않아도 데이터베이스에 외래 키 값이 정상 입력된다.
team1.getMembers().add(member1); // 무시(연관관계의 주인이 아님)
team1.getMembers().add(member2); // 무시(연관관계의 주인이 아님)
주인이 아닌 곳에 입력된 값은 외래 키에 영향을 주지 않는다.
이전 코드는 데이터베이스에 저장할 때 무시된다.
member1.setTeam(team1); // 연관관계 설정(연관관계의 주인)
member2.setTeam(team1); // 연관관계 설정(연관관계의 주인)
Member.team은 연관관계의 주인이다. 엔티티 매니저는 이곳에 입력된 값을 사용해서 외래 키를 관리한다.
양방향 연관관계의 주의점
연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것
데이터베이스 외래 키 값이 정상적으로 저장되지 않으면 이것부터 의심해보자
순수한 객체까지 고려한 양방향 연관관계
정말 연관관계의 주인에만 값을 저장하고 주인이 아닌 곳에는 값을 저장하지 않아도 될까?
객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다.
순수한 객체 연관관계
public void test(){
//팀 1
Team team1 = new Team("team1","팀1");
Member member1 = new Member("member1", "회원1");
Member member2 = new Member("member2", "회원2");
member1.setTeam(team1); // 연관관계 설정 member1 -> team1
member2.setTeam(teaam2); // 연관관계 설정 member2 -> team1
List<Member> members = team1.getMembers();
System.out.println("members.size = " + members.size());
}
// 결과 : members.size = 0
코드를 보면 Member.team에만 연관관계를 설정하고 반대 방향은 연관고나게를 설정하지 않았다.
양방향은 양쪽다 관계를 설정해야 한다.
member1.setTeam(team1);
team1.getMembers().add(member1);
public void test(){
//팀 1
Team team1 = new Team("team1","팀1");
Member member1 = new Member("member1", "회원1");
Member member2 = new Member("member2", "회원2");
member1.setTeam(team1); // 연관관계 설정 member1 -> team1
team1.getMembers().add(member1); // 연관관계 설정 team1 -> member1
em.persist(member1);
member2.setTeam(teaam2); // 연관관계 설정 member2 -> team1
team1.getMembers().add(member2); // 연관관계 설정 team1 -> member2
em.persist(member2);
}
Member.team : 연관관계의 주인, 이 값으로 외래키를 관리한다.
Team.members: 연관관계의 주인이 아니다. 따라서 저장시에 사용되지 않는다.
결론 : 객체의 양방향 연관관계는 양쪽 모두 관계를 맺어주자.
'Backend > JPA' 카테고리의 다른 글
[김영한 자바 ORM 표준 JPA ] -연관관계 매핑 기초 (0) | 2024.05.17 |
---|---|
[김영한 JPA활용 2편] 컬렉션 조회 최적화 (1) | 2024.04.26 |
[김영한의 JPA 활용 강의 2편] (1) | 2024.03.27 |
API 개발 고급 정리 (0) | 2024.01.04 |
엔티티 설계시 주의할점 (1) | 2023.12.06 |