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

6) 다양한 연관관계 매핑

backend dev 2024. 5. 28.

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

• 다중성

• 단방향, 양방향

• 연관관계의 주인

 

다중성

[데이터베이스의 다중성을 기준으로 고민해서 선택하면 된다.]

• 다대일: @ManyToOne

• 일대다: @OneToMany

• 일대일: @OneToOne

• 다대다: @ManyToMany

 

 

단방향, 양방향

 

테이블

외래 키 하나로 양쪽 조인 가능

사실 방향이라는 개념이 없음

 

 

 

객체

참조용 필드가 있는 쪽으로만 참조 가능

한쪽만 참조하면 단방향

양쪽이 서로 참조하면 양방향

 

 

연관관계의 주인

 

테이블은 외래 키 하나로 두 테이블이 연관관계를 맺음

 

• 객체 양방향 관계는 A->B, B->A 처럼 참조가 2군데

 

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

 

연관관계의 주인: 외래 키를 관리하는 참조하는 필드
@JoinColumn(name = "MEMBER_ID")와 같이 외래키를 매핑한 필드가 연관관계의 주인

, 테이블구조상 외래키를 가지고있는 클래스에서 연관관계주인 필드를 가지고있으면 된다.

 

주인의 반대편: 외래 키에 영향을 주지 않음, 단순 조회만 가능

 

 

다대일 [N:1]

 

다대일 단방향

 

다대일 단방향 정리

 

가장 많이 사용하는 연관관계

• 다대일의 반대는 일대다

 

 

다대일 양방향

 

다대일 양방향 정리

외래 키가 있는 쪽이 연관관계의 주인
• 양쪽을 서로 참조하도록 개발

 

 

 

 

일대다 [1:N]

일대다는 1이 연관관계의 주인

일대다 단방향

실무에서는 거의 사용하지않는다.

 

 

 

객체 설계로는 팀은 멤버리스트를 가지고 멤버는 팀을 안가진다고 가정 [단방향]

테이블 설계상 "다" 쪽에 외래키가 들어갈수밖에 없다.

이렇게 설계가 된다면 팀의 members필드에 변경이 있을때

members에 멤버를 추가,변경,삭제한다면 해당 멤버의 외래키를 수정해줘야하니까
멤버라는 다른 테이블의 외래키를 업데이트 처리해줘야한다.

[현재 팀의 members라는 필드가 연관관계 주인이므로]

@Setter
@Getter
@ToString
@Entity
public class Team {
   ....
    @OneToMany
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<>();
}

자신의 primary key를 매핑함.

일대다 단방향 정리

 

일대다 단방향은 일대다(1:N)에서 일(1)이 연관관계의 주인

 

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

 

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

 

@JoinColumn을 꼭 사용해야 함. 그렇지 않으면 조인 테이블 방식을 사용함
(각각의 키를 들고있는 테이블을 하나 생성함)

 

 

일대다 단방향 매핑의 단점

엔티티가 관리하는 외래 키가 다른 테이블에 있음

 

연관관계 관리를 위해 추가로 UPDATE SQL 실행

 

 

 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자

 

 

일대다 양방향

 

@Entity
@Getter
@Setter
@Table(name = "MEMBER")
public class Member {

	...
    @ManyToOne
    @JoinColumn(name = "TEAM_ID", insertable=false, updatable=false)
    private Team team;

}

연관관계 주인 설정하는것처럼 하다가. insertable, updatable을 false로 해서 읽기전용으로 만든다.

연관관계 주인이 아니면 조회만 되니까. 그것처럼 설정해서 읽기전용 매핑을 한것이다.

 

일대다 양방향 정리

• 이런 매핑은 공식적으로 존재X
• @JoinColumn(insertable=falseupdatable=false)
• 읽기 전용 필드를 사용해서 양방향 처럼 사용하는 방법
• 다대일 양방향을 사용하자

 

일대다는 사용  X


일대일 [1:1]

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


• 주 테이블이나 대상 테이블 중에 외래 키 선택 가능 [ 둘중 하나에만 넣으면 된다.]
    -  주 테이블에 외래 키를 넣거나
    -  대상 테이블에 외래 키를 넣거나


• 외래 키에 데이터베이스 유니크(UNI) 제약조건 추가해야 일대일관계 가능

 

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

하나의 멤버는 하나의 락커를 가진다는 가정에서

멤버에 락커아이디를 가져도 되고, 락커에 멤버아이디를 가져도 된다.

 

다대일(@ManyToOne) 단방향 매핑과 유사하다.

 

주 테이블은 관계에서 외래키를 소유하는 테이블을 의미하며, 대상 테이블은 외래키를 통해 연결되는 테이블을 의미한다.

일대일: 주 테이블에 외래 키 양방향

락커에 멤버만 추가해주면된다. ( 락커-> 멤버 단방향 연관관계를 추가해주기만하면된다.)

 

 

다대일 양방향 매핑 처럼 외래 키가 있는 곳이 연관관계의 주인

반대편은 mappedBy 적용

 

 

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

단방향 관계는 JPA가 지원하지않는다.

양방향 관계는 지원

 

 

 

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

• 사실 일대일 주 테이블에 외래 키 양방향과 매핑 방법은 같음

 

 

주 테이블(member)에서 외래키를 가지고있고 멤버가 락커필드를 가지고있다면

개발자가 편리하다. 일반적으로 비즈니스로직에서 멤버는 거의 가져오기때문에 멤버안에 락커를 가져와서 쓰기만 하면 되기때문, 또한 비즈니스가 하나의 락커는 여러 사용자를 가진다.라고 할때 해당 설계가 맞는설계가된다.

 

대상 테이블(member)에서 외래키를 가지고있고 멤버가 락커필드를 가지고있다면

비즈니스로직이 하나의 락커가 여러 사용자를 가진다라고 할때 맞는 설계가 된다.

 

일대일 정리

주 테이블에 외래 키

 

• 주 객체가 대상 객체의 참조를 가지는 것 처럼
  주 테이블에 외래 키를 두고 대상 테이블을 찾음

 

객체지향 개발자 선호

 

JPA 매핑 편리

 

• 장점: 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능

 

단점: 값이 없으면 외래 키에 null 허용

 

주 테이블을 조회만 해도 대상테이블의 데이터까지 확인가능하다. 

추가로 대상 테이블을 조회할 필요없으므로 성능상 이점이 존재한다.

 

대상 테이블에 외래 키

 

대상 테이블에 외래 키가 존재

전통적인 데이터베이스 개발자 선호

장점: 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조 유지

단점: 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됨(프록시는 뒤에서 설명)

[ 다른 테이블에 쿼리를 이용해서 조회 해봐야하기때문에 즉시 실행된다. ]  

 

DBA와 논의를 해서 선택하면 된다고 한다.

하지만 주테이블에 외래키를 넣는것이 유리할거같다.

 


다대다 [N:M]

 

다대다는 절대사용금지

 

• 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없음

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

 

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

멤버는 프로덕트 List를 필드로 가지고, 프로덕트는 멤버 List를 필드로 가지면

객체 2개로 다대다 관계를 표현할 수 있다.

매핑할 테이블들은 연결테이블이 추가된 테이블들이여야 한다.

 

• @ManyToMany 사용

• @JoinTable로 연결 테이블 지정

• 다대다 매핑: 단방향, 양방향 가능

 

다대다 매핑의 한계

편리해 보이지만 실무에서 사용X

• 연결 테이블이 단순히 연결만 하고 끝나지 않음

• 주문시간, 수량 같은 데이터가 들어올 수 있음

 

 

@Getter
@Setter
@Entity
public class Member {
	...
    @ManyToMany
    @JoinTable(name = "MEMBER_PRODUCT")
    private List<Product> products = new ArrayList<>();
}

이렇게까지만 하면 단방향

 

product클래스에도 연관관계설정해주면 양방향

@Getter
@Setter
@Entity
public class Product {
	...
    @ManyToMany(mappedBy = "products")
    private List<Member> members = new ArrayList<>();
}

연관관계 주인은 Member클래스의 products필드

 

 

다대다 한계 극복

• 연결 테이블용 엔티티 추가(연결 테이블을 엔티티로 승격)

• @ManyToMany -> @OneToMany, @ManyToOne

 

@Getter
@Setter
@Entity
public class Member {
	...
    @OneToMany(mappedBy ="member")
    private List<MemberProduct> memberProducts = new ArrayList<>();
}

product또한 추가해주면되고 memberproduct의 연관관계주인에서 외래키 매핑해주면 된다.

 

다대다는 사용금지

 


 

다양한 연관관계 매핑 - 예제

 

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

 

• 주문과 배송은 1:1   (@OneToOne)

• 상품과 카테고리는 N:M   (@ManyToMany)

 

 

배송, 카테고리 추가 - ERD

 

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

 

 

@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<>();

}

카테고리는 부모카테고리필드만 추가해서 단방향으로 구성해도되고,

자식카테고리필드까지 추가해서 양방향으로 구성해도된다.

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


}

배달은 오더와 양방향매핑

@Entity
@Table(name = "ITEM")
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<>();

}

아이템은 카테고리와 양방향 매핑

@Entity
@Getter
@Setter
@Table(name = "MEMBER")
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<>();

}

멤버는 주문과 양방향 매핑

@Entity
@Table(name = "ORDERS")
@Getter
@Setter
public class Order {
    @Id @GeneratedValue
    @Column(name="ORDER_ID")
    private Long id;

    @ManyToOne // order 입장에서 member는 many to one ,  한사람은 여러 주문을 가질수있다. order 입장에서는 자신을 주문한 사람은 한명
    @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;

    @Enumerated(EnumType.STRING)
    private OrderStatus status;

    // 연관관계 편의 메소드
    public void addOrderItem(OrderItem orderItem) {
        orderItems.add(orderItem);
        orderItem.setOrder(this);
    }

}

 

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

테이블의 N:M 관계는 중간 테이블을 이용해서 1:N, N:1

 

• 실전에서는 중간 테이블이 단순하지 않다.

 

• @ManyToMany는 제약: 필드 추가X, 엔티티 테이블 불일치

 

실전에서는 @ManyToMany 사용X

 

 

 

@JoinColumn

• 외래 키를 매핑할 때 사용

@ManyToOne - 주요 속성

• 다대일 관계 매핑

 

다대일은 mappedby가 없다.

다인쪽이 연관관계 주인이므로

@OneToMany - 주요 속성

• 일대다 관계 매핑

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

댓글