양방향 연관관계의 이해를 위해, 다대일(N:1) 양방향 관계로 설명을 해보겠습니다.
이해를 돕기 위해, 회원(Member)과 팀(Team)의 관계를 예시로 들어보겠습니다.
위 조건들을 살펴보면, 다대일 단방향 관계와 동일합니다..
위 조건에 다대일 양방향 관계를 위한 추가 조건은 아래와 같습니다.
위 관계를 통해서 객체 및 테이블 모델링을 한 결과는 아래와 같습니다.
해당 객체 모델링을 코드로 나타내어 보도록 하겠습니다.
Member 클래스 (다대일
에서 다
에 해당합니다.)
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID)
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
// Getter, Setter, Constructor...
}
Team 클래스 (다대일
에서 일
에 해당합니다.)
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID)
private Long id;
@Column(name = "NAME")
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
// NPE 방지를 위해서, Collection 객체를 초기화 합니다.
// NPE : Null Pointer Exception
// Getter, Setter, Constructor
}
다대일 양방향 매핑은 다대일 단방향
매핑과 일대다 단방향
을 함께 사용하는 형태입니다.
다대일 양방향 매핑에서 다
에 속하는 Member 클래스
는 다대일 단방향
매핑의 코드와 동일합니다.
일
에 속하는 Team 클래스에 아래의 사항을 추가합니다.일
에 해당하는 클래스에 다
에 해당하는 클래스를 Collection 프레임워크로 감싼 형태의 참조 필드로 작성해주시면 됩니다.@OneToMany(mappedBy = "반대쪽 매핑의 필드 이름값")
를 추가하여 줍니다.양방향 연관관계 매핑시, 반대편 테이블의 외래키를 관리하는 객체입니다.
양방향 연관관계는 2개의 단방향 매핑으로 이루어져있습니다.
즉, 객체의 참조가 2개이며, 이를 테이블 연관관계로 표현하면 외래 키 하나로 두 테이블의 연관관계를 관리합니다.
객체의 참조는 2개이며, 외래키는 1개인 객체 모델링과 테이블 모델링의 차이가 발생합니다.
그렇다면 2개의 단방향 매핑 중, 하나의 관계를 정해서 테이블의 외래키를 관리해야합니다.
이때 테이블의 외래키를 관리하는 객체를 연관관계의 주인이라고 합니다.
연관관계의 주인은 mappedBy 속성을 활용하여 지정한다.
연관관계의 주인에 대한 더 자세한 내용은 JPA 에서의 연관관계를 참고해 주시기 바랍니다.
Team team1 = new Team(0L, "팀1");
entityManager.persist(team1);
// 현재 연관관계의 주인은 외래키 (TEAM_ID)를 관리하는 Member 클래스 입니다.
Member member1 = new Member(0L, "회원1");
entityManager.persist(member1);
// 연관관계의 주인에 값 설정
member1.setTeam(team1);
// 역방향 연관관계를 설정하지 않아도, 지연 로딩을 통해서 아래에서 Member에 접근할 수 있다.
//team.getMembers().add(member);
// 이 동작이 수행되지 않으면 FK가 설정되어 있지 않은 1차캐시에만 영속화 된 상태이다.
// SELECT 쿼리로 조회해봤자 list 사이즈 0이다.
entityManager.flush();
entityManager.clear();
Team findTeam = entityManager.find(Team.class, team1.getId());
List<Member> findMembers = findTeam.getMembers();
for (Member m : findMembers) {
// flush, clear가 일어난 후에는 팀의 Members에 넣어주지 않았지만, 조회를 할 수 있음. 이것이 지연로딩
System.out.println(m.getUsername());
}
Assertions.assertEquals(1, findMembers.size());
// 팀 및 회원 객체 생성
Team team = new Team(0L, "팀1");
Member member1 = new Member(0L, "회원1");
Member member2 = new Member(0L, "회원2");
member1.setTeam(team);
member2.setTeam(team);
List<Member> members = team.getMembers();
System.out.println("members.size = " + members.size());
Assertions.assertEquals( 2, members.size());
양방향 연관관계 설정 시, 결국 코드상에서 양쪽 모두 연관관계를 설정해 주는 것 이 좋습니다.
이때 위 두 코드는 항상 동시에 실행되어야 하기 때문에 이를 하나의 메서드로 사용하는 것이 안전합니다.
Member 클래스의 setTeam() 메서드를 아래와 같이 수정합니다.
public class Member {
...
// 연관관계 편의 메서드
public void setTeam(Team team) {
// 기존 팀과 연관관계를 제거
if (this.team != null) {
this.team.getMembers().remove(this);
}
// 새로운 연관관계 설정
this.team = team;
if (team != null) { // 연관관계 제거 시, team == null
team.getMembers().add(this);
}
}
}
연관관계를 등록, 조회, 수정, 삭제 하는 예제를 통해 연관관계를 어떻게 사용하는지 알아보겠습니다.
public void saveTest() {
Team team1 = new Team(0L, "팀1");
entityManager.persist(team1);
Member member1 = new Member(0L, "회원1");
Member member2 = new Member(1L, "회원2");
entityManager.persist(member1);
entityManager.persist(member2);
// 연관관계의 주인에 값 설정
member1.setTeam(team1);
member2.setTeam(team1);
}
JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태이어야 합니다.
위 코드를 실행하였을 때, 실행되는 SQL 은 아래와 같습니다.
INSERT INTO TEAM (TEAM_ID, NAME) VALUES (0, '팀1');
INSERT INTO MEMBER (MEMBER_ID, USERNAME, TEAM_ID) VALUES (0, '회원1', 0);
INSERT INTO MEMBER (MEMBER_ID, USERNAME, TEAM_ID) VALUES (1, '회원2', 0);
객체 그래프 탐색
Member member = em.find(Member.class, 0L);
Team team = member.getTeam();
객체지향 쿼리 (JPQL) 사용
String jpql = "select t from Team t join Member m on t.id = m.team.id "
+ "where m.id = :memberId";
Team team = entityManager.createQuery(jpql, Team.class)
.setParameter("memberId", 0L)
.getSingleResult();
SELECT t.*
FROM Team t
JOIN Member m ON t.TEAM_ID = m.TEAM_ID
WHERE m.MEMBER_ID = 0;
객체 그래프 탐색
Team team = entityManager.find(Team.class, 0L);
List<Member> members = team.getMembers();
객체지향 쿼리 (JPQL) 사용
String jpql = "select m from Member m join m.team t on t.id = m.team.id "
+ "where t.id = :teamId";
List<Member> members = entityManager.createQuery(jpql, Member.class)
.setParameter("teamId", 0L)
.getResultList();
SELECT m.*
FROM Member m
JOIN Team t ON t.TEAM_ID = m.TEAM_ID
WHERE t.TEAM_ID = 0;
Team team2 = new Team(1L, "팀2");
entityManager.persist(team2);
Member member1 = entityManager.find(Member.class, 0L);
member1.setTeam(team2);
수정은 entityManager.update(); 와 같은 메서드가 없으며,
조회한 엔티티의 값만 변경해두면 트랜잭션을 커밋할 때, 플러시가 일어나면서 변경 감지 기능이 작동합니다.
이때, 변경사항을 데이터베이스에 자동으로 반영해줍니다.
UPDATE MEMBER
SET
TEAM_ID = 1, ...
WHERE
MEMBER_ID = 0;
다대일 연관관계
에서 다
에 연관된 엔티티인 Team 객체들 중, 팀1 을 제거해보겠습니다.
이때, 기존에 있던 연관관계를 먼저 제거하고 삭제를 수행해야 합니다.
그렇지 않으면 외래 키 제약 조건에 의해 데이터베이스에서 오류가 발생합니다.
Team team1 = entityManager.find(Team.class, 0L);
// Member 와의 연관관계를 제거합니다.
List<Member> members = team1.getMembers();
while (members.size() > 0) {
members.get(0).setTeam(null);
}
entityManager.remove(team1);
UPDATE MEMBER
SET
TEAM_ID = null, ...
WHERE
MEMBER_ID = 0;
UPDATE MEMBER
SET
TEAM_ID = null, ...
WHERE
MEMBER_ID = 1;
DELETE
FROM TEAM
WHERE TEAM_ID = 0;
단방향 매핑만으로도 이미 연관관계 매핑은 끝입니다.
설계할 때 객체 입장에서 보면 양방향 매핑은 양쪽다 신경을 써야 하므로 복잡도가 증가합니다.
실무에서 JPA 모델링 할 때, 단방향 매핑으로 처음에 설계를 끝냅니다(객체와 테이블을 매핑하는 것).
일대다에서 다쪽에 단방향 매핑으로 쭉 설계하면 이미 테이블 FK 설정은 끝납니다.
거기서 필요할때 양방향 매핑을 추가해서 역방향 조회 기능을 쓰면 된다. 자바 코드에 컬렉션만 추가하면 됩니다.
양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐이다.
연관관계의 주인을 정하는 기준은