연관관계 매핑 기초
5. 연관관계 매핑 기초
- 연관관계 매핑
- 엔티티는 다른 엔티티와 연관관계를 형성한다.
- 객체는 참조를 사용하여 다른 객체와 관계를 형성한다.
- 데이터베이스 테이블은 외래 키를 사용하여 다른 테이블과 관계를 형성한다.
- 연관관계 매핑이란 객체의 참조와 테이블의 외래 키를 매핑하는 것을 말한다.
- 방향
- 단방향: 회원->팀 또는 팀->회원 처럼 한 쪽만 참조하는 관계를 말한다.
- 양방향: 회원->팀, 팀->회원 양쪽 모두 참조하는 관계를 말한다. 테이블 관계는 항상 양방향이다.
- 다중성
- 다대일(N:1)
- 일대다(1:N)
- 일대일(1:1)
- 다대다(N:N)
- 연관관계의 주인
- 객체를 양방향 연관관계로 만들 때 연관관계의 주인을 정해야한다.
5.1 단방향 연관관계
- 회원과 팀의 관계는 다대일(N:1) 단방향 관계이다.
- 객체 연관관계
- 객체는 Member.team 필드로 연관관계를 가진다.
- 회원 객체와 팀 객체는 단방향 관계이다. 회원은 팀에 접근가능하지만 팀은 회원에 접근할 수 없다.
- 테이블 연관관계
- 회원 테이블은 TEAM_ID 외래 키로 팀 테이블과 연관관계를 가진다.
- 회원 테이블과 팀 테이블은 양방향 관계이다.
- TEAM_ID 외래 키를 통해 회원과 팀을 조인할 수 있고 반대로 팀과 회원을 조인할 수 있다.
-- MEMBER 와 TEAM 조인
SELECT *
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
-- TEAM 과 MEMBER 조인
SELECT *
FROM TEAM T
JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID
- 객체 연관관계와 테이블 연관관계의 차이
- 참조를 통한 연관관계는 단방향이다.
- 객체의 양방향관계를 만들려면 반대쪽 객체에 필드를 추가해서 참조해야한다. 정확히 표현하면 객체의 양방향관계는 서로 다른 2개의 단방향관계이다.
- 테이블은 외래 키를 통해 양방향으로 조인이 가능하다.
- 정리
- 객체는 참조로 연관관계를 맺는다.
- 참조는 단방향이다.
- 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.
- 테이블은 외래 키로 연관관계를 맺는다.
- 외래 키는 양방향으로 조인이 가능하다.
- 객체는 참조로 연관관계를 맺는다.
5.1.3 객체 관계 매핑
- 객체의 Member.team 필드와 테이블의 MEMBER.TEAM_ID 외래 키를 매핑하는 것이 연관관계 매핑이다.
- @ManyToOne은 다대일(N:1) 관계 매핑 정보를 나타낸다.
- @JoinColumn은 외래 키를 매핑한다. name 속성에 매핑 할 외래 키 이름을 지정한다.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
// getter, setter 생략
}
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "team_id")
private Long id;
private String name;
// getter, setter 생략
}
- @JoinColumn
속성 | 기능 | 기본값 |
---|---|---|
name | 매핑할 왜래 키 이름 | 필드명 + ‘_’ + 참조하는 테이블의 기본 키 컬럼명 |
referencedColumnName | 외래 키가 참조하는 대상 테이블의 컬럼명 | 참조하는 테이블의 기본 키 컬럼명 |
foreignKey | 외래 키 제약조건을 직접 지정할 수 있다. | |
unique | @Table의 uniqueConstraints와 같지만 한 컬럼에 간단히 유니크 제약조건을 걸 때 사용한다. 만약 두 컬럼 이상을 사용해서 유니크 제약조건을 사용하려면 클래스 레벨에서 @Table.uniqueConstraints를 사용해야 한다. | |
nullable | null 값의 허용 여부를 설정한다. | true |
insertable | 엔티티 저장 시 이 필드도 같이 저장한다. false 옵션은 읽기 전용일 때 사용한다. | true |
updatable | 엔티티 수정 시 이 필드도 같이 수정한다. false 옵션은 읽기 전용일 때 사용한다. | true |
columnDefinition | 이터베이스 컬럼 정보를 직접 줄 수 있다. | |
table | 하나의 엔티티를 두 개 이상의 테이블에 매핑할 때 사용한다. 지정한 필드를 다른 테이블에 매핑할 수 있다. | 현재 클래스가 매핑된 테이블 |
- @ManyToOne
속성 | 기능 | 기본값 |
---|---|---|
targetEntity | 연관된 엔티티의 타입정보를 설정한다. 이 기능은 거의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다. | |
cascade | 영속성 전이 기능을 사용한다. | |
fetch | 글로벌 페치 전략을 설정한다. | @ManyToOne = FetchType.EAGER @OneToMany = FetchType.LAZY |
optional | false로 설정하면 연관된 엔티티가 항상 있어야 한다. 연관된 객체 즉시로딩시 false 일 경우 Inner Join 을 수행하고 true 일 경우 Outer Join 을 수행한다. https://geco.tistory.com/102 | true |
5.2 연관관계 사용
5.2.1 저장
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Team team1 = new Team();
team1.setName("team1");
em.persist(team1);
Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(team1); // 연관관계 설정 member1 -> team1
em.persist(member1);
Member member2 = new Member();
member2.setUsername("member2");
member2.setTeam(team1); // 연관관계 설정 member2 -> team2
em.persist(member2);
tx.commit();
}
catch (Exception e) {
tx.rollback();
e.printStackTrace();
}
finally {
em.close();
}
emf.close();
}
}
insert into Team (name, team_id) values(?, ?)
insert into Member (team_id, username, member_id) values (?, ?, ?)
insert into Member (team_id, username, member_id) values (?, ?, ?)
5.2.2 조회
- 객체 그래프 탐색
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Team team1 = new Team();
team1.setName("team1");
em.persist(team1);
Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(team1); // 연관관계 설정 member1 -> team1
em.persist(member1);
Member member2 = new Member();
member2.setUsername("member2");
member2.setTeam(team1); // 연관관계 설정 member2 -> team2
em.persist(member2);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member1.getId());
Team findTeam = findMember.getTeam(); // 객체 그래프 탐색
System.out.println("findTeam.getName() = " + findTeam.getName());
tx.commit();
}
catch (Exception e) {
tx.rollback();
e.printStackTrace();
}
finally {
em.close();
}
emf.close();
}
}
- 객체지향 쿼리 사용(JPQL)
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Team team1 = new Team();
team1.setName("team1");
em.persist(team1);
Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(team1); // 연관관계 설정 member1 -> team1
em.persist(member1);
Member member2 = new Member();
member2.setUsername("member2");
member2.setTeam(team1); // 연관관계 설정 member2 -> team2
em.persist(member2);
em.flush();
em.clear();
String jpql = "select m from Member m join m.team t where t.name = :teamName";
List<Member> members = em.createQuery(jpql, Member.class)
.setParameter("teamName", "team1")
.getResultList();
for (Member member : members) {
System.out.println("member.getUsername() = " + member.getUsername());
}
tx.commit();
}
catch (Exception e) {
tx.rollback();
e.printStackTrace();
}
finally {
em.close();
}
emf.close();
}
}
5.2.3 수정
- 트랜잭션 커밋할 때 플러시가 일어나면서 변경감지 기능이 동작한다.
- Member 의 Team 참조 변수 값만 변경하면 변경감지 기능에 의해 처리된다.
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Team team1 = new Team();
team1.setName("team1");
em.persist(team1);
Team team2 = new Team();
team2.setName("team2");
em.persist(team2);
Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(team1); // 연관관계 설정 member1 -> team1
em.persist(member1);
Member member2 = new Member();
member2.setUsername("member2");
member2.setTeam(team1); // 연관관계 설정 member2 -> team2
em.persist(member2);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member1.getId());
findMember.setTeam(team2); // team 수정
tx.commit();
}
catch (Exception e) {
tx.rollback();
e.printStackTrace();
}
finally {
em.close();
}
emf.close();
}
}
update Member set team_id=?, username=? where member_id=?
5.2.4 연관관계 제거
- Member 의 Team 참조 변수 값을 null으로 설정하면 연관관계가 제거된다.
Member findMember = em.find(Member.class, member1.getId());
findMember.setTeam(null);
5.2.5 연관된 엔티티 삭제
- 연관된 엔티티 삭제시 연관관계를 먼저 제거하고 삭제해야한다.
- 외래 키 제약조건 때문이다.
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Team team1 = new Team();
team1.setName("team1");
em.persist(team1);
Team team2 = new Team();
team2.setName("team2");
em.persist(team2);
Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(team1); // 연관관계 설정 member1 -> team1
em.persist(member1);
Member member2 = new Member();
member2.setUsername("member2");
member2.setTeam(team1); // 연관관계 설정 member2 -> team2
em.persist(member2);
em.flush();
em.clear();
Team findTeam = em.find(Team.class, team1.getId());
Member findMember1 = em.find(Member.class, member1.getId());
Member findMember2 = em.find(Member.class, member2.getId());
findMember1.setTeam(null); // member1 연관관계 제거
findMember2.setTeam(null); // member2 연관관계 제거
em.remove(findTeam); // team1 제거
tx.commit();
}
catch (Exception e) {
tx.rollback();
e.printStackTrace();
}
finally {
em.close();
}
emf.close();
}
}
5.3 양방향 연관관계
- 회원과 팀의 관계는 다대일(N:1) 단방향 관계이다.
- 팀과 회원은 일대다(1:N) 단방향 관계이다.
- 객체 연관관계
- 회원 객체는 Member.team 필드로 연관관계를 가진다.
- 팀 객체는 Team.members 필드로 연관관계를 가진다. 일대다(1:N) 이므로 컬렉션을 사용한다.
- 테이블 연관관계
- 회원 테이블은 TEAM_ID 외래 키로 팀 테이블과 연관관계를 가진다.
- 회원 테이블과 팀 테이블은 양방향 관계이다.
- TEAM_ID 외래 키를 통해 회원과 팀을 조인할 수 있고 반대로 팀과 회원을 조인할 수 있다.
- 단방향 연관관계의 테이블과 차이가 없다.
5.3.1 양방향 연관관계 매핑
- 팀 엔티티에 List
컬렉션을 추가한다. - 일대다 관계를 @OneToMany 으로 매핑한다.
- mappedBy 속성은 양방향 매핑일 때 반대쪽 매핑의 필드 이름을 값으로 설정한다.(여기서는 Member 엔티티의 team 필드로 설정한다.)
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
// getter, setter 생략
}
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "team_id")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
// getter, setter 생략
}
5.4 연관관계의 주인
- 객체의 양방향 연관관계는 정확히 말하면 서로 다른 2개의 단방향 연관관계로 구성되어있다.
- 데이터베이스의 테이블은 외래 키 하나로 서로 조인할 수 있다. 따라서 테이블은 외래 키 하나만으로 양방향 연관관계를 맺는다.
- 단방향 매핑시 객체의 참조는 하나이므로 이 참조로 외래 키를 관리하면된다.
- 양방향 매핑시 객체의 참조는 두개이므로 어떤 참조를 사용해서 외래 키를 관리할지 정해야한다.
- 두 객체의 연관관계 중 하나를 연관관계의 주인으로 설정하여 외래 키를 관리하도록 해야한다.
5.4.1 양방향 매핑의 규칙: 연관관계의 주인
- 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고, 외래 키를 관리(등록, 수정, 삭제) 할 수 있다.
- 주인이 아닌쪽은 읽기만 할 수 있다.
- 연관관계의 주인이 아닌쪽은 mappedBy 속성을 사용하여 속성의 값으로 연관관계의 주인을 지정한다.
- MEMBER.TEAM_ID 외래 키를 관리할 관계를 정해야 하는데, Member.team 또는 Team.members 중 하나의 관계를 연관관계의 주인으로 설정해야한다.
5.4.2 연관관계의 주인은 외래 키가 있는 곳
- 연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 한다.
- 회원 테이블이 외래 키를 가지고 있으므로 Member.team이 주인이 된다.
- 주인이 아닌 Team.members에는 mappedBy = “team” 속성을 사용하여 주인이 아님을 설정한다.
- mappedBy 속성의 값은 연관관계의 주인 필드 이름인 “team” 이다.
5.5 양방향 연관관계 저장
- 양방향 연관관계는 연관관계의 주인이 외래 키를 관리한다.
- Member 와 Team 의 관계에서 연관관계의 주인은 Member.team 이므로 엔티티 매니저는 Member.team 의 입력 값을 사용하여 외래 키를 관리한다.
Team team1 = new Team();
em.persist(team1);
Member member1 = new Member();
member1.setTeam(team1); // 연관관계 설정 member1 -> team1
em.persist(member1);
Member member2 = new Member();
member2.setTeam(team1); // 연관관계 설정 member2 -> team2
em.persist(member2);
tx.commit();
member1.setTeam(team1); // 연관관계 설정 member1 -> team1
member2.setTeam(team1); // 연관관계 설정 member2 -> team2
team1.getMembers().add(member1) // 연관관계의 주인이 아니므로 무시됨
team1.getMembers().add(member2) // 연관관계의 주인이 아니므로 무시됨
5.6 양방향 연관관계의 주의점
- 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하면 외래 키가 저장되지 않는다.
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);
Member member2 = new Member();
member2.setUsername("member2");
em.persist(member2);
Team team1 = new Team();
team1.setName("team1");
em.persist(team1);
team1.getMembers().add(member1); // 연관관계의 주인이 아닌 곳에만 연관관계 설정
team1.getMembers().add(member2); // 연관관계의 주인이 아닌 곳에만 연관관계 설정
5.6.1 순수한 객체까지 고려한 양방향 연관관계
- 연관관계의 주인에만 값을 입력하면 외래 키는 문제없이 저장된다.
- 그러나 연관관계의 주인이 아닌 곳에도 모두 값을 입력해주는 것이 좋다. 왜냐하면 객체 입장에서는 양쪽에 모두 저장되는 것이 맞기 때문이다.
- 예를 들면, JPA를 사용하지 않고 엔티티에 대한 테스트를 할 때 문제가 될 수 있다.
Team team1 = new Team();
team1.setName("team1");
em.persist(team1);
Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(team1); // 연관관계 설정(주인) member1 -> team1
team1.getMembers().add(member1); // 연관관계 설정 team1 -> member1
em.persist(member1);
Member member2 = new Member();
member2.setUsername("member2");
member2.setTeam(team1); // 연관관계 설정(주인) member2 -> team1
team1.getMembers().add(member2); // 연관관계 설정 team1 -> member2
em.persist(member2);
5.6.2 연관관계 편의 메소드
- 양방향 연관관계 설정시 양쪽의 메소드 호출 중 실수로 누락될 수 있으므로 하나의 메소드로 설정하도록 연관관계 편의 메소드를 만드는 것이 좋다.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
// 연관관계 편의 메소드
public void setTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
}
5.6.3 연관관계 편의 메소드 작성 시 주의사항
- 연관관계 편의 메소드 호출시 기존 연관관계를 제거하는 코드가 필요하다.
member1.setTeam(team1); // 1번
member1.setTeam(team2); // 2번
- 1번 코드 수행시 객체 연관관계
- 2번 코드 수행시 객체 연관관계
public class Member {
// ...
public void changeTeam(Team team) {
// 기존 팀 관계 제거
if (this.team != null) {
this.team.getMembers().remove(this);
}
this.team = team;
team.getMembers().add(this);
}
}
public class Team {
// ...
public void addMember(Member member) {
if (member.getTeam() != null) {
member.getTeam().getMembers().remove(member);
}
member.setTeam(this); // setter
members.add(member);
}
}
Leave a comment