0+ 스프링/0 + 스프링 ORM(JPA)

[JPA] 연관관계 매핑(@ManyToOne, @OneToMany, @OneToOne, @ManyToMany)

힘들면힘을내는쿼카 2023. 2. 20. 01:05
728x90
반응형

[JPA] 연관관계 매핑(@ManyToOne, @OneToMany, @OneToOne, @ManyToMany)

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의
이 글은 인프런에서 제공하는 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 참고했고
강의 내용을 다시 복습하면서 정리하려는 목적으로 작성합니다.

 

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

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

www.inflearn.com

앞에서 우리는 연관관계 매핑에 관련해서 찍먹해봤습니다.
이번 포스팅에서는 연관관계에 대해서 본격적으로 알아봅시다!

 

 

매핑에 사용되는 주요 애노테이션

@JoinColumn

외래 키를 매핑할 때 사용합니다.

 

@ManyToOne

다대일 관계 매핑시 사용합니다.

 

@OneToMany

일대다 관계 매핑시 사용합니다.
@ManyToOnemappedBy가 없는것을 확인할수 있습니다.
이말은 @ManyToOne을 사용하면 반드시 연관관계 주인이 되어야 한다는 의미 입니다.

연관관계 매핑시 고려사항

ORM은 객체와 테이블간의 패러다임 불일치를 해결하기 위해서 등장했습니다.

테이블과 객체의 패러다임 불일치

  • 테이블
    • 외래 키 하나로 연관된 테이블 탐색
  • 객체
    • 참조를 이용하여 연관된 객체 탐색
    • 참조용 필드가 있는 쪽으로만 참조가 가능

이처럼 테이블은 외래 키 하나로 테이블간에 연관관계를 형성합니다.
하지만, 객체는 참조를 사용하기 때문에 참조용 필드가 있어야 합니다.
이때 참조용 필드가 한쪽에만 설정되어 있으면 단방향 관계,
양쪽에 설정되어 있다면 우리는 양방향 관계라고 말할 수 있습니다.
(단방향 설정만 해도 우리는 테이블 매핑이 완료됨을 인지하고 있어야 합니다.🤗)

양방향 관계는 외래키를 관리하는 곳을 연관관계의 주인으로 설정해야 합니다.
이때 연관관계의 주인이 아닌곳에 mappedBy를 설정 하게 되며,
연관관계 주인은 외래키를 관리하는 엔티티에 설정하면 됩니다.
외래 키를 관리하는 곳은 항상 다(N)쪽임을 우리는 테이블을 통해 알수 있습니다…!

단방향 관계

참조용 필드가 한쪽에만 설정되어 있으면 단방향 관계

@Entity
@Getter @Setter
public class Member {
    @Id
    private Long id;
    /**
     * 다대일 관계
     * 여러명의 회원은 하나의 팀에 소속 될 수 있음
     */
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;
}
@Entity
@Getter @Setter
public class Team {
    @Id
    private Long id;
    private String name;
}

양방향 관계

양쪽에 설정되어 있다면 우리는 양방향 관계

@Entity
@Getter @Setter
// 연관관계의 주인(외래 키를 관리하는 곳)
public class Member {
    @Id
    private Long id;
    /**
     * 다대일 관계
     * 여러명의 회원은 하나의 팀에 소속 될 수 있음
     */
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;
}
@Entity
@Getter @Setter
// 외래 키에 영향을 주지 않음(단순 조회만 가능)
public class Team {
    @Id
    private Long id;
    private String name;
    /**
     * 일대다 관계
     * 하나의 팀에 여러명의 회원이 있음
     */
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
}

양방향 관계 설정시 주의 사항

양방향 관계를 설정하면 반드시 양쪽의 참조 값에 데이터를 넣어야 합니다.
이러한 실수를 방지하기 위해, 일반적으로 연관관계의 주인쪽에 연관관계 편의 메소드를 생성하여 관리 합니다.

@Entity
@Getter @Setter
public class Member {
    @Id
    private Long id;
    /**
     * 다대일 관계
     * 여러명의 회원은 하나의 팀에 소속 될 수 있음
     */
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

    // 연관관계 편의 메소드
    public void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}

다대일(N:1)[@ManyToOne]

단방향

가장 많이 사용하는 연관관계 입니다.
당연하겠지만, 다대일(N:1)의 반대는??? 일대다(1:N) 입니다.^^

팀과 회원의 관계로 이야기 해봅시다.
하나의 팀에는 여러명의 회원이 존재할수 있습니다.
그렇게 되면 당연하게도 외래 키(team_id)는 회원쪽에 있어야 합니다.
그래야 동일한 외래 키(team_id)를 공유한 회원이 존재할수 있겠죠?

 

 

페이커와 뱅기는 team_id1이기 때문에 SK T1소속이고,
매드라이프는 team_id2이기 때문에 CJ Frost 소속 입니다.

@Entity
@Getter @Setter
public class Member {
    @Id @GeneratedValue
    private Long id;
    @Column(name = "username")
    private String name;
    /**
     * 다대일 관계
     * 여러명의 회원은 하나의 팀에 소속 될 수 있음
     */
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;
}
@Entity
@Getter @Setter
public class Team {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
}

이렇게 한 후에 반드시 MemberTeam의 연관관계를 주입해줘야 합니다.
(member.setTeam(team); 같은 로직을 넣으셔야 합니다.)

양방향

외래 키가 있는 쪽이 연관관계의 주인 입니다.
양쪽을 서로 참조하도록 개발(단방향이 2개)하면 됩니다.

Member에서 이미 Team을 참조하고 있으니, TeamEntity만 수정하면 됩니다.
mappedBy를 통해 Team이 연관관계의 주인이 아니라는 의미(단순 읽기만 가능)를 명시해 줍니다.

@Entity
@Getter @Setter
public class Team {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    /**
     * 일대다 관계
     * 하나의 팀에 여러명의 회원이 있음
     * Team에서 member는 단순 읽기만 가능
     */
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
}

MemberEntity에서 반드시 연관관계를 주입해줘야 합니다.
Member에서 Team의 연관관계를 주입하고, Team에서 Member의 연관관계를 주입해야 합니다.
(객체 지향적으로 생각해보세요.! TeamMember를 주입해줘야 Team이 알맞은 Member를 탐색할수 있겠죠?)
둘 중 하나를 빼먹을수 있기 때문에 연관관계 편의 메소드를 작성하여 사용하는 것을 추천 드립니다.

@Entity
@Getter @Setter
public class Member {
    @Id @GeneratedValue
    private Long id;
    @Column(name = "username")
    private String name;
    /**
     * 다대일 관계
     * 여러명의 회원은 하나의 팀에 소속 될 수 있음
     */
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

    // 연관관계 편의 메소드
    public void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}

일대다(1:N)[@OneToMany]

단방향

일(1)이 연관관계의 주인 입니다.
그런데, 테이블 일대다 관계는 항상 다(N) 쪽에 외래 키가 있습니다.
우리는 외래 키가 있는 곳을 연관관계의 주인으로 설정하라고 배웠습니다..!! 🤷🏻‍♂️


객체와 테이블의 차이 때문에 반대편 테이블의 외래 키를 관리하는 특이한 구조를 가지게 됩니다.
(객체는 Team에서 외래 키를 관리하고, 테이블은 Member에서 외래 키를 관리합니다.)
이럴때는 @JoinColumn을 반드시 사용해야 합니다.

@Entity
@Getter @Setter
public class Member {
    @Id @GeneratedValue
    private Long id;
    @Column(name = "username")
    private String name;
}
@Entity
@Getter @Setter
public class Team {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    /**
     * 일대다 단방향 매핑
     * 외래키를 Team에서 관리
     */
    @OneToMany
    @JoinColumn(name = "team_id")
    private List<Member> members = new ArrayList<>();
}
// 회원 저장
Member member1 = new Member();
member1.setName("페이커");
em.persist(member1);

// 팀 저장
Team team1 = new Team();
team1.setName("SK T1");
/**
 * 객체는 Team에서 외래 키 관리
 * 테이블은 Member에서 외래 키 관리
 */
team1.getMembers().add(member1);
em.persist(team1);

 

연관관계 관리를 위해 UPDATE SQL 실행

Hibernate: 
    /* insert com.study.purejpa.entity.Member
        */ insert 
        into
            Member
            (username, id) 
        values
            (?, ?)
Hibernate: 
    /* insert com.study.purejpa.entity.Team
        */ insert 
        into
            Team
            (name, id) 
        values
            (?, ?)
Hibernate: 
    /* create one-to-many row com.study.purejpa.entity.Team.members */ 
update
        Member 
    set
        team_id=? 
    where
        id=?

일대다 단방향 매핑을 설정하면 다음과 같은 단점이 생깁니다.

  • Entity가 관리하는 외래 키가 다른 테이블에 존재
  • 연관관계 관리를 위해 추가로 UPDATE SQL 실행
    • em.persist(team)을 실행하는데 Team 테이블에는 외래 키가 없습니다.!
    • 따라서 Member 테이블에 UPDATE SQL을 실행할 수 밖에 없습니다.

 

일대다(1:N) 단방향 매핑 보다는 다대일(N:1) 양방향 매핑을 사용!

 

양방향

위 그림과 같은 매핑은 공식적으로 존재하지 않습니다.
@JoinColumn(insertable = false, updatable = false) 사용하면 매핑을 할수 있긴 합니다.....
(읽기 전용으로 만든다.)
읽기 전용 필드를 사용해서 양방향 처럼 사용하는 방법 입니다.

@Entity
@Getter @Setter
public class Member {
    @Id @GeneratedValue
    private Long id;
    @Column(name = "username")
    private String name;
    /**
     * 일대다 양방향
     */
    @ManyToOne
    @JoinColumn(name = "team_id", 
            insertable = false, updatable = false)
    private Team team;
}
@Entity
@Getter @Setter
public class Team {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    /**
     * 일대다 단방향 매핑
     * 외래키를 Team에서 관리
     */
    @OneToMany
    @JoinColumn(name = "team_id")
    private List<Member> members = new ArrayList<>();
}

 

다대일(N:1) 양방향 매핑을 사용합시다.!

 

일대일(1:1)[@OneToOne]

일대일(1:1) 관계는 그 반대도 일대일(1:1) 입니다!
이러한 이유로 주 테이블(Member)이나 대상 테이블(Locker) 중 외래 키를 선택할 수 있습니다.
외래 키에 데이터베이스 유니크 제약 조건을 추가 해야 합니다.

단방향

@ManyToOne 단방향 매핑과 유사합니다.

@Entity
@Getter @Setter
public class Member {
    @Id @GeneratedValue
    private Long id;
    @Column(name = "username")
    private String name;
    @OneToOne
    @JoinColumn(name = "locker_id")
    private Locker locker;
}
@Entity
@Getter @Setter
public class Locker {
    @Id @GeneratedValue
    private Long id;
    private String name;
}

양방향

외래 키가 있는 곳이 연관관계의 주인

@Entity
@Getter @Setter
public class Member {
    @Id @GeneratedValue
    private Long id;
    @Column(name = "username")
    private String name;
    @OneToOne
    @JoinColumn(name = "locker_id")
    private Locker locker;
}

mappedBy를 사용하여 연관관계의 주인이 아닌것을 명시합니다.

@Entity
@Getter @Setter
public class Locker {
    @Id @GeneratedValue
    private Long id;
    private String name;
    // 읽기 전용
    @OneToOne(mappedBy = "locker")
    private Member member;
}

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


이렇게는 불가합니다.

하지만 양방향 관계는 지원 합니다.


양방향 이기 때문에 LockerMember를 연관관계의 주인으로 잡아서 매핑하면 됩니다.

 

주 테이블에 외래 키

  • 주 객체가 대상 객체의 참조를 가지는 것 처럼
  • 주 테이블에 외래 키를 두고 대상 테이블을 탐색
  • 객체지향 개발자가 선호
    • JPA 매핑 편리
    • 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능
      • 값이 없으면 외래 키에 null

 

대상 테이블에 외래 키

  • 대상 테이블에 외래 키가 존재
  • 데이터베이스 개발자 선호
    • 주 테이블과 대상 테이블을 일대일(1:1)에서 일대다(1:N) 관계로 변경할 때 테이블 구조 유지 가능
      • 대상 테이블의 유니크 제약 조건을 해제하면 됨(하나의 회원에 여러개의 라커)
    • 프록시 기능의 한계로 즉시로딩 됨(지연로딩이 불가)

 

다대다(N:M)[@ManyToMany]

실무에서는 사용하는것을 지양함.

관계형 데이터베이스는 정규화된 테이블 2개로 다대다(N:M) 관계를 표현할수 없습니다.

 

연결 테이블을 추가해서 일대다(1:N), 다대일(N:1) 관계로 풀어내야 합니다.

 

반면에 객체는 컬렉션을 사용해서 객체 2개로 다대다(N:M) 관계를 표현할수 있습니다.

@ManyToMany를 사용하여 매핑할 수 있습니다.
@JoinTable로 연결 테이블을 지정 합니다.
단방향, 양방향 가능 합니다.

단방향

@Entity
@Getter @Setter
public class Member {
    @Id @GeneratedValue
    private Long id;
    @Column(name = "username")
    private String name;
    // member_product 연결테이블 생성
    @ManyToMany
    @JoinTable(name = "member_product")
    private List<Product> products = new ArrayList<>();
}
@Entity
@Getter @Setter
public class Product {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
}

 

member_product 연결 테이블이 생성된것을 확인 할수 있습니다.

Hibernate: 

    create table Member (
       id bigint not null,
        username varchar(255),
        team_id bigint,
        primary key (id)
    )
Hibernate: 

    create table member_product (
       Member_id bigint not null,
        products_id bigint not null
    )
Hibernate: 

    create table Product (
       id bigint not null,
        name varchar(255),
        primary key (id)
    )

 

양방향

@Entity
@Getter @Setter
public class Member {
    @Id @GeneratedValue
    private Long id;
    @Column(name = "username")
    private String name;
    // member_product 연결테이블 생성
    @ManyToMany
    @JoinTable(name = "member_product")
    private List<Product> products = new ArrayList<>();
}
@Entity
@Getter @Setter
public class Product {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    @ManyToMany(mappedBy = "products")
    private List<Member> members = new ArrayList<>();
}

오 굉장히 편해보이는데???

그런데 사용하면 안됩니다. 안됩니다. 안됩니다. 안됩니다.

 

다대다 매핑의 한계

편리해 보이지만 실무에서 사용할수 없다.😿
연결 테이블은 단순히 연결만 하고 끝나지 않기 때문 입니다!!!!!!!!!!!!
쉽게 이야기 해서 주문시간, 수량 같은 추가 데이터가 들어올수 있기 때문입니다.

그렇다면 다대다(N:M) 관계는 포기해야하는 것 일까? 🤔

 

다대다 한계 극복

포기하기에 이릅니다!!!!
쉽게 생각하면 됩니다.
테이블에서 다대다(N:M) 관계는 일대다(1:N) 다대일(N:1)의 관계로 풀었습니다.
객체도 마찬가지로 일대다(1:N) 다대일(N:1)의 관계로 풀면 됩니다.

@ManyToMany ➡️ @OneToMany, @ManyToOne
MemberProduct 사이에 Order를 넣어서 해결해 봅시다.

 

양방향

@Entity
@Getter @Setter
public class Member {
    @Id @GeneratedValue
    private Long id;
    @Column(name = "username")
    private String name;
    @OneToMany(mappedBy = "member")
    private List<Order> orders;
}
@Entity
@Getter @Setter
@Table(name = "orders")
public class Order {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Integer orderAmount;
    private LocalDateTime orderDate;
    @ManyToOne
    @JoinColumn(name = "member_id")
    private Member member;
    @ManyToOne
    @JoinColumn(name = "product_id")
    private Product product;
}
@Entity
@Getter @Setter
public class Product {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    @OneToMany(mappedBy = "product")
    private List<Order> orders = new ArrayList<>();
}
Hibernate: 

    create table Member (
       id bigint not null,
        username varchar(255),
        team_id bigint,
        primary key (id)
    )
Hibernate: 

    create table orders (
       id bigint generated by default as identity,
        orderAmount integer,
        orderDate timestamp,
        member_id bigint,
        product_id bigint,
        primary key (id)
    )
Hibernate: 

    create table Product (
       id bigint not null,
        name varchar(255),
        primary key (id)
    )

 

단방향
mappedBy를 제거하면 됩니다.^^

@Entity
@Getter @Setter
public class Member {
    @Id @GeneratedValue
    private Long id;
    @Column(name = "username")
    private String name;
}
@Entity
@Getter @Setter
public class Product {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
}

 

실전예제

배운 내용을 토대로 다음과 같은 테이블을 Entity로 매핑해볼까요? 🧑‍💻

Member

@Entity
@Setter @Getter
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;
    private String city;
    private String street;
    private String zipcode;
}

Order

@Entity
@Getter @Setter
@Table(name = "orders")
public class Order {
    @Id @GeneratedValue
    private Long id;
    private LocalDateTime orderDate;
    @Enumerated(EnumType.STRING)
    private OrderStatus status;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

    /**
     * 일대일 관계는 외래 키를 양쪽 어디에나 매핑 가능
     * order에 외래 키를 두면 성능(바로 확인 가능, 프록시), 객체 입장에서 편함
     */
    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "delivery_id")
    private Delivery delivery;

    // 연관관계 주인 필드를 선택
    @OneToMany(mappedBy = "order")
    private List<OrderItem> orderItems = new ArrayList<>();
}

Delivery

@Entity
@Getter @Setter
public class Delivery {
    @Id @GeneratedValue
    private Long id;
    private String city;
    private String street;
    private String zipcode;
    /**
     * status를 enum 타입으로 설정
     */
    @Enumerated(EnumType.STRING)
    private DeliveryStatus status;

    /**
     * 양방향으로 설정
     * 연관관계 주인 필드를 선택
     */
    @OneToOne(mappedBy = "delivery")
    private Order order;
}

OrderItem

@Entity
@Setter @Getter
public class OrderItem {
    @Id @GeneratedValue
    private Long id;
    @ManyToOne
    @JoinColumn(name = "item_id")
    private Item item;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;
}

Item

@Entity
@Getter @Setter
public class Item {
    @Id @GeneratedValue
    private Long id;
    private String name;
    private Integer price;
    private Integer stockQuantity;

    // 연관관계 주인 필드를 선택
    @OneToMany(mappedBy = "item")
    private List<OrderItem> orderItem = new ArrayList<>();

    // 연관관계 주인 필드를 선택
    @ManyToMany(mappedBy = "items")
    private List<Category> categories = new ArrayList<>();
}

Category

@Entity
@Getter @Setter
public class Category {

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

    /**
     * 다대다 설정
     */
    @ManyToMany
    @JoinTable(name = "category_item",
        // Category에서 join해야 할 대상은 category_id
        joinColumns = @JoinColumn(name = "category_id"),
        // Item에서 join해야 할 대상은 item_id
        inverseJoinColumns = @JoinColumn(name = "item_id")
    )
    private List<Item> items = new ArrayList<>();
    /**
     * 상위 카테고리를 설정
     * 하나의 부모 카테고리는 여러개의 자식 카테고리가 있음
     */
    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Category parent;

    /**
     * 양방향으로 설정
     * 연관관계 주인 필드를 선택
     */
    @OneToMany(mappedBy = "parent")
    private List<Category> child = new ArrayList<>();
}

 

테이블 생성 결과

@ManyToMany로 인해서 category_item가 생성된것을 확인 할 수 있습니다.
✅ 실무에서는 다대다(N:M)는 반드시 일대다(1:N) 다대일(N:1)로 풀어서 사용하시는것을 권장 합니다.

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 varchar(255),
        street varchar(255),
        zipcode varchar(255),
        primary key (id)
    )
Hibernate: 

    create table Item (
       id bigint not null,
        name varchar(255),
        price integer,
        stockQuantity integer,
        primary key (id)
    )
Hibernate: 

    create table Member (
       id bigint not null,
        city varchar(255),
        name varchar(255),
        street varchar(255),
        zipcode varchar(255),
        primary key (id)
    )
Hibernate: 

    create table OrderItem (
       id bigint not null,
        item_id bigint,
        order_id bigint,
        primary key (id)
    )
Hibernate: 

    create table orders (
       id bigint not null,
        orderDate timestamp,
        status varchar(255),
        delivery_id bigint,
        member_id bigint,
        primary key (id)
    )

 

 

 

 

728x90
반응형