Spring Framework/[인프런] Java ORM 표준 프로그래밍 - JPA

Chapter 05. 연관 관계 매핑 기초

계란💕 2022. 9. 5. 16:23

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) 실행 결과

 

 

 

 

출처 - https://www.inflearn.com/course/ORM-JPA-Basic

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com