단방향 관계: 회원과 팀이 관계가 있을 때 회원-> 팀 또는 팀-> 회원 둘 중 한 쪽만 참조하는 것
양방향 관계: 회원->팀, 팀->회원 양쪽 모두 서로 참조하는 것
방향은 객체관계에만 존재하고 테이블 관계는 항상 양방향이다.
다중성
[다대일(N:1), 일다대(1:N), 일대일(1:1), 다대다(N:M)]다중성이 있다.
회원과 팀이 관계가 있을 때 여러 회원은 한 팀에 속하므로 다대일 관계
한팀에 여러 회원이 소속될 수 있으므로 팀과 회원은 일대다 관계
연관관계의 주인: 객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 한다.
1. 단방향 연관관계
객체 연관관계 vs 테이블 연관관계 정리
객체 연관관계
테이블 연관관계
참조(주소)로 연관관계를 맺는다.
외래키로 연관관계를 맺는다.
a.getB().getC()
JOIN
단방향 연관관계
양방향 연관관계
참조를 통한 연관관계는 단방향
A -> B(a.b)
외래키를 사용하는 테이블의 연관관계는 양방향
A JOIN B가 가능하면 B JOIN A도 가능
객체간에 연관관계를 양방향으로 만들고 싶으면 반대쪽에도 필드를 추가해서 참조를 보관해야 한다. 결국 연관관계를 하나 더 만들어야 한다.
A -> B(a.b)
B -> A(b.a)
이렇게 양쪽에서 소로 참조하는 것을 양방향 연관관계라 한다.
정확히 이야기하면 이것은 양방향 관계가 아니라 서로 다른 단방향 관계 2개다.
// 단방향 연관관계
class A{
B b;
}
class B{}
// 양방향 연관관계
class A{
B b;
}
class B{
A a;
}
1) 순수한 객체 연관관계
JPA를 사용하지 않고 순수한 회원과 팀 클래스의 코드
// 회원 클래스
public class Member{
private String id;
private String username;
private Team team;
public void setTeam(Team team){
this.team = team;
}
// Getter, Setter ...
}
// 팀 클래스
public class Team{
private String id;
private String name;
// Getter, Setter ...
}
// 동작코드
public static void main(String[] args) {
Member member1 = new Member("member1", "회원1");
Member member2 = new Member("member2", "회원2");
Team team = new Team("team1", "팀1");
member1.setTeam(team1);
member2.setTeam(team2);
// 회원1이 속한 팀1을 조회할 수 있다.
Team findTeam = member1.getTeam();
}
객체는 참조를 사용해서 연관관계를 탐색할 수 있는데 이것을 객체 그래프 탐색이라 한다.
2) 테이블 연관관계
데이터베이스 테이블의 회원과 팀의 관계
SELECT T.*
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.ID
WHERE M.MEMBER_ID = 'member1'
데이터베이스는 외래 키를 사용해서 연관관계를 탐색할 수 있는데 이것을 조인이라 한다.
3) 객체 관계 매핑
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 ...
}
// 매핑한 팀 엔티티
@Entity
public class Team{
@Id
@Column(name = "TEAM_ID")
private String id;
private String name;
// Getter, Setter ...
}
객체 연관관계: 회원 객체의 Member.team 필드 사용
테이블 연관관계: 회원 테이블의 MEMBER.TEAM_ID 외래 키 컬럼을 사용
@ManyToOne: 이름 그대로 다대일(N:1) 관계라는 매핑 정보
@JoinColumn(name="TEAM_ID"): 조인 컬럼은 외래 키를 매핑할 때 사용
연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다.
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);
em.persist(member2);
}
회원 엔티티는 팀 엔티티를 참조하고 저장했다.
JPA는 참조한 팀의 식별자(Team.id)를 외래 키로 사용해서 적절한 등록 쿼리를 생성한다.
SQL을 보면 회원 테이블의 외래 키 값으로 참조한 팀의 식별자 값인 team1이 입력된 것을 확인할 수 있다.
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', 'team2')
2) 조회
객체 그래프 탐색(객체 연관관계를 사용한 조회)
Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); // 객체 그래프 탐색
System.out.println("팀 이름 = " + team.getName());
// 출력 결과 : 팀 이름 = 팀1
객체지향 쿼리 사용 JPQL
private static void queryLogicJoin(EntityManager 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());
}
}
// 결과: [query] member.username=회원1
// 결과: [query] member.username=회원2
JPQL의 from Member m join m.team t 부분을 보면 회원이 팀과 관계를 가지고 있는 필드(m.team)를 통해서 Member와 Team을 조인했다.
where 절을 보면 조인한 t.name을 검색조건으로 사용해서 팀1에 속한 회원만 검색했다.
이때 실행되는 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(EntityManager 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'
객체 연관관계에서 일대다 관계에서 일은 여러 건과 연관관계를 맺을 수 있으므로 컬렉션을 사용해야 한다. 따라서 Team.members를 List 컬렉션으로 추가한다.
데이터베이스 테이블은 외래 키 하나로 양방향으로 조회할 수 있다. 따라서 데이터베이스에 추가 할 내용은 없다.
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
}
// 매핑한 팀 엔티티
@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 ...
}
mappedBy 속성은 양방향 매핑일 떄 사용하는데 반대쪽 매핑의 필드 이름을 값으로 주면 된다.
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개를 애플리케이션 로직으로 잘 묶어서 양방향인 것처럼 보이게 할 뿐이다.
반면 데이터베이스 테이블은 외래 키 하나로 양쪽이 서로 조인할 수 있다. 따라서 테이블은 외래 키 하나만으로 양방향 연관관계를 맺는다.
엔티티를 단방향으로 매핑하면 참조를 하나만 사용하므로 이 참조로 외래 키를 관리하면 된다.
엔티티를 양방향으로 매핑하면 두곳에서 서로를 참조한다. 따라서 객체의 연관관계를 관리하는 포인트는 2곳으로 늘어난다.
엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나다. 따라서 둘 사이에 차이가 발생한다.
JPA에서는 두 객체 연관관계 중 하나를 정해 테이블의 외래키를 관리하는데 이것을 연관관계의 주인이라 한다.
1) 양방향 매핑의 규칙
양방향 연관관계 매핑 시 두 연관관계 중 하나를 연관관계의 주인으로 정해야 한다.
연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리 할 수 있다. 반면에 주인이 아닌 쪽은 읽기만 할 수 있다.
주인은 mappedBy 속성을 사용하지 않는다. 반면에 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.
연관관계의 주인을 정한다는 것은 외래 키 관리자를 선택하는 것아다.
2) 연관관계의 주인
연관관계의 주인은 테이블 외래 키가 있는 곳으로 정해야 한다.
데이터베이스 테이블의 다대일, 일대다 관계에서는 항상 다 쪽이 외래 키를 가진다.
다 쪽인 @ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy를 설정할 수 없다.
따라서 @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);
}
단방향 연관관계에서 살펴본 회원과 팀을 저장하는 코드와 완전 같다.
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.getMember().add(member1);
team1.getMember().add(member2);
em.persist(team1);
}
회원1, 회원2를 저장하고 팀의 컬렉션에 담은 후에 팀을 저장했다.
연관관계의 주인이 아닌 Team.members에만 값을 저장했기 떄문인데 연관관계의 주인만이 외래 키의 값을 변경할 수 있다. 따라서 TEAM_ID 외래 키의 값은 null이 저장된다.
1) 순수한 객체까지 고려한 양방향 연관관계
객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다.
양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있다.
ORM은 객체와 관계형 데이터베이스 둘 다 중요한다. 데이터베이스뿐만 아니라 객체도 함께 고려해야 한다.
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(team1); // 연관관계 설정 member2 -> team1
List<Member> members = team1.getMembers();
System.out.println("member.size = " + members.size());
// 결과: member.size = 0
}
예제 코드는 JPA를 사용하지 않는 순수한 객체다.
마지막 줄에서 팀에 소속된 회원이 몇 명인지 출력해보면 결과는 0이 나온다.
양방향은 양쪽다 관계를 설정해야 한다. 이처럼 회원 -> 팀을 설정하면 다음 코드처럼 반대방향인 팀 -> 회원도 설정해야 한다.
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);
team1.getMembes().add(member1);
member2.setTeam(team1);
team1.getMembes().add(member2);
List<Member> members = team1.getMembers();
System.out.println("member.size = " + members.size());
// 결과: members.size = 2
}
양쪽 모두 관계를 설정했다. 결과도 기대했던 2가 출력된다.
객체까지 고려하면 이렇게 양쪽 다 관계를 맺어야 한다.
public void testORM_양방향() {
// 팀1 저장
Team team1 = new Team("team1", "팀1");
em.persist(team1);
Member member1 = new Member("member1", "회원1");
// 양방향 연관관계 설정
member1.setTeam(team1);
team1.getMember().add(member1);
em.persist(member1);
Member member2 = new Member("member2", "회원2");
// 양방향 연관관계 설정
member2.setTeam(team1);
team1.getMember().add(member2);
em.persist(member2);
}
양쪽에 연관관계를 설정했다. 따라서 순수한 객체 상태에도 동작하며, 테이블의 외래 키도 정상 입력된다.
연관관계 매핑 기초
1. 단방향 연관관계
객체 연관관계 vs 테이블 연관관계 정리
1) 순수한 객체 연관관계
2) 테이블 연관관계
3) 객체 관계 매핑
@ManyToOne
: 이름 그대로 다대일(N:1) 관계라는 매핑 정보@JoinColumn(name="TEAM_ID")
: 조인 컬럼은 외래 키를 매핑할 때 사용4)
@JoinColumn
이 속성은 테이블을 생성할 때만 사용한다.
nullable
insertable
updatable
columnDefinition
table
@JoinColumn 생략
@JoinColumn
을 생략하면 외래 키를 찾을 떄 기본 전략을 사용한다.5)
@ManyToOne
@ManyToOne=FetchType.EAGER
@OneToMany=FetchType.LAZY
이 기능은 거의 사용하지 않는다.
컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다.
2. 연관관계 사용
1) 저장
2) 조회
3) 수정
4) 연관관계 제거
5) 연관된 엔티티 삭제
3. 양방향 연관관계
1) 양방향 연관관계 매핑
4. 연관관계의 주인
1) 양방향 매핑의 규칙
2) 연관관계의 주인
@ManyToOne
은 항상 연관관계의 주인이 되므로 mappedBy를 설정할 수 없다.@ManyToOne
에는 mappedBy 속성이 없다.5. 양방향 연관관계 저장
6. 양방향 연관관계의 주의점
1) 순수한 객체까지 고려한 양방향 연관관계
2) 연관관계 편의 메소드
3. 연관관계 편의 메소드 작성 시 주의사항