연관관계 중에선 다대일(N:1)인 단방향 연관관계를 이해해야한다.
예를들어
회원과 팀이 있다.
회원은 하나의 팀에 소속된다.
회원(N)과 팀(1)은 다대일 관계다.
(여러명의 회원이 하나의 팀에 소속된다. 하나의 회원은 하나의 팀에 소속된다.)
Tip.
외래키를 가지고있는 테이블이라면 (N이라고 생각하고)
그 외래키를 PK로 가지고있는 테이블이 (1이라고 생각하자)
public class Member {
private String id;
private String username;
private Team team; // 팀의 참조를 보관
public void setTeam(Team team){
this .team = team;
}
}
public class Team {
private String id;
private String name;
}
회원 객체는 Member.team필드로 팀 객체와 연관관계를 맺는다.
회원 객체와 팀 객체는 단방향 관계다.
예를 들어 회원은 Member.team필드를 통해 팀을 알 수 있지만
반대로 팀은 회원을 알 수 없다.
member -> team의 조회는 member.getTeam()으로 가능하지만
team -> member 를 접근하는 필드는 없다.
여기서 잠깐
테이블 연관관계 VS 객체 연관관계
1. 객체는 참조(주소)로 연관관계를 맺는다.
2. 테이블은 외래 키로 연관관계를 맺는다.
참조를 사용하는 객체의 연관관계는 단방향이다.
A -> B (a.b)
외래 키를 사용하는 테이블의 연관관계는 양방향이다.
A JOIN B가 가능하면 반대로 B JOIN A 도 가능
그렇다면 객체를 양방향으로 할 수는 없을까?
객체를 양방향으로 참조하여면 단방향 연관관계를 2개 만들어야한다.
A -> B (a.b)
B -> A (b.a)
위에 Member Class와 Team Class를 JPA로 매핑해보자
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID")
private String id;
private String username;
//연관관계 매핑
@ManyToOne
@JoinColumn(name = "TEMA_ID")
private Team team;
//연관관계 설정
public void setTeam(Team team){
this.team = team;
}
}
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
private String id;
private String name;
}
두 클래스가 단방향 연관관계로 맵핑이 되었다.
회원과 팀은 다대일 관계기 때문에 이렇게 어노테이션을 필수로 사용해야한다.
name 속성에는 매핑할 외래 키 이름을 지정한다.
회원과 팀 테이블은 TEAM_ID 외래 키로 연관관계를 맺으므로 이 값을 지정하면 된다.
이 어노테이션은 생략할 수 있다.
@JoinColumn 생략시 기본 전략을 사용
기본전략 : 필드명 + + 참조하는 테이블의 컬럼명
ex)필드명(team)+(밑줄)+참조하는 테이블의 컬럼명(TEAM_ID) = team_TEAM_ID 외래키를 사용
public void testSave(){
//팀1저장
Team team = 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는 참조한 팀의 식별자 (Team.id)를 외래 키로 사용해서 적절한 등록 쿼리를 생성한다.
회원 테이블의 외래 키 값으로 참조한 팀의 식별자 값인 team1이 입련된 것을 확인할 수 있다.
객체 그래프 탐색 (객체 연관관계를 사용한 조회)
객체지향 쿼리사용 (JPQL)
방금 위에서 저장한 대로 회원1,회원2가 팀1에 소속해 있다고 가정하자.
객체 그래프 탐색
member.getTeam()을 사용해서 member와 연관된 team 엔티티를 조회할 수 있다.
Member member = em.find(Member.class,"member1");
Team team = member.getTeam(); //객체 그래프 탐색
System.out.printLn("팀 이름 = " + team.getName());
출력결과 팀 이름 = 팀1
이 처럼 객체를 통해 연관된 엔티티를 조회하는 것을 객체 그래프 탐색이라 한다.
예를 들어 회원을 대상으로 조회하는데 팀1에 소속된 회원만 조회하려면 회원과 연관된 팀은
엔티티를 검색 조건으로 사용해야 한다. SQL은 연관된 테이블을 조인해서 검색조건을 사용하면 된다.
private static void queryLogicJoin(EntityManager em){
String jpal = "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());
}
}
출력결과 [query] member.username = 회원1
출력결과 [query] member.username = 회원2
JPQL의 from Member m join m.team t 부분을 보면 회원이 팀과 관계를 가지고 있는 필드(m.team)를 통해서 Member 와 Team을 조인했다. 그리고 where 절을 보면 조인한 t.name을 검색조건으로 사용해서 팀1에 속한 회원만 검색했다.
참고로 :teamName과 같이 :로 시작하는 것은 파라미터를 바인딩 받는 문법이다.
private static void updateRelation(EntityManager em){
//새로운 팀2
Team team = new Team("team2","팀2");
em.persist(team2);
//회원1에 새로운 팀2 설정
Member member = em.find(Member.class, "member1");
member.setTeam(team2);
}
연관된 엔티티를 삭제하려면 기종에 있던 연관관계를 먼저 제거하고 삭제해야한다.
그렇지 않으면 외래 키 제약조건으로 인해, 데이터베이스 오류 발생
팀1에는 회원1과 회원2가 소속되어있다. 이때 팀1을 삭제하려면 연관관계를 먼저 끊자
member1.setTeam(null); //회원1 연관관계 제거
member2.setTeam(null); //회원2 연관관계 제거
em.remove(team); //팀 삭제
이번에는 반대방향인 팀에서 회원으로 접근하는 관계를 추가하자
회원 -> 팀 (Member.team)
팀 -> 회원 (Team.members)
@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;
}
}
여기까지는 위에서 처음에 설정했던 Member Class와 동일하다 바뀐것이 없다.
@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>();
}
팀과 회원은 1대N 관계다. 따라서 엔티티에 컬렉션인 Listmembers를 추가했다.
그리고 1대N관계를 매핑하기 위해 @OneToMany 매핑 정보를 사용했다.
mappedBy속성은 양방향 매핑일때 사용하는데 반대쪽 매핑의 필드 이름을 값으로 주면 된다.
반대쪽 매핑이 Member.team 이므로 team을 값으로 주었다.
이것으로 양방향 매핑을 완료
public void biDirection(){
Team team = em.find(Team.class,"team1");
List<Member> members = team.getMember(); //(팀 -> 회원)
//객체그래프 탐색
for(Member member : members){
System.out.println("member.username = " + member.getUsername());
}
}
출력결과 member.username = 회원1
출력결과 member.username = 회원2
@OneToMany 만있으면 되지 (mappedBy = "team") 왜 이것이 필요할까?
처음부터 말했듯이 객체에는 양방향 연관관계라는 것이 없다.
서로 다른 단방향 연관관계 2개를 잘 묶어서 양방향인 것처럼 보이게 할뿐이다.
다시 강조하지만 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다.
하지만 엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래키는 하나인셈
따라서 둘 사이에 차이가 발생한다.
그렇다면 둘 중 어떤 관계를 사용해서 외래키를 관리해야할까?
이런차이를 방지하기 위해 외래키를 관리하는 즉 연관관계의 주인을 선정해야한다.
Tip.
1:N관계에서 N이 오너라고 생각하자 (더 자세한 설명은 아래서)
두 연관관계 중 하나를 연관관계의 주인으로 정해야한다.
연관관계의 주인만이 데이터 베이스 연관관계와 매핑되고 외래키를 관리(등록,수정,삭제)할 수 있다.
반면에 주인이 아닌 쪽은 읽기만 할 수 있다.
구분하는 방법은 mappedBy속성을 사용하면 된다. 주인은 mappedBy속성을 사용하지 않는다.
주인이 아니면 mappedBy속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.
연관관계의 주인은 외래 키가 있는곳으로 정해야한다.
@ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy를 설정할 수 없다.
따라서 @ManyToOne에는 mappedBy 속성이 없다.
team1.getMember().add(member1); //무시(연관관계의 주인이 아님)
team1.getMember().add(member2); //무시(연관관계의 주인이 아님)
Team.members는 연관관계의 주인이 아니다. 주인이 아니 곳에 입력된 값은 외래 키에 영향을 주지 않는다.
따라서 이전 코드는 데이터베이스에 저장할 때 무시된다.
member1.setTeam(team1); //연관관계 설정(연관관계의 주인)
member2.setTeam(team1); //연관관계 설정(연관관계의 주인)
Member.team은 연관관계의 주인이다. 엔티티 매니저는 이곳에 입력된 값을 사용하여 왜래키를 관리한다.
양방향 연관관계를 설정하고 가장 흔히 하는 실수는 연관관계의 주인에는 값을 입력하지 않고,
주인이 아닌 곳에만 값을 입력하는 것이다.
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);
team1.getMembers().add(member2);
em.persist(team1);
}
이렇게 회원1,회원2를 저장하고 팀의 컬렉션에 담은 후에 팀을 저장했다.
하지만 이때 회원을 조회하면 외래키 TEAM_ID에는 team1이 아닌 null값이 저장된다.
연관관계의 주인이 아닌 Team.members 에만 값을 저장했기 때문이다.
다시한번 강조하지만 연관관계의 주인만이 외래키의 값을 변경할수 있다.
그렇다면 정말 연관관계의 주인에만 값을 저장하고 주인이 아닌 곳에는 값을 저장하지 않아도 되는걸까? 사실은 객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다.
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);
}
member1.setTeam(team1); //연관관계의 주인
team1.getMembers().add(member1); // 주인이 아니다. 저장 시 사용되지 않는다.
Member.team : 연관관계의 주인, 이 값으로 외래 키를 관리
Team.members : 연관관계의 주인이 아니다. 따라서 저장 시에 사용되지 않는다.
앞서 이야기한 것처럼 객체까지 고려해서 주인이 아닌 곳에도 값을 입력하자.
결론 : 객체의 양방향 연관관계는 양쪽 모두 관계를 맺어주자
위에 사용한 코드를 하나의 코드로 사용하자
일단 Member클래스의 setTeam() 메소드를 수정해서 사용하자
(이렇게 한 번에 양방향 관계를 설정하는 메소드를 연관관계 편의 메소드라 한다.)
public void setTeam(Team team){
this.team = team;
team.getMembers().add(this);
}
이렇게 수정하면 setTeam() 메소드 하나로 양방향 관계를 모두 설정하도록 변경했다.
//기존코드삭제//
team1.getMembers().add(member1);
team1.getMembers().add(member2);
하지만 위 setTeam() 메소드에는 버그가 있다.
member1.setTeam(teamA); //1
member2.setTeam(teamB); //2
Member findMember = teamA.getMember(); //member1이 여전히 조회된다.
teamB로 변경할때 teamA -> member1 관계를 제거하지않았다.
따라서 제거하는 코드를 추가해주어야한다.
public void setTeam(Team team){
//기존 팀과 관계를 제거
if(this.team != null){
this.team.getMembers().remove(this);
}
this.team = team;
team.getMember().add(this);
}