Java - SpringJPA

Spring JPA (6) - JPA 개념 잡기 : 엔티티 연관관계 매핑 1:N, N:1, N:M, 1:1

TerianP 2022. 10. 3.
728x90

1. 연관관계 매핑 시 고려사항 3가지

1) 다중성 - 관련 어노테이션 기억!!

다중성 어노테이션 주의사항
N:1 @ManyToOne  
1:N @OneToMany  
1:1 @OneToOne  
N:M @ManyToMany 실무에서 거의 사용 X

 

2) 단방향, 양방향

테이블
   - 외래키 하나로 양쪽 조인 가능 => 사실 방향이라는 개념이 없음
객체 
   - 참조용 필드가 있는 쪽으로만 참조 가능
   - 한쪽만 참조하면 단방향
   - 양쪽이 서로 참조하면 양방향 => 얘도 사실 단방향 + 단방향 : 객체 서로서로에 대한 단방향

 

3) 연관관계의 주인

- 테이블은 외래 키 하나로 두 테이블이 연관관계를 맺음 -> 객체 양방향 관계는 A->B , B->A 처럼 참조가 2군데

- 객체 양방향 관계는 참조가 2군데 있음!! 따라서 둘중 테이블의 외래 키를 관리할 곳을 지정해야 함

- 연관관계의 주인 : 외래 키를 관리하는 쪽

- 주인의 반대편 : 외래 키에 영향을 주지 않음!! Read Only

 

4) 꼭! 기억하자

@JoinColumn 에 해당하는 컬럼은 FK 가 있는 컬럼을 의미한다.
따라서 name = "FK컬럼명" 을 매핑해주면 된다

일대다 : 다대일 -1:N, N:1 - 에서 연관관계의 주인은 항상 '다' 쪽이 외래키를 갖고, 
'다' 쪽이 연관관계의 주인이 된다

 

 

2. 다대일 N:1

1) 단방향

- 가장 흔하고, 가장 많이 사용되는 다중성

- 다대일의 반대는 일대다 N:1 -> 1:N

 

2) 양방향

- 외래키가 있는 쪽이 양방향 연관관계의 주인

- 양쪽을 서로 참조하도록 개발 -> 단방향이 2개

 

3. 일대다 1:N

1) 단방향

- 일대다 단방향은 일대다에서 일(1) 쪽이 연관관계의 주인이 됨

- 테이블 일대다 관계는 항상 다(N) 쪽에 외래키가 있음

- 객체와 테이블의 차이 때문에 반대편에서 테이블의 외래 키를 관리하는 특이한 구조

- @JoinColumn 을 꼭 사용해야함!! 그렇지 않으면 조인 테이블 방식을 사용함(중가에 테이블이 하나 추가됨)

 

엔티티가 관리하는 외래키가 다른 테이블에 존재
연관고나계 고나리를 위해 추가로 update SQL 이 실행됨
따라서 일대다 단방향보다는 다대일 양방향 매핑을 사용이 권장됨!!

2) 양방향

- 사실 양방향 일대다 매핑은 공식적으로 존재하지는 X

- 연관관계의 주인이 아닌 쪽에서 @JoinColumn(insertable=false, updatable=false) 를 사용 => 읽기 전용 필드를 사용해서 양방향 처럼 사용하는 방법

- 그냥 다대일 양방향을 사용하자 

 

4. 일대일 1:1

1) 단방향

- 일대일 관계는 그 반대도 일대일

- 주 테이블이나 대상 테이블 중 외래 키 선택 가능 => 외래키가 있는곳이 연관관계 주인

- DB 에 외래키 제약조건에 유니크 제약조건이 꼭 필요함 => 다대일에서 FK 에 유니크 제약조건이 추가된 경우

쉽게 생각하면 회원 - 회원 사물함의 관계
회원 한 명 당 회원 사물함은 하나여야만 함 => 1:1 관계

LOL 같은 게임에서 이메일 하나 당 계정 하나가 있는 것과 비슷 => 이메일 : 계정 = 1:1

Member

// Member 하나당 Locker 하나
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;

 

Locker

@Entity
public class Locker {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @OneToOne(mappedBy = "locker")
    private Member member;
}

 

2) 양방향

- 다대일 양방향 매핑과 동일하게 FK 가 있는 곳이 연관관계의 주인

- 반대편은 mappedBy 적용

 

3) 일대일 매핑 정리

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

 

5. 다대다 N:M

- 관계형 DB 는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없음

   => 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야 함

- 그런데!! 객체는 컬렉션을 사용해서 객체 2개로 다대다 관계 가능

 

- @ManyToMany 어노테이션을 사용함

- @JoinTable 를 사용해서 연결 테이블을 지정함

- 다대다 매핑도 양방향, 단방향 가능


1) 단방향

Member

// 다대다 매핑
// JoinTable 어노테이션을 사용 => name 파라미터에는 join 하는 테이블 명이 들어감 : 없으면 만들어짐
@ManyToMany
@JoinTable(name = "MEMBER_PRODUCT")
private List<Product> products = new ArrayList<>();

2) 양방향

Product 클래스에서 member 를 양방향으로 참조할 수 있도록 List 를 만든다

@Entity
public class Product {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @ManyToMany(mappedBy = "products")
    private List<Member> members = new ArrayList<>();
}

 

3) 다대다 매핑의 한계

편리해보이지만 실무에서 사용 불가능

연결 테이블이 단순히 연결만하고 끝나지 않음
=> JoinTable 가 단순히 join 을 위한 테이블로서 다루기 힘듦
 : 주문시간, 수량 같은 데이터가 들어올 수 있음

따라서 JoinTable 을 하나의 Entity 로 승격시켜서 만들어 두면 해결 가능!!

 

MemberProduct Entity

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

 

Member

// 다대다 매핑에서 memberproduct 테이블을 따로 만들어서 일대다, 다대일로 변경!!
@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProducts = new ArrayList<>();

 

Product

@Entity
public class Product {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

	// 다대다 매핑에서 memberproduct 테이블을 따로 만들어서 일대다, 다대일로 변경!!
    @OneToMany(mappedBy = "product")
    private List<MemberProduct> memberProducts = new ArrayList<>();
}

최종적으로 만들어지는 테이블 & 엔티티 연관관계

 

6. 실전 코드로 확인하기

예제 ERD

 

1) Order

public class Order {

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

    // 현재 클래스 : MEMBER => 하나의 MEMBER 에 여러 ORDER 이 올 수 있음 -> 1:N
    @ManyToOne
    @JoinColumn(name="MEMBER_ID")
    private Member member;

    // 양방향 연관관계 => 현재클래스 : OrderItem = 1: N
    @OneToMany(mappedBy = "order")
    private List<OrderItem> orderItems = new ArrayList<>();

    private LocalDateTime orderDate;

    // 주문 상태는 Enum 으로
    @Enumerated(EnumType.STRING)
    private OrderStatus status;

    // 양방향 연관관계 편의 메서드
    public void addOrderItems(OrderItem orderItem){
        orderItems.add(orderItem);
        orderItem.setOrder(this);
    }

    // Order - delivery 관계에서 order 이 연관관계의 주인
    @OneToOne
    @JoinColumn(name = "DELIVERY_ID")
    private Delivery delivery;

}

 

2) Item

@Entity
@Table
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
@Setter
public class Item {

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

    private String name;

    private int price;

    private int stockQuantity;

    // 다대다 매핑
    // joinTable 을 통해서 임의로 테이블 생성 후 join 하게 됨
    // 단 이때 연관관계의 주인은 Category 에서 가져갔음으로 여기는 mappedBy 를 사용한다
    @ManyToMany(mappedBy = "items")
    private List<Category> categories = new ArrayList<>();

}

 

3) Delivery

@Entity
public class Delivery {
    @Id
    @GeneratedValue
    @Column(name = "delivery_id")
    private Long id;

    private String city;

    private String street;

    private String zipcode;

    @Enumerated(EnumType.STRING)
    private DelverStatus status;

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

 

4) Category

@Entity
public class Category {
    @Id
    @GeneratedValue
    @Column(name ="CATEGORY_ID")
    private Long id;

    private String name;

    // JPA 는 자기 자신을 조인 가능 : 부모 카테고리
    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    private Category parent;

    // 자식 카테고리
    @OneToMany(mappedBy = "parent")
    private List<Category> child = new ArrayList<>();

    // 다대다 매핑
    // joinTable 을 통해서 임의로 테이블 생성 후 join 하게 됨
    // 이때 joinColumns 와 inverseJoinColumns 를 사용하는데
    // 각각 joinColumns 는 현재 자신의 엔티티에서 조인하는 joinColumn 을 넣어주고
    // inverseJoinColumns 에는 반대 - 다른 Many 에 해당하는 엔티티 - 엔티티에서 조인하는 joincolumn 을 넣어주면 된다    @ManyToMany
    @ManyToMany
    @JoinTable(name = "CATEGORY_ITEM",
            joinColumns = @JoinColumn(name = "CATEGORY_ID"),
            inverseJoinColumns = @JoinColumn(name = "ITEM_ID")
    )
    private List<Item> items = new ArrayList<>();
}

 

7. 다양한 연관관계 매핑 정리 : 사용하는 어노테이션과 속성

@JoinColumn : 외래 키를 매핑할 때 사용

속성 설명 기본값
name 매핑할 외래키 이름 필드명_참조 테이블 PK 컬럼명
referencedColumnName 외래키가 참조하는 대상 테이블의 컬럼명 참조하는테이블의 기본키 컬럼명
foreignKey(DDL) 외래키 제약조건을 직접 지정 가능
테이블 생성 시에만 사용
 
unique
nullable
insertable
updateable
columnDefinition
table
@Column 의 속성과 동일  

 

@ManyToOne 의 주요 속성

속성 설명 기본값
optional false 로 설정 시 연관된 엔티티가 항상 있어야 함 TRUE
fetch 글로벌 페치 전략을 설정
- 지연로딩, 즉시로딩
 
cascade 영속성 전이 기능 사용  
targetEntity 연관된 엔티티의 타입 정보를 설정.
이 기능은 거의 사용 X
컬렉션을 사용해도 제네릭 타입 정보를 확인 가능
 

 

- Reference

https://jgrammer.tistory.com/entry/JPA-%EB%8B%A4%EC%96%91%ED%95%9C-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91-1-%EB%8B%A4%EB%8C%80%EC%9D%BC-%EC%9D%BC%EB%8C%80%EB%8B%A4-1

 

댓글