객체의 참조와 테이블의 외래 키 매핑
- 엔티티들은 대부분 다른 엔티티와 연관관계가 있음
- 그런데 객체는 참조(주소)를 사용해서 관계를 맺고 테이블은 외래 키를 사용해서 관계를 맺으므로 완전히 다른 특징을 가지므로
객체의 참조와 테이블의 외래 키를 매핑하는 것이 중요 - 연관관계
- 방향 : [단방향, 양방행]
한쪽만 참조하는 단방향 관계, 서로 참조하는 양방향 관계
방향은 객체관계에만 존재하고 테이블 관계는 항상 양방향 - 다중성 : [다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:N)]
여러 회원이 한 팀에 속할 때는 다대일 관계, 한 팀에 여러 회원이 소속될 때는 일대다 관계 - 연관관계의 주인 : 객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 함
- 방향 : [단방향, 양방행]
단방향 연관관계
- 다대일 단방향 관계
- 다대일 단방향 연관관계와 분석
- 회원과 팀이 있다.
- 회원은 하나의 팀에만 소속될 수 있다.
- 회원과 팀은 다대일 관계다.
- 객체 연관관계
회원 객체는 Member.team 필드로 팀 객체와 연관관계를 맺으며 회원 객체와 팀 객체는 단방향 관계
그러므로 Member.team 필드를 통해 팀을 알 수 있지만 반대로 팀은 회원을 알 수 없음 - 테이블 연관관계
회원 테이블은 TEAD_ID 외래 키로 팀 테이블과 연관관계를 맺으며 회원 테이블과 팀 테이블은 양방향 관계
그러므로 회원 테이블의 TEAD_ID 외래 키를 통해 회원과 팀을 조인할 수 있고 반대로 팀과 회원을 조인할 수 있음
// 외래 키로 회원과 팀을 조인하는 SQL
SELECT *
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAD_ID
// 외래 키로 팀과 회원을 조인하는 SQL
SELECT *
FROM TEAM T
JOIN MEMBER M ON T.TEAM_ID = M.TEAD_ID
-
- 객체 연관관계와 테이블 연관관계의 가장 큰 차이
참조(객체)를 통한 연관관계는 언제나 단방향이므로 양방향으로 만들고 싶으면
반대쪽에도 필드를 추가해서 참조를 보관해야 하므로 결국 연관관계를 하나 더 만들어야 하는데
이것은 정확히 말하자면 양방향 연관관계가 아니라 서로 다른 단방향 관계 2개인 것임
반면에 테이블은 외래 키 하나로 양방향으로 조인 가능
- 객체 연관관계와 테이블 연관관계의 가장 큰 차이
// 단방향
class A {
B b;
}
class B {}
// 양방향
class A {
B b;
}
class B {
A a;
}
-
- 객체 연관관계 vs 테이블 연관관계 정리
- 객체는 참조(주소)로 연관관계를 맺고, 테이블은 외래 키로 연관관게를 맺음
- 연관된 데이터를 조회할 때 객체는 참조를 사용하고 테이블은 조인을 사용
- 참조를 사용하는 객체의 연관관계는 단방향, 외래 키를 사용하는 테이블의 연관관계는 양방향
객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 함
- 객체는 참조(주소)로 연관관계를 맺고, 테이블은 외래 키로 연관관게를 맺음
- 객체 연관관계 vs 테이블 연관관계 정리
- 순수한 객체 연관관계
회원과 팀 클래스는 순수한 객체 단방향, 다대일(N:1)
객체는 참조를 사용해서 연관관계를 탐색할 수 있으며, 이를 객체 그래프 탐색이라고 함
// 순수하게 객체만 사용한 연관관계
// JPA를 사용하지 않은 순수한 회원과 팀 클래스의 코드
// 회원과 팀 클래스
public class Member {
private String id;
private String username;
private Team team; // 팀의 참조를 보관
public void setTeam(Team tema) {
this.team = team;
}
// Getter, Setter ...
}
public class Team {
private String id;
private String name;
// Getter, Setter ...
}
// 회원 1과 회원2를 팀1에 소속시킴
// 그 후 회원1이 속한 팀1을 조회
public static void main(String[] args) {
// 생성자(id, 이름)
Member member1 = new Member("member1", "회원1");
Member member2 = new Member("member2", "회원2");
Team team1 = new Team("team1", "팀1");
member1.setTeam(team1);
member2.setTeam(team1);
Team findTeam = member1.getTeam(); // 객체 그래프 탐색
}
- 테이블 연관관계
데이터베이스는 외래 키를 사용해서 연관관계를 탐색할 수 있으며 이를 조인이라고 함
// 회원 테이블과 팀 테이블의 DDL
// 추가로 회원 테이블의 TEAD_ID에 외래 키 제약조건을 설정
CREATE TABLE MEMBER {
MEMBER_ID VARCHAR(255) NOT NULL,
TEAD_ID VARCHAR(255),
USERNAME VARCHAR(255),
PRIMARY KEY (MEMBER_ID)
}
CREATE TABLE TEAM {
TEAM_ID VARCHAR(255) NOT NULL,
NAME VARCHAR(255),
PRIMARY KEY (TEAM_ID)
}
ALTER TABLE MEMBER ADD CONTRAINT FK_MEMBER_TEAM
FOREIGN KEY (TEAM_ID)
REFERENCES TEAM
// 회원1과 회원2를 팀1에 소속시키는 SQL
INSERT INTO TEAM(TEAM_ID, NAME) VALUES('team1', '팀1');
INSERT TNTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME) VALUES('member1', 'team1', '회원1');
INSERT TNTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME) VALUES('member2', 'team1', '회원2');
// 회원1이 소속된 팀을 조회하는 SQL
// 조인을 통한 연관관계 탐색
SELECT T.*
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
WHERE M.MEMBER_ID = 'member1'
- 객체 관계 매핑
JPA를 사용해서 회원 객체의 Member.team 필드와 회원 테이블의 MEMBER_TEAM_ID 둘을 매핑하는 연관관계 매핑
// 매핑한 회원 엔티티
@Entity
public class Member {
@Id
@Column (name = "MEBER_ID")
private String id;
private String username;
// 연관관계 매핑
@ManyToOne // 다대일(N:1) 관계라는 매핑 정보
@JoinColumn (name = "TEAM_ID") // 외래 키를 매핑할 때 사용되며 매핑할 외래 키 이름 지정
private Team tema;
// 연관관계 설정
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 ...
}
- 연관관계 매핑 어노테이션
- @JoinColumn : 외래 키를 매핑할 때 사용
@JoinColumn의 주요 속성
- name : 매핑할 외래 키 이름으로 기본값은 필드명을 참조하는 테이블의 기본 키 컬럼명
- referenceColumnName : 외래 키가 참조하는 대상 테이블의 컬럼명으로 기본 값은 참조 테이블의 키본키 컬러명
- foreignKey(DDL) : 외래 키 제약조건을 직접 지정할 수 있으며 테이블을 생성할 때만 사용
- unique, nullable, insertable, updatable, columnDefinition, table : @Column의 속성과 같음
- @ManyToOne : 다대일 관계에서 사용
@ManyToOne의 속성
- optional : false로 설정하면 연관된 엔티티가 항상 있어야 함
- fetch : 글로벌 패치 전략으로 기본값은 @ManyToOne=FetchType.EAGER, @OneToMany=FetchType.LAZY
- cascade : 영속성 전이 기능을 사용
- targetEntity : 연관된 엔티티의 타입 정보를 설정하며 거의 사용하지 않음
- @JoinColumn : 외래 키를 매핑할 때 사용
@OneToMany
private List<Member> members; // 제네릭으로 타입 정보를 알 수 있음
@OneToMany(targetEntity=Member.class)
private List members; // 제네릭이 없으면 타입 정보를 알 수 없음
연관관계 사용
- 저장
회원 엔티티가 팀 엔티티를 참조하고 저장하면 JPA는 참조한 팀의 식별자(Team.id)를 외래 키로 사용해 등록 쿼리 생성
// 회원과 팀을 저장하는 코드
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
/* 회원 엔티티는 팀 엔티티를 참조하고 저장
-> JPA는 참조한 팀의 식별자 (Team.id)를 외래 키로 사용해서 적절한 등록 쿼리 생성 */
em.persist(member1);
// 회원2 저장
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1); // 연관관계 설정 member2 -> team1
em.persist(member1);
// 생성된 등록 쿼리
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')
// 데이터베이스 확인
SELECT M.MEMBER_ID, M.NAME, M.TEAD_ID, T.NAME AS TEAM_NAME
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
데이터베이스 확인 결과
MEMBER_ID | NAME | TEAM_ID | TEAM_NAME |
member1 | 회원1 | team1 | 팀1 |
member2 | 회원2 | team1 | 팀1 |
- 조회
연관관계가 있는 엔티티를 조회하는 2가지 방법
1) 객체 그래프 탐색 (객체 연관관계를 사용한 조회)
// member.getTeam()을 사용해서 member와 연관된 team 엔티티 조회
Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); // 객체 그래프 탐색
System.out.println("팀 이름 = " + team.getName()); // 출력결과 : 팀 이름 = 팀1
2) 객체지향 쿼리 사용 (JPQL)
// 팀1에 소속된 모든 회원을 조회하는 JPQL
// JPQL 조인 탐색
private static void queryLoginJoin(EntityManager em) {
// 회원이 팀과 관계를 가지고 있는 필드(m.team)를 통해 Member와 Team을 조인 후
// where 절을 통해 조인한 t.name을 검색조건으로 사용해 팀1에 속한 회원만 검색
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
// 실행된 SQL
SELECT M.* FROM MEMBER MBMER
INSERT JOIN
TEAM TEAM ON MEMBER.TEAM_ID = TEAM1_.ID
WHERE
TEAM1_.NAME='팀1'
- 수정
팀1 소속이던 회원을 팀2에 소속하도록 수정
// 연관관계를 수정하는 코드
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'
// 불러온 엔티티 값만 변경해두면 트랜잭션을 커밋할 때 플러시가 일어나면서 변경 감지 기능이 작동
// 그리고 변경사항을 데이터베이스에 자동으로 반영
- 연관관계 제거
회원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'
- 연관된 엔티티 삭제
연관된 엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 함
그렇지 않으면 외래 키 제약조건으로 인해, 데이터베이스에서 오류가 발생
// 팀1을 삭제하기
member1.setTeam(null); // 회원1 연관관계 제거
member2.setTeam(null); // 회원2 연관관계 제거
em.remove(team); // 팀 삭제
양방향 연관관계
- 팀에서 회원으로 접근하는 관계를 추가한 양방향 연관관계
- 객체 연관관계
회원과 팀은 다대일 관계이고, 팀에서 회원은 일대다 관계
일대다 관계는 여러 건과 연관관계를 맺을 수 있으므로 컬렉션(List, Collection, Set, Map 등)을 사용
회원 → 팀 (Member.team)
팀 → 회원 (Team.members) - 테이블 연관관계
데이터베이스 테이블은 외래 키 하나로 양방향으로 조회할 수 있음
두 테이블의 연관관계는 외래 키 하나만으로 양방향 조회가 가능하므로 처음부터 양방향 관계이기 때문에
데이터베이스에 추가할 내용은 없음
- 객체 연관관계
- 양방향 연관관계 매핑
// 매핑한 회원 엔티티
@Entity
public class Member {
@Id
@Column (name = "MEMBER_ID")
private String id;
private String username;
@ManyToOne // 다대일 관계 매핑
@JoinColum (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") // 일대다 관계 매핑, 양방향 매핑일 때 mappedBy 사용
// 회원 컬렉션
private List<Member> members = new ArrayList<Member>();
// Getter, Setter ...
}
- 일대다 컬렉션 조회
팀에서 회원 컬렉션으로 객체 그래프 탐색을 사용해서 조회한 회원들을 출력
// 일대다 방향으로 객체 그래프 탐색
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
연관관계의 주인
- @OneToMany의 mappedBy 속성
엄밀히 말하면 객체에는 양방향 연관관계라는 것이 없으며 서로 다른 단방향 연관관계 2개를 애플리케이션 로직으로 묶어놓은 것
반면에 데이터베이스 테이블은 외래 키 하나로 양쪽이 서로 조인할 수 있어 양방향 연관관계를 맺음
엔티티를 단방향으로 매핑하면 참조를 하나만 사용하므로 이 참조로 외래 키를 관리하면 되지만,
엔티티를 양방향으로 매핑하면 회원 → 팀, 팀 → 회원 두 곳에서 서로를 참조하므로 객체의 연관관계를 관리하는 포인트는 2곳
그러므로 엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나이므로 차이가 발생
이런 차이로 인해 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야하는데 이것이 연관관계의 주인 - 양방향 매핑의 규칙 : 연관관계의 주인
양방향 연관관계 매핑 시 두 연관관계 중 하나를 연관관계의 주인으로 정해야 하며,
연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제) 가능하고,
주인이 아닌 쪽은 읽기만 할 수 있으므로 연관관계의 주인을 정할 때, 주인이 아닌 쪽에 mappedBy 속성을 사용
연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것이므로
회원 테이블에 있는 TEAM_ID 외래 키를 관리할 관리자를 선택하는 것이므로
회원 엔티티에 있는 Member.team을 주인으로 선택하여 자기 테이블에 있는 외래 키인 TEAM_ID를 관리
반면, 팀 엔티티에 있는 Team.members를 주인으로 선택하면 물리적으로 전혀 다른 테이블의 외래 키를 관리해야 함
// 회원 → 팀 (Member.team) 방향
class Member {
@ManyToOne
@Column (name = "TEAM_ID")
private Team team;
...
}
// 팀 → 회원 (Team.members) 방향
class Team {
@OneToMany
private List<Member> members = new ArrayList<Member>();
...
}
- 연관관계의 주인은 외래 키가 있는 곳
연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 함
회원 테이블이 외래 키를 가지고 있으므로 Member.team이 주인이 되고,
주인이 아닌 Team.members에는 mappedBy="team" 속성을 사용해서 주인이 아님을 설정
mappedBy 속성의 값으로는 연관관계의 주인인 team(연관관계의 주인인 Member 엔티티의 team 필드)을 주면 됨
class Team {
@OneToMany(mappedBy = "team") // MappedBy 속성의 값은 연관관계의 주인인 Member.team
private List<Member> members = new ArrayList<Member>();
...
}
양방향 연관관계 저장
- 양방향 연관관계를 사용해서 팀1, 회원1, 회원2를 저장
// 데이터베이스에서 회원 테이블 조회
SELECT * FROM MEMBER;
// 양방향 연관관계 저장
public void testSave() {
// 팀1 저장
Team team1 = new Team("team1", "팀1");
em.persist(team1);
// 회원1 저장
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1); // Member.team 필드를 통해 연관관계 설정 member1 -> team1
em.persist(member1);
// 회원2 저장
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1); // Member.team 필드를 통해 연관관계 설정 member2 -> team1
em.persist(member2);
}
데이터베이스 확인 결과 : TEAM_ID 외래 키에 팀의 기본 키 값이 저장
주인이 아닌 곳에 입력된 값은 외래 키에 영향을 주지 않으며,
엔티티 매니저는 Member.team(연관관계의 주인)에 입력된 값을 사용해서 외래 키를 관리
MEMBER_ID | USERNAME | TEAM_ID |
member1 | 회원1 | team1 |
member2 | 회원2 | team1 |
양방향 연관관계의 주의점
- 양방향 연관관계를 설정하고 가장 흔히 하는 실수
연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것
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를 저장하고 팀의 컬렉션에 담은 후 팀을 저장한 후 회원 테이블을 조회
SELECT * FROM MEMBER;
회원을 조회한 결과 연관관계의 주인이 아닌 Team.members에만 값을 저장했기 때문에
외래 키 TEAM_ID에 team1이 아닌 null 값이 저장된 것을 볼 수 있음
MEMBER_ID | USERNAME | TEAM_ID |
member1 | 회원1 | null |
member2 | 회원2 | null |
- 순수한 객체까지 고려한 양방향 연관관계
사실은 객체 관점에서는 주인에만 값을 저장하는 것이 아닌, 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전
양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있음
// 순수한 객체 연관관계
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
member2.setTeam(team1); // 연관관계 설정 member2 -> team1
team1.getMembers().add(member2); // 연관관계 설정 team1 -> member2
/* 양쪽 모두 관계를 설정하면 결과가 2가 출력되지만,
setTeam만으로 연관관계 설정을 할 경우 결과가 0이 출력됨
List<Member> members = team1.getMembers();
System.out.println("members.size = " + members.size());
}
// 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.setTeam(Team)과 team.getMembers().add(member)를
각각 호출하다 보면 실수로 둘 중 하나만 호출해서 양방향이 깨질 수 있으므로 두 코드를 메소드 하나로 설정하도록 변경
public void setTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
이 후 연관관계를 설정하는 부분을 수정
// 양방향 리팩토리 전체코드
public void testORM_양방향_리팩토링() {
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);
}
- 연관관계 편의 메소드 작성 시 주의사항
하지만 위의 코드의 경우 member1을 teamB로 변경할 때 이전의 teamA → member1 관계를 제거하지 않았으므로
기존 팀이 있으면 기존 팀과 회원의 연관관계를 삭제하는 코드를 추가해야 함
public void setTeam(Team team) {
// 기존 팀과 관계를 제거
if (this.team != null) {
this.team.getMembers().remove(this);
}
this.team = team;
team.getMembers().add(this);
}
'Java-Spring > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍] 다양한 연관관계 매핑 (0) | 2022.04.04 |
---|---|
[자바 ORM 표준 JPA 프로그래밍] 연관관계 매핑 기초 - 실전 예제 (0) | 2022.03.31 |
[자바 ORM 표준 JPA 프로그래밍] 엔티티 매핑 - 실전 예제 (0) | 2022.03.26 |
[자바 ORM 표준 JPA 프로그래밍] 엔티티 매핑 (0) | 2022.03.23 |
[자바 ORM 표준 JPA 프로그래밍] 영속성 관리 (0) | 2022.03.20 |