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

Chapter 06. 다양한 연관관계 매핑

계란💕 2022. 9. 5. 21:35

6.1 다대일 (n : 1) 

 

다대일 단방향

  • 다대일 단방향은 가장 많이 사용하는 연관관계이다.
  • 다대일의 반대는 일대다

 

 

  Ex) 다대일 단방향

  • Member("다", 주인 쪽)에만 아래와 같이 표시된다. 
  • Team("일") 쪽에는 아무 표시없다.
<hide/>
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;  // 연관 관계의 주인

 

 

다대일 양방향

  • 반대 쪽에도 추가한다.
  • 양 쪽을 서로 참조하도록 개발한다.
  • 외래 키가 있는 쪽(MEMBER)이 연관 관계의 주인이다.
  • 테이블에 전혀 영향을 주지 않는다. (주인이 아니라서)

 

 

  Ex) 다대일 양방향

  • Team 클래스에도 @OneToMany를 추가한다.
  • "mappedBy"가 있으면 가짜 주인 클래스 => (mappedBy ="team")
    • mappedBy는 자신이 연관관계의 주인이 아님을 뜻한다.
    • 주인 쪽 Member클래스에서 Team의 변수명이 team이므로 괄호안에 team을 넣어준다.
  • Team 객체에서 특정 team에 속하는 모든 member를 조회하려면 List<Member> 객체가 필요하다.
  • Team =>One, List => Many
<hide/>
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();

 

 

 

6.2 일대다 (1 : n)

 

일대다 단방향

  • "일" 에 해당하는 Team에 대해 연관 관계를 매핑한다.
  • 팀은 멤버를 알고 싶은데 멤버는 팀을 모르게 하려면?
  • 그런데, 데이터베이스에는 무조건 "다" 에 해당하는 Member가 테이블로 들어가야한다.
  • 일대다 단방향은 일대다에서 "일" 에 해당하는 Team이 연관관계의 주인이다.
  • 테이블 일대다 관계는 항상 "다" 쪽에 외래 키가 있다.
  • 객체와 테이블의 차이 때문에 반대편 테이블(MEMBER)의 외래 키를 관리하는 구조가 특이하다.
  • @JoinColumn을 꼭 사용해야한다. 그렇지 않으면 조인 테이블 방식을 사용한다. (중간에 테이블을 하나 추가)
    • Tip: 강사님은 실무에서 일대다 단방향을 쓰지 않는다.
    • 운영이 어려워진다. 다대일 단방향 쓰다가 필요하면 양방향으로 쓰는 것을 권장한다.
  • 일대다 단방향의 단점: 엔티티가 관리하는 외래 키가 다른 테이블에 있다.
    • 따라서, 일대다 단방향은 연관 관계 관리를 위해 추가로 UPDATE 쿼리를 실행한다.

 

 

  Ex) 일대다 단방향

  • 연관 관계를 반대 쪽에 넣어야한다. Team에 join 컬럼을 넣는다.
  • Team 클래스에 다음과 같이 추가한다.
    • 그런데, 여기서 Team의  변수 id의 name과 같아도 되는건가?
<hide/>
@OneToMany
@JoinColumn(name ="TEAM_ID")
private List<Member> members = new ArrayList<>();

 

  • 이 부분은 팀 테이블에 insert 될 수 있는 내용이 아니다. 
  • 연관 관계가 멤버 테이블에 있기 때문이다.
  • 따라서, 연관 관계의 주인 Member 테이블의 team_id를 업데이트 해줘야한다.
team.getMembers().add(member);

 

<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);
            tx.commit();

        }catch (Exception e) {
            tx.rollback();
        }finally {
            em.close();
        }
        emf.close();    // 팩토리를 나중에 닫는다.
    }
}

 

  Note) 실행 결과

  • create table Team하고나서 update 쿼리를 실행한다.
    • update가 실행될까?
    • em.persist(team): team 엔티티를 저장하는데 team_id를 저장하려면  member 테이블을 업데이트 시키는 수밖에 없다.
    • 팀을 수정했는데 멤버가 업데이트된다.

 

 

 

  Ex) 위 예제의 Team 클래스에 @JoinColumn 애너테이션을 주석 처리하고 실행

<hide/>
@OneToMany
//    @JoinColumn(name ="TEAM_ID")
private List<Member> members = new ArrayList<>();

 

  Note) 실행 결과

  • TEAM_MEMBER라는 중간 테이블이 생성된다.
    • @JoinColumn을 붙이면 조인 테이블에 대한 설정 정보를 넣을 수 있다.
    • 그런데, 테이블이 하나 더 생기면 성능, 운영상 좋은 편은 아니다.



일대다 양방향 - 비공식

  • 공식적으로 존재하는 방법은 아니다.
  • 기존의  일대다 단방향 방식에서 멤버 클래스가 팀 클래스를 참조 가능하며 MEMBER 테이블 읽기 전용으로 매핑된다.
  • @JoinColumn(insertable = false, updatable = false)
  • 읽기 전용 필드를 사용해서 양방향처럼 사용하는 방법이다.
  • 위에서 Team의 members가 연관 관계의 주인이다.
  • 그런데 Member에서 Team을 조회하려면?

 

 

  Ex) 일대다 양방향

  • Member 클래스에 다음과 같은 필드를 추가
    • 그런데 이렇게 해버리면 연관 관계의 주인이 2개가 되버린다.
<hide/>
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;  // 연관 관계의 주인

 

  Note) 실행 결과 - 오류가 난다.

  • 오류를 막기 위해 Member 클래스의 team에 대해 @JoinColumn() 안에 옵션을 넣어준다.
    • 그러면 읽기 전용이 되버린다.
    • 매핑은 되어 있으나 insert, update는 되지 않도록 한다.
<hide/>
@ManyToOne
@JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
private Team team;  // 연관 관계의 주인

 

 

6.3 일대일 (1: 1)

 

일대일 관계

  • 일대일 관계는 그 반대도 일대일이다.
  • 주 테이블이나 대상 테이블 중에 외래 키를 선택 가능하다.
    • 주 테이블에 외래 키
    • 대상 테이블에 외래 키
  • 외래 키에 데이터베이스 유니크(UNI) 제약 조건을 추가한다.

 

 

일대일 - 주 테이블에 외래 키 단방향

  • 다대일 단방향 매핑과 유사하다. 어노테이션만 다르다.
  • 다대일 양방향 매핑처럼 외래 키가 있는 곳이 연관 관계의 주인이다. 반대편은 mappedBy 적용
  • ex) 회원 - 사물함의 관계

 

  cf) 다대일 단방향과 유사하다.

다대일 단방향

 

 

  Ex) 일대일 

  • 새로운 클래스 Locker를 만들고 멤버 테이블에 locker를 추가한다.
<hide/>
package hellojpa;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToOne;
@Entity
public class Locker {

    @Id @GeneratedValue
    private Long id;

    private  String name;

    @OneToOne(mappedBy = "locker")	// 멤버 테이블에 있는 변수 locker
    private Member member;
}
<hide/>
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;

 

 

일대일 - 대상 테이블에 외래 키 단방향

 

  • 멤버의 locker를 연관관계 주인이라 하고 싶은데  외래 키가 LOCKER에 있다?
  • Member의  locker로 LOCKER의 MEMBER_ID를 관리할 수 있을까? => 불가능하다.
  • 대상 테이블에서의 단방향 관계는 JPA에서 지원하지 않는다.
  • 양방향 관계는 지원한다.

 

 

일대일 - 대상 테이블에 외래 키 양방향

  • Locker의 member를 연관 관계 주인으로 잡고 LOCKER 테이블과 매핑한다.
  • (일대일 - 주 테이블에 외래 키 양방향) 을 뒤집은 모양이다.
  • 일대일 관계는 내가 내 것만 관리 가능하다.

 

 

일대일 정리

  • 주 테이블에 외래 키
    • 주 객체가 대상 객체의 참조를 가지는 것처럼 주 테이블에 외래 키를 두고 대상 테이블을 찾는다.
    • 객체 지향 개발자가 선호한다.
    • JPA 매핑이 편리하다.
    • 장점: 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인이 가능하다.
    • 단점: 값이 없으면 외래 키에 null을 허용한다.
  • 대상 테이블에 외래 키
    • 대상 테이블에 외래 키가 존재한다.
    • 전통적인 데이터베이스 개발자가 선호한다.
    • 장점: 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때, 테이블 구조를 유지한다.
    • 단점: 프록시 기능의 한계로 지연 로딩으로 설정해서 항상 즉시 로딩된다. 
      • 프록시: 지연 로딩으로 설정했을 때 연관된 엔티티가 있으면 프록시 객체가 대신 들어가면 되지만 연관된 엔티티가 없으면 null이 들어가야한다.

 

 

6.4 다대다 (n : m)

 

다대다 - 실무에서 쓰지 않도록 한다.

  • 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.
  • 중간 역할을 하는 연결 테이블(Member_Product)을 추가해서 일대다, 다대일 관계로 풀어내야한다.
  • @ManyToMany
  • @JoinTable로 연결 테이블을 지정한다.
  • 다대다 매핑: 단방향, 양방향 가능하다.

  • 관계형데이터베이스와 다르게 객체는 컬렉션을 사용해서 객체 2개로 다대다 관계가 가능하다.

 

 

  Ex) 다대다 매핑 (member - product)

 

  • product 클래스
<hide/>
package hellojpa;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class Product {

   @Id @GeneratedValue
   private Long id;

   private String name;

   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;
   }
}

 

  • Member클래스의 products 변수에 중간 테이블 이름 "member_product"을 넣어준다.
<hide/>
@ManyToMany
@JoinTable(name = "MEMBER_PRODUCT")
private List<Product> products = new ArrayList<>();

 

  Note) 실행 결과

  • 중간 테이블 member_product 테이블이 생긴다.

 

  • 다음과 같이 외래 키 제약 조건도 생긴다.
    • MemberProduct테이블의 product (컬럼명: products_id)
    • MemberProduct테이블의 member (컬럼명: MEMBER_ID)

GG

  • 양방향으로 만드려면?
    • Product 클래스에 다음과 같이 추가한다.
<hide/>
@ManyToMany(mappedBy = "products")
private List<Member> members = new ArrayList<>();

 

 

 

다대다 매핑의 한계

  • 편리해 보이지만 실무에서는 사용하지 않는다.
  • 연결 테이블이 단순히 연결만하고 끝나지 않는다.
    • 중간 테이블이 숨겨져 있어서 생각치 못한 쿼리가 나온다.
  • 주문 시간, 수량 같은 데이터가 들어올 수 있다. 
  • PK이면서 FK 를 만족하도록 잡는다.
    • 즉, (MEMBER_ID + PRODUCT_ID): PK
    • MEMBER_ID와 PRODUCT_ID 각각은 FK

 

 

다대다 한계 극복

  • 연결 테이블용 엔티티를 추가한다. (연결 테이블을 엔티티로 승격)
  • @ManyToMany => @OneToMany, @ManyToMany

 

  Ex)

  • 연결용 테이블 MembeProduct를 만든다.
<hide/>
package hellojpa;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
@Entity
public class MemberProduct {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name="MEMBER_ID")
    private Member member;

    @ManyToOne
    @JoinColumn(name="PRODUCT_ID")
    private Product product;
}

 

  • Product 수정 - List 안의 구성 요소를 수정한다.
<hide/>
@OneToMany(mappedBy = "product")
private List<MemberProduct> memberProducts = new ArrayList<>();

 

 

 

6.5 실전 예제 3 - 다양한 연관 관계 매핑

 

  Ex) JPA-SHOP

  • 배송, 카테고리 추가 - Entity
    • 주문과 배송은 1: 1 (@OneToMany)
    • 상품과 카테고리는 N : M (@ManyToMany)

  • 카테고리
<hide/>
package jpabook.jpashop.domain;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;

@Entity
public class Category {

    @Id @GeneratedValue
    private Long id;
    private String name;

    @ManyToOne  // 자식 입장에서 부모는 하나
    @JoinColumn(name ="PARENT_ID")
    private Category parent;  // 상위 카테고리

    @OneToMany(mappedBy = "parent")
    private List<Category> child = new ArrayList<>();
    
    @ManyToMany
    @JoinTable(name ="CATEGORY_ITEM",
                joinColumns = @JoinColumn(name ="CATEGORY_ID"),
                inverseJoinColumns = @JoinColumn(name = "ITEM_ID")
    )
    // 중간 테이블을 만든다. 내가 조인하는 건 CATEGORY_ID, 반대편 조인은 ITEM_ID
    private List<Item> items = new ArrayList<>();
}

 

  • 배송
<hide/>
package jpabook.jpashop.domain;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class Delivery {

    @Id
    @GeneratedValue
    private Long id;
    private String city;
    private String street;
    private String zipcode;
    private DeliveryStatus status;
}

 

  • 배송, 카테고리 추가 - ERD

  • item 클래스
<hide/>
@ManyToMany(mappedBy = "items")
private List<Category> categories = new ArrayList<>();

 

  • order  클래스
<hide/>
@OneToOne
@JoinColumn(name ="DELIVERY_ID")
private Delivery delivery;

 

  • 배송, 카테고리 추가 - Entity 상세

 

 

  Note) 실행 결과

<hide/>
Hibernate: 
    
    create table Category (
       id bigint not null,
        name varchar(255),
        PARENT_ID bigint,
        primary key (id)
    )
Hibernate: 
    
    create table CATEGORY_ITEM (
       CATEGORY_ID bigint not null,
        ITEM_ID bigint not null
    )
Hibernate: 
    
    create table Delivery (
       id bigint not null,
        city varchar(255),
        status integer,
        street varchar(255),
        zipcode varchar(255),
        primary key (id)
    )
Hibernate: 
    
    create table Item (
       ITEM_ID bigint not null,
        name varchar(255),
        price integer not null,
        stockQuantity integer not null,
        primary key (ITEM_ID)
    )
Hibernate: 
    
    create table Member (
       MEMBER_ID bigint not null,
        city varchar(255),
        name varchar(255),
        street varchar(255),
        zipcode varchar(255),
        primary key (MEMBER_ID)
    )
Hibernate: 
    
    create table OrderItem (
       ORDER_ITEM_ID bigint not null,
        count integer not null,
        orderPrice integer not null,
        ITEM_ID bigint,
        ORDER_ID bigint,
        primary key (ORDER_ITEM_ID)
    )
Hibernate: 
    
    create table ORDERS (
       ORDER_ID bigint not null,
        orderDate timestamp,
        status varchar(255),
        DELIVERY_ID bigint,
        MEMBER_ID bigint,
        primary key (ORDER_ID)
    )
Hibernate: 
    
    create table Team (
       MEMBER_ID bigint not null,
        city varchar(255),
        name varchar(255),
        street varchar(255),
        zipcode varchar(255),
        primary key (MEMBER_ID)
    )
Hibernate: 
    
    alter table Category 
       add constraint FK8tepc1qkmluodspg6tnliwhit 
       foreign key (PARENT_ID) 
       references Category
Hibernate: 
    
    alter table CATEGORY_ITEM 
       add constraint FKf1uerpnmn49vl1spbbplgxaun 
       foreign key (ITEM_ID) 
       references Item
Hibernate: 
    
    alter table CATEGORY_ITEM 
       add constraint FKjip0or3vemixccl6vx0kluj03 
       foreign key (CATEGORY_ID) 
       references Category
Hibernate: 
    
    alter table OrderItem 
       add constraint FKabge9eqalspcejij53rat7pjh 
       foreign key (ITEM_ID) 
       references Item


Hibernate: 
    
    alter table OrderItem 
       add constraint FKk7lmf97wukpquk6d8blxy5neq 
       foreign key (ORDER_ID) 
       references ORDERS
Hibernate: 
    
    alter table ORDERS 
       add constraint FKdbs21f1yi0coxy9y0kxw4g9jf 
       foreign key (DELIVERY_ID) 
       references Delivery
Hibernate: 
    
    alter table ORDERS 
       add constraint FKh0db7kqr88ed8hqtcqw3jkcia 
       foreign key (MEMBER_ID) 
       references Member
  • 의도한대로 CATEGORY_ITEM 테이블이 만들어진다.
  • ORDERS 테이블에도 DELIVERY_ID 필드가 들어간다.
  • 주문할 때 배송지 정보를 추가하려면 연관 관계 편의 메서드 추가하면 된다.

 

 

N : M 관계 보다는  1 : N 또는 N : 1로

  • 테이블의 N : M 관계는 중간 테이블을 이용해서 1 : N 또는 N : 1로 만든다.
  • 실전에서는 중간 테이블이 단순하지 않다.
  • @ManyToMany는 제약이 있다.
  • 실전에서는 @ManyToMany 사용하지 않는다.

 

 

@JoinColumn 속성 - (기본값)

  • @JoinColumn : 외래 키를 매핑할 때 사용 - (필드명 + _ + 참조하는 테이블의 기본 키 컬럼명)
  • name: 매핑할 외래 키 이름 - (참조하는 테이블의 기본 키 컬럼명)
  • referencedColumnName: 외래 키가 참조하는 대상 테이블의 컬럼명
  • foreignKey(DDL): 외래 키 제약 조건을 직접 지정 가능하다. 테이블 생성 시에만 사용한다.
  • unique / nullable insertable / updateable / columnDefinition /table: @Column의 속성과 같다.

 

 

@ManyToOne 속성 - (기본값)

  • optional: false로 설정하면 연관된 엔티티가 항상 있어야한다. - (TRUE)
  • fetch: 글로벌 페치 전략을 설정한다.
    •  기본값
      • @ManyToOne = FetchType.EAGER    (엔티티를 로드할 때 연관관계에 있는 엔티티 모두 가져온다. )
      • @OneToMany = FetchType.LAZY     (getter() 로 접근할때 가져온다.)
  • cascade: 영속성 전이 기능을 사용한다.
  • targetEntity: 연관된 엔티티의 타입 정보를 설정한다. 거의 사용하지 않는 기능이다. 컬렉션을 사용해서 제네릭으로 타입 정보를 알 수 있다. 옛날 버전에서 썼다. 지금은 무의미한 기능

 

 

@OneToMany 속성 

  • mappedBy: 연관 관계의 주인 필드를 선택한다.
  • fetch: 글로벌 페치 전략을 설정한다.
    •  기본값
      • @ManyToOne = FetchType.EAGER
      • @OneToMany = FetchType.LAZY
  • cascade: 영속성 전이 기능을 사용한다.
  • targetEntity: 연관된 엔티티의 타입 정보를 설정한다. 거의 사용하지 않는 기능이다. 컬렉션을 사용해서 제네릭으로 타입 정보를 알 수 있다. 옛날 버전에서 썼다. 지금은 무의미한 기능

 

 

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

 

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

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

www.inflearn.com