5.1 단방향 연관 관계
연관 관계가 필요한 이유
- 연관 관계가 없으면 계속해서 객체를 끄집어내서 조회해야한다. => 객체 지향의 성격과 동떨어진다.
Ex)
- jpa-basic 프로젝트의 멤버 클래스
<hide/>
package hellojpa;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name= "USERNAME") // DB에 넣고 싶은 값
private String userName; // 객체에 주는 값
@Column(name= "TEAM_ID")
private Long teamId;
}
<hide/>
package hellojpa;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class Team {
@Id @GeneratedValue
@Column(name ="TEAM_ID")
private Long id;
private String name;
}
Note) 실행 결과
- 테이블 두 개가 생성된다.
- 여기서 무슨 문제가 있을까?
- 객체를 테이블에 맞춰서 모델링하면 문제가 있다.
- 테이블에 맞춰서 외래 키 값을 그대로 가지고 있기 때문이다.
Ex) 문제점
- persist()하면 pk가 세팅된 다음 영속 상태가 된다.
<hide/>
package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
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 team = new Team();
team.setName("TeamA");
em.persist(team); // persist 하면 항상 id 값이 들어간다.
Member member = new Member();
member.setUserName("member1");
member.setTeamId(team.getId());
em.persist(team); // persist 하면 항상 id 값이 들어간다.
tx.commit();
}catch (Exception e) {
tx.rollback();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
- 멤버 테이블에 TEAM_ID 값을 그대로 가지고 있다.
- 즉, 테이블에 맞춰 외래키 값을 그대로 가지고 있다는 문제가 있다.
Ex)
<hide/>
package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
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 team = new Team();
team.setName("TeamA");
em.persist(team); // persist 하면 항상 id 값이 들어간다.
//
Member member = new Member();
member.setUserName("member1");
member.setTeamId(team.getId());
em.persist(member); // persist 하면 항상 id 값이 들어간다.
tx.commit();
}catch (Exception e) {
tx.rollback();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
- persist() 순서대로 팀 쿼리 => 멤버 쿼리 나간다.
- H2 DB는 내부적으로 sequence를 쓴다.
객체를 테이블에 맞춰 데이터 중심으로 모델링하면 협력 관계를 만들 수 없다.
- 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다.
- 객체는 참조를 사용해서 연관된 객체를 찾는다.
- 테이블과 객체 사이에는 이런 큰 간격이 있다.
- 객체 지향 모델링 (객체 연관관계 사용)
Ex) 객체 지향 모델링
- 멤버 테이블에 다음과 같이 애너테이션을 추가하면 기본키 - 외래키 관계를 매핑한다.
<hide/>
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
- 다음 코드를 추가하면 JPA가 자동으로 team 이라는 PK를 꺼내서 FK로 가져온다.
member.setTeam(team);
Team findTeam = findMember.getTeam();
- persist() 하면 영속성 컨텍스트에 들어간다.
- 그래서 find() 하면 1차 캐시에서 꺼내올 수 있다. => 다음 예제는 데이터베이스에서 가져오는 방법
<hide/>
package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
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 team = new Team();
team.setName("TeamA");
em.persist(team); // persist 하면 항상 id 값이 들어간다.
Member member = new Member();
member.setUserName("member1");
member.setTeam(team);
em.persist(member); // persist 하면 항상 id 값이 들어간다.
Member findMember = em.find(Member.class, member.getId());
Team findTeam = findMember.getTeam();
System.out.println("findTeam = " + findTeam.getName());
tx.commit();
}catch (Exception e) {
tx.rollback();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
- 멤버에 대해 getTeam()을 하면 바로 팀 이름이 나오게끔 세팅완료
- 그러면 이제 객체 지향의 특성을 살릴 수 있다.
Ex) 영속성 컨텍스트 말고 데이터베이스에서 가져오려면?
<hide/>
package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
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 team = new Team();
team.setName("TeamA");
em.persist(team); // persist 하면 항상 id 값이 들어간다.
Member member = new Member();
member.setUserName("member1");
member.setTeam(team);
em.persist(member); // persist 하면 항상 id 값이 들어간다.
em.flush(); // 영속성 컨텍스트에 있는 것들에 대해 쿼리를 날린다.
em.clear(); // 영속성 컨텍스트 초기화
Member findMember = em.find(Member.class, member.getId());
Team findTeam = findMember.getTeam();
System.out.println("findTeam = " + findTeam.getName());
tx.commit();
}catch (Exception e) {
tx.rollback();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
- 아까 1차 캐시에서 가져올 때와는 다르다.
- insert 쿼리 두 개 날아가고 그 다음에 select쿼리 실행된다.
- 멤버와 팀을 조인해서 실행한다.
Ex) 연관 관계 수정하기
- 커밋하기 전에 아래 코드를 추가하면 어떤 멤버의 팀을 수정 가능
- 그럼 DB에 외래 키 값이 update 된다.
<hide/>
Team newTeam = em.find(Team.class, 100L);
findMember.setTeam(newTeam);
5.2 양방향 연관 관계와 연관 관계의 주인 (1) - 기본
양방향 연관 관계
- 테이블 연관관계는 단방향에서의 테이블 연관 관계와 똑같다.
- 테이블은 왜 전혀 변화가 없을까?
- 테이블은 외래키 하나로 양방향을 모두 이어준다. 따라서, 테이블에는 방향 개념이 없다.
- 문제는 객체이다.
- 따라서, Team에 List members를 넣어줘야 양쪽으로 파악 가능하다.
객체와 테이블 간에 연관 관계를 맺는 차이
- 객체 연관 관계: 2개
- 회원 => 팀 연관관계 1개 (단방향)
- 팀 => 회원 연관관계 1개 (단방향)
- 객체의 양방향 관계는 사실 단방향 2개라고 보는 것이 맞다.
- 테이블 연관 관계: 1개
- 회원 <=> 팀 연관관계 1개 (양방향)
- 따라서, 둘 중 하나로 외래키를 관리해야한다.
- Member의 team이 바뀌었을 때, Team의 members가 바뀌었을 때, 이 둘 중 무엇이 바뀌었을 때, MEMBER 테이블이 업데이트 되어야할까?
- "연관 관계의 주인"
연관 관계의 주인(Owner)
- 양방향 매핑 규칙
- 객체의 두 관계 중 하나를 연관 관계의 주인으로 지정한다.
- 연관 관계의 주인만이 외래 키를 관리한다. (관리: 등록, 수정)
- 주인이 아닌 쪽은 읽기만 가능
- 주인은 mappedBy 속성을 사용하면 안 된다.
- 주인이 아니면 mappedBy 속성으로 주인을 지정한다.
- 주인이 아닌 쪽에 데이터를 넣어봐야 아무 일도 일어나지 않는다. (오류도 없다.) 단순 조회만 가능
- 외래 키가 있는 곳을 주인으로 정한다.(다대일 중 "다"에 해당한다.) 멤버랑 팀 중에 멤버를 말한다.
- 현재 Member.Team이 연관 관계의 주인이다.
Ex) 양방향 연관 관계 - 반대 방향으로 객체 그래프 탐색
- Team에 다음과 같이 추가한다.
- mapped by = "team" : 에서의 team은 멤버 클래스의 변수 team을 의미한다.
<hide/>
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
- flush(), clear() 해줘야 DB에서 깔끔하게 값을 가져온다.
- 멤버 => 팀 => 멤버 ... 이를 양방향 연관관계라고 한다.
- team.getId() 에서 SELECT 쿼리가 실행된다.
<hide/>
package hellojpa;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
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 team = new Team();
team.setName("TeamA");
em.persist(team); // persist 하면 항상 id 값이 들어간다.
Member member = new Member();
member.setUserName("member1");
member.setTeam(team);
em.persist(member); // persist 하면 항상 id 값이 들어간다.
em.flush(); // 영속성 컨텍스트에 있는 것들에 대해 쿼리를 날린다.
em.clear(); // 영속성 컨텍스트 초기화
Member findMember = em.find(Member.class, member.getId());
List<Member> members = findMember.getTeam().getMembers();
for (Member m : members) {
System.out.println("m = " + m.getUserName());
}
tx.commit();
}catch (Exception e) {
tx.rollback();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
- List에 세팅한 게 없지만 그래도 출력된다.
- 왜? JPA에서
- 위에 코드에서 flush() 위에 아래 코드를 추가해야 문제가 생기지 않는다.
- 왜?
- 완전히 flush(), clear()가 되면 문제가 없다. 하지만?
team.getMembers().add(member);
- flush(), clear() 가 없으면 영속성 컨텍스트에 멤버와 아이디가 그대로 들어가 있다.
※ 중요※ 연관 관계의 주인과 mappedBy
- mappedBy를 알려면 객체와 테이블 간에 연관 관계를 맺는 차이를 이해해야한다.
5.3 양방향 연관 관계와 연관 관계의 주인 (2) - 주의점, 정리
Ex) 양방향 연관 관계
<hide/>
package hellojpa;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
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{
Member member = new Member();
member.setUserName("member1");
em.persist(member);
Team team = new Team();
team.setName("TeamA");
team.getMembers().add(member);
em.persist(team);
em.flush(); // 영속성 컨텍스트에 있는 것들에 대해 쿼리를 날린다.
em.clear(); // 영속성 컨텍스트 초기화
tx.commit();
}catch (Exception e) {
tx.rollback();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
- 연관관계의 주인에 값을 입력하지 않으면 아무 일도 일어나지 않는다.
// team.getMembers().add(member); // 읽기 전용이므로 쿼리 날라가지도 않는다.
Note) 실행 결과
- 팀 아이디가 null이다 왜그럴까?
- 연관 관계의 주인은 멤버이지 팀이 아니다.
- 따라서, 다음과 같이 코드를 수정해야한다.
- member.setTeam() 을 추가한다. => changeTeam()
<hide/>
package hellojpa;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
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 team = new Team();
team.setName("TeamA");
// team.getMembers().add(member);
em.persist(team);
Member member = new Member();
member.setUserName("member1");
member.setTeam(team);
em.persist(member);
em.flush(); // 영속성 컨텍스트에 있는 것들에 대해 쿼리를 날린다.
em.clear(); // 영속성 컨텍스트 초기화
tx.commit();
}catch (Exception e) {
tx.rollback();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과 - 다음과 같이 팀 아이디가 잘 들어간다.
- 결론: 순수힌 객체 관계를 고려하면 항상 양 쪽에 값을 입력해야한다.
- 양쪽에 모두 add() 코드를 넣어준다.
Ex) 연관 관계 편의 메서드 생성
- Mermber 클래스에 team에 대한 내용을 다음과 같이 추가할 수도 있다.
- 이렇게 하면 하나만 호출해도 양쪽의 값을 모두 가져올 수 있다.
<hide/>
public void changeTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
- 그리고 매인 클래스의 team.getMembers().add() ... 이 부분은 삭제하도록 한다.
<hide/>
package hellojpa;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
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 team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setUserName("member1");
member.changeTeam(team);
em.persist(member);
// team.getMembers().add(member); 삭제
em.flush();
em.clear();
Team findTeam = em.find(Team.class, team.getId()); // SELECT FROM TEAM
List<Member> members = findTeam.getMembers(); // SELECT FROM MEMBER
System.out.println("==========");
for(Member m : members){
System.out.println("m = " + m.getUserName());
}
System.out.println("==========");
tx.commit();
}catch (Exception e) {
tx.rollback();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
양방향 연관 관계 주의
- 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정한다.
- 연관관계 편의 메서드를 생성한다.
- 양방향 매핑 시에 무한 루프를 조심한다.
- ex) toString() lombok, JSON 생성 라이브러리(컨트롤러에서 response로 엔티티를 보내버리면 엔티티가 가진 연관 관계가 양방향일 때)
- 컨트롤러에는 엔티티를 반환하지 않도록 한다.
양방향 매핑 정리
- 단방향 매핑만으로 이미 연관관계 매핑은 완료
- 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐이다.
- JPQL에서 역방향으로 탐색할 일이 많다.
- 우선, 단방향 매핑을 잘 해두고 양방향은 나중에 필요할 때 추가해도된다. (테이블에 영향을 주지 않기 때문이다.)
5.4 실전 예제 2 - 연관 관계 매핑 시작
Ex)
- Order 테이블
<hide/>
// 외래키 값을 매핑해서 그대로 가지고 있었지만 필요 없어진다.
// @Column(name = "MEMBER_ID")
// private Long memberId;
@ManyToOne
@JoinColumn(name ="MEMBER_ID")
private Member member;
- OrderItem 클래스가 "다"에 해당한다. (하나의 Order에 여러 개의 OrderItem이 있기 때문)
- OrderItem 클래스
- 두 개의 필드
(itemId, orderId)를 지운다. - 이렇게 하면 외래 키 값을 가져오는 것이 아니라 order, item이라는 객체를 가진다.
- 따라서 getOrder(), getItem()이 가능해진다.
- 두 개의 필드
<hide/>
package jpabook.jpashop.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
@Entity
public class OrderItem {
@Id
@GeneratedValue
@Column(name ="ORDER_ITEM_ID")
private Long id;
// 필요없어진다.
// @Column(name ="ORDER_ID")
// private Long orderId;
@ManyToOne
@JoinColumn(name = "ORDER_ID")
private Order order;
// 제거
// @Column(name ="ITEM_ID")
// private Long itemId;
@ManyToOne
@JoinColumn(name = "ITEM_ID")
private Item item;
private int orderPrice;
private int count;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Order getOrder() {
return order;
}
public void setOrder(Order order) {
this.order = order;
}
public Item getItem() {
return item;
}
public void setItem(Item item) {
this.item = item;
}
public int getOrderPrice() {
return orderPrice;
}
public void setOrderPrice(int orderPrice) {
this.orderPrice = orderPrice;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
}
Note) 실행 결과
- 위 그림에서 굵게 표시된 필드는 반대 방향을 참조할 가능성이 있는 것들이다.
Ex)
- Member
<hide/
package jpabook.jpashop.domain;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String name;
private String city;
private String street;
private String zipcode;
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
public Long getId() {return id;
}
public void setId(Long id) {this.id = id;
}
public String getName() {return name;
}
public void setName(String name) {this.name = name;
}
public String getCity() {return city;
}
public void setCity(String city) {this.city = city;
}
public String getStreet() {return street;
}
public void setStreet(String street) {this.street = street;
}
public String getZipcode() {return zipcode;
}
public void setZipcode(String zipcode) {this.zipcode = zipcode;
}
}
- Order
- 연관 관계 편의 메서드 addOrderitem() 를 만든다.
<hide/>
public void addOrderItem(OrderItem orderItem){ // 양방향 연관관계
orderItems.add(orderItem);
orderItem.setOrder(this);
}
- Main
<hide/>
package jpabook.jpashop;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderItem;
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{
Order order = new Order();
em.persist(order);
OrderItem orderItem = new OrderItem();
orderItem.setOrder(order);
em.persist(orderItem);
tx.commit();
}catch (Exception e) {
tx.rollback();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
'Spring Framework > [인프런] Java ORM 표준 프로그래밍 - JPA' 카테고리의 다른 글
Chapter 07. 고급 매핑 (2) | 2022.09.06 |
---|---|
Chapter 06. 다양한 연관관계 매핑 (0) | 2022.09.05 |
Chapter 04. 엔티티 매핑 (entity mapping) (0) | 2022.09.04 |
Chapter 03. 영속성 관리 - 내부 동작 방식 (0) | 2022.09.03 |
Chapter 02. JPA 시작하기 (0) | 2022.09.03 |