나를 기록하다
article thumbnail
반응형

5. 다대다 [N : N]

다대다 연관관계 매핑

  • 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없음.
  • 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야함

객체는 다대다, 테이블은 일대다, 다대일 관계로 풀어냄

  • 객체는 컬렉션을 사용해서 객체 2개로 다대다 관계 가능

사용방법

  1. @ManyToMany 사용
  2. @JoinTable로 연결 테이블 지정
  3. 다대다 매핑: 단방향, 양방향 가능

다대다 매핑의 한계

  1. 편리해보이지만 실무에서 사용X
  2. 연결 테이블이 단순히 연결만 하고 끝나지 않음
  3. 주문시간, 수량 같은 데이터가 들어올 수 있음

다대다 한계 극복

  1. 연결 테이블용 엔티티 추가(연결 테이블을 엔티티로 승격)
  2. @ManyToMany → @OneToMany, @ManyToOne

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

배송, 카테고리 추가 - 엔티티

  1. 주문과 배송은 1:1(@OneToOne)
  2. 상품과 카테고리는 N:M(@ManyToMany)

엔티티 구조

배송, 카테고리 추가 - ERD

배송과 카테고리를 추가한 ERD 구조

배송, 카테고리 추가 - 엔티티 상세

엔티티 상세

N:M 관계는 1:N, N:1로 변경

  • 테이블의 N:M 관계는 중간 테이블을 이용해서 1:N, N:1로 변경한다.
  • 실전에서는 중간테이블이 단순하지 않다.
  • @ManyToMany는 제약: 필드 추가 X, 엔티티 테이블 불일치
  • 실전에서는 @ManyToMany 사용 X

@JoinColumn

  • 외래 키를 매핑할 때 사용
  • 속성
    1. name: 매핑할 외래 키 이름. 기본값 → 필드명 + _ + 참조하는 테이블의 기본 키 컬럼명
    2. referencedColumnName: 외래 키가 참조하는 대상 테이블의 컬럼명. 기본값 → 참조하는 테이블의 기본키 컬럼명
    3. foreignKey(DDL): 외래 키 제약조건을 직접 지정할 수 있다. 이 속성은 테이블을 생성할 때만 사용
    4. unique / nullable / insertable / updatable / columnDefinition / table: @Column의 속성과 같음

@ManyToOne

  • 다대일 관계 매핑
  • 속성
    1. optional: false로 설정하면 연관된 엔티티가 항상 있어야 한다. 기본값 → TRUE
    2. fetch: 글로벌 페치 전략을 설정.
      [기본값]
      1. @ManyToOne=FetchType.EAGER
      2. @OneToMany=FetchType.LAZY
    3. cascade: 영속성 전이 기능 사용
    4. targetEntity: 연관된 엔티티의 타입 정보를 설정. 이 기능은 거의 사용 X. 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있음

@OneToMany

  • 일대다 관계 매핑
  • 속성
    1. mappedBy: 연관관계의 주인 필드를 선택
    2. fetch: 글로벌 페치 전략을 설정
      [기본값]
      1. @ManyToOne=FetchType.EAGER
      2. @OneToMany=FetchType.LAZY
    3. cascade: 영속성 전이 기능 사용
    4. targetEntity: 연관된 엔티티의 타입 정보를 설정. 이 기능은 거의 사용 X. 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있음
// Member
package jpabook.jpashop.domain;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@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<>(); // 관례상 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
package jpabook.jpashop.domain;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "ORDERS") // DB는 ORDER가 예약어로 걸려있는 경우가 존재하기에 ORDERS로 많이씀
public class Order {

    @Id
    @GeneratedValue
    @Column(name = "ORDER_ID")
    private Long id;

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

    @OneToOne
    @JoinColumn(name="DELIVERY_ID")
    private Delivery delivery;

    @OneToMany(mappedBy = "order")
    private List<OrderItem> orderItems = new ArrayList<>();

    private LocalDateTime orderDate; // order_date(스프링부트 기본설정 캐멀을 언더스코어로 변경해줌)

    @Enumerated(EnumType.STRING)
    private OrderStatus status;

    public void addOrderItem(OrderItem orderItem) {
        orderItems.add(orderItem);
        orderItem.setOrder(this);
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Member getMember() {
        return member;
    }

    public void setMember(Member member) {
        this.member = member;
    }

    public LocalDateTime getOrderDate() {
        return orderDate;
    }

    public void setOrderDate(LocalDateTime orderDate) {
        this.orderDate = orderDate;
    }

    public OrderStatus getStatus() {
        return status;
    }

    public void setStatus(OrderStatus status) {
        this.status = status;
    }

}
// Delivery
package jpabook.jpashop.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToOne;

@Entity
public class Delivery
{

    @Id
    @GeneratedValue
    private Long id;

    private String city;
    private String street;
    private String zipcode;
    private DeliveryStatus status;

    @OneToOne(mappedBy = "delivery")
    private Order order;

}
// Category
package jpabook.jpashop.domain;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@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"))
    private List<Item> items = new ArrayList<>();

}
// Item
package jpabook.jpashop.domain;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
public class Item {

    @Id
    @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;

    private String name;
    private int price;
    private int stockQuantity;

    @ManyToMany(mappedBy = "items")
    private List<Category> categories = 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 int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    public int getStockQuantity() {
        return stockQuantity;
    }

    public void setStockQuantity(int stockQuantity) {
        this.stockQuantity = stockQuantity;
    }

}
// OrderItem
package jpabook.jpashop.domain;

import javax.persistence.*;

@Entity
public class OrderItem {

    @Id
    @GeneratedValue
    @Column(name = "ORDER_ITEM_ID")
    private Long id;

    @ManyToOne
    @JoinColumn(name = "ORDER_ID")
    private Order order;

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

1. 상속관계 매핑

  • 객체는 상속관계가 있지만 관계형 데이터베이스는 상속 관계 X
  • 슈퍼타입 서브타입 관계라는 모델링 기법이 객체의 상속관계와 유사
  • 상속관계 매핑: 객체의 상속 구조와 DB의 슈퍼타입 서브타입 관계를 매핑하는 것

상속관계 매핑

 

주요 어노테이션

@Inheritance(strategy=InheritanceType.XXX)

  • JOINED: 조인 전략
  • SINGLE_TABLE: 단일 테이블 전략
  • TABLE_PER_CLASS: 구현 클래스마다 테이블 전략

@DiscriminatorColumn(name=”DTYPE”)

  • DTYPE(기본값)을 넣는 것이 관례. 원하면 변경 가능

@DiscriminatorValue(”XXX”)

  • 자식 클래스에서 예를 들어 Album, Movie, Book 대신 A,M,B를 넣고 싶을 때 자식 클래스에 어노테이션을 달고 XXX에 각각 값을 대입한다. default는 엔티티 이름이다.

h2 DB 조회결과

 

em.flush()&em.clear()후 em.find()를 실행 시, 기존의 테이블이 제거x, select문이 나가지 않는 오류

https://www.inflearn.com/questions/17219/기존-테이블이-삭제되지-않는-문제

위 링크처럼 해결할 수 있고, 나는 H2 데이터베이스 1.4.200을 쓰고 있었기에 에러가 발생하였다. 따라서 1.4.199를 새로 설치하고 pom.xml 설정을 변경함으로써 문제를 해결하였다.

조인 전략(정석)

  • JpaMain
try {
            Movie movie = new Movie();
            movie.setDirector("aaaa");
            movie.setActor("bbbb");
            movie.setName("바람과 함께 사라지다");
            movie.setPrice(10000);

            em.persist(movie);

            em.flush();
            em.clear();

            Movie findMovie = em.find(Movie.class, movie.getId());
            System.out.println("findMovie = " + findMovie);

            tx.commit();
        }

  • Item
package hellojpa;

import javax.persistence.*;

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn
public class Item {

    @Id @GeneratedValue
    private Long id;

    private String name;
    private int price;

    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 int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }
}
  • Album
package hellojpa;

import javax.persistence.Entity;

@Entity
public class Album extends Item {
    private String artist;
}
  • Movie
package hellojpa;

import javax.persistence.Entity;

@Entity
public class Movie extends Item {
    private String director;
    private String actor;

    public String getDirector() {
        return director;
    }

    public void setDirector(String director) {
        this.director = director;
    }

    public String getActor() {
        return actor;
    }

    public void setActor(String actor) {
        this.actor = actor;
    }
}
  • Book
package hellojpa;

import javax.persistence.Entity;

@Entity
public class Book extends Item {
    private String author;
    private String isbn;
}
  • 출력
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    /* insert hellojpa.Movie
        */ insert 
        into
            Item
            (name, price, DTYPE, id) 
        values
            (?, ?, 'Movie', ?)
Hibernate: 
    /* insert hellojpa.Movie
        */ insert 
        into
            Movie
            (actor, director, id) 
        values
            (?, ?, ?)
Hibernate: 
    select
        movie0_.id as id2_2_0_,
        movie0_1_.name as name3_2_0_,
        movie0_1_.price as price4_2_0_,
        movie0_.actor as actor1_6_0_,
        movie0_.director as director2_6_0_ 
    from
        Movie movie0_ 
    inner join
        Item movie0_1_ 
            on movie0_.id=movie0_1_.id 
    where
        movie0_.id=?
findMovie = hellojpa.Movie@2e1792e7

 

조인전략 정리

  • 장점
    1. 테이블 정규화
    2. 외래 키 참조 무결성 제약조건 활용가능
    3. 저장공간 효율화
  • 단점
    1. 조회시 조인을 많이 사용, 성능 저하
    2. 조회 쿼리가 복잡함
    3. 데이터 저장시 INSERT SQL 2번 호출

단일 테이블 전략

단일 테이블 전략(SINGLE_TABLE)

@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
// @DiscriminatorColumn을 쓰지 않아도 DTYPE이 자동으로 생성된다.
Hibernate: 

    create table MemberProduct (
       id bigint not null,
        count integer not null,
        orderDateTime timestamp,
        price integer not null,
        MEMBER_ID bigint,
        PRODUCT_ID bigint,
        primary key (id)
    )

H2 DATABASE

 

단일 테이블 전략 정리

  • 장점
    1. 조인이 필요 없으므로 일반적으로 조회 성능이 빠름
    2. 조회 쿼리가 단순함
  • 단점
    1. 자식 엔티티가 매핑한 컬럼은 모두 null 허용(데이터 무결성이 애매하다)
    2. 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있음. 상황에 따라서 조회 성능이 오히려 느려질 수 있음.

구현 클래스마다 테이블 전략(사용하지 말 것)

조상인 Item 클래스를 abstract 클래스로 만들고, 전략을 TABLE_PER_CLASS로 변경한다.

TABLE_PER_CLASS

→ ITEM 테이블은 생성 X

데이터를 넣을 때는 상관없지만 조회할 때 상당히 느리다.

Item item = em.find(Item.class, movie.getId());
            System.out.println("item = " + item);
Hibernate: 
    select
        item0_.id as id1_2_0_,
        item0_.name as name2_2_0_,
        item0_.price as price3_2_0_,
        item0_.artist as artist1_0_0_,
        item0_.author as author1_1_0_,
        item0_.isbn as isbn2_1_0_,
        item0_.actor as actor1_6_0_,
        item0_.director as director2_6_0_,
        item0_.clazz_ as clazz_0_ 
    from
        ( select
            id,
            name,
            price,
            artist,
            null as author,
            null as isbn,
            null as actor,
            null as director,
            1 as clazz_ 
        from
            Album 
        union
        all select
            id,
            name,
            price,
            null as artist,
            author,
            isbn,
            null as actor,
            null as director,
            2 as clazz_ 
        from
            Book 
        union
        all select
            id,
            name,
            price,
            null as artist,
            null as author,
            null as isbn,
            actor,
            director,
            3 as clazz_ 
        from
            Movie 
    ) item0_ 
where
    item0_.id=?
item = hellojpa.Movie@c68a5f8

단순한 조회쿼리에 이정도의 SQL 구문이 실행된다.

→ 이 전략은 데이터베이스 설계자와 ORM 전문가 둘다 추천 X

구현 클래스마다 테이블 전략 정리

  • 장점
    1. 서브 타입을 명확하게 구분해서 처리할 때 효과적
    2. not null 제약조건 사용 가능
  • 단점
    1. 성능 느리다(UNION SQL 필요)
    2. 자식 테이블을 통합해서 쿼리하기 어려움

2. @MappedSuperclass

@MappedSuperclass

  • 공통 매핑 정보가 필요할 때 사용(id, name)
  • BaseEntity
@MappedSuperclass
public class BaseEntity {

    @Column(name = "INSERT_MEMBER")
    private String createdBy;
    private LocalDateTime createdDate;
    @Column(name = "UPDATE_MEMBER")
    private String lastModifiedBy;
    private LocalDateTime lastModifiedDate;
  • Member
public class Member extends BaseEntity
  • Team
public class Team extends BaseEntity
  • 결과
Hibernate: 
    
    create table Member (
       MEMBER_ID bigint not null,
        INSERT_MEMBER varchar(255),
        createdDate timestamp,
        UPDATE_MEMBER varchar(255),
        lastModifiedDate timestamp,
        USERNAME varchar(255),
        LOCKER_ID bigint,
        TEAM_ID bigint,
        primary key (MEMBER_ID)
    )

...

Hibernate: 
    
    create table Team (
       TEAM_ID bigint not null,
        INSERT_MEMBER varchar(255),
        createdDate timestamp,
        UPDATE_MEMBER varchar(255),
        lastModifiedDate timestamp,
        name varchar(255),
        primary key (TEAM_ID)
    )
  • 특징
  1. 상속관계 매핑을 하지 않는다.
  2. 엔티티(@Entity)가 아니다 → JPA에서 관리하지 X, 테이블과 매핑하지 않는다.
  3. 부모 클래스를 상속 받는 자식 클래스에 매핑 정보만 제공한다.
  4. 조회 및 검색이 불가하다(em.find(BaseEntity) 불가능)
  5. 직접 생성해서 사용할 일이 없으므로 추상 클래스를 권장한다.
  6. 테이블과 관계 없고, 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할을 한다.
  7. 주로 등록일, 수정일, 등록자, 수정자 같은 전체 엔티티에서 공통으로 적용하는 정보를 모을 때 사용한다.
  • 참고: @Entity 클래스는 엔티티나 @MappedSuperclass로 지정한 클래스만 상속이 가능하다.

 

3. 실전 예제 - 4. 상속관계 매핑

요구사항 추가

  • 상품의 종류는 음반, 도서, 영화가 있고 이후 더 확장될 수 있다.
  • 모든 데이터는 등록일과 수정일이 필수다.

도메인 모델

도메인 모델
테이블 설계

 

SINGLE_TABLE

  • JpaMain
try {

            Book book = new Book();
            book.setName("JPA");
            book.setAuthor("김영한");

            em.persist(book);

            tx.commit();
        }
  • Item
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn
public abstract class Item
  • 결과
Hibernate: 
    
    create table Item (
       DTYPE varchar(31) not null,
        ITEM_ID bigint not null,
        name varchar(255),
        price integer not null,
        stockQuantity integer not null,
        actor varchar(255),
        director varchar(255),
        author varchar(255),
        isbn varchar(255),
        artist varchar(255),
        etc varchar(255),
        primary key (ITEM_ID)
    )

JOINED 전략 결과

H2 DB

BaseEntity 넣고 난 후

H2 DB

Item, Member, Delivery, Order, … 모두 BaseEntity를 상속받음

반응형
profile

나를 기록하다

@prao

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

profile on loading

Loading...