연관관계

박영준·2023년 1월 29일
0

DB

목록 보기
32/41

1. 연관 관계의 필요성

1) 연관 관계를 사용하지 않을 경우

member 의 team 을 찾는 상황을 가정해보자.

Member findMember = em.find(Member.class, member.getId());

Long findTeamId = findMember.getId();
Team findTeam = em.find(Team.class, findTeamId);
  1. member 를 꺼내오고
  2. member 에서 id 를 가져와서
  3. 다시 team 을 가져온다

이런 긴 과정은 전혀 객체 지향스럽지 않은 코드이며, 비용 多

2) 연관 관계를 사용할 경우

Member : Team = N : 1 (다대일 관계) 상황을 가정해보자.

Member Entity

@Entity
public class Member {

  @Id
  @GeneratedValue
  private Long id;

  @Column
  private String name;

  @Column
  private Long teamId;
  
  ...
  
}

Team Entity

@Entity
public class Team {

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

Member 과 Team 저장

//팀 저장
Team team = new Team();
team.setName("wooteco");
em.persist(team);

//회원 저장
Member member = new Member();
member.setName("conas");			
member.setTeamId(team.getId());		// Team 객체를 참조
em.persist(member);
  • 테이블은 외래키로 조인을 사용해서 연관된 테이블을 찾는다.

2. 단방향 vs 양방향

1) DB vs 객체

DB 의 경우

  • 외래 키 하나로, 양 쪽 테이블을 JOIN 가능
  • 외래 키 하나로, 양 쪽 테이블 모두 조회 가능

따라서, 데이터베이스는 단방향, 양방향을 구분할 필요가 없다.

객체의 경우

객체는 참조용 필드가 있는 객체만 다른 객체를 참조하는 것이 가능

  • 단방향 관계 : 두 객체 中 '하나의 객체만' 참조용 필드를 가지고 참조

  • 양방향 관계 : '두 객체 모두' 각각 참조용 필드를 가지고 서로를 참조

    • 단, 실제로는 두 객체가 각자 다른 객체를 단방향 참조하는 것이다

2) 필요성

(1) Board.getPost() 처럼 참조가 필요할 경우

Board → Post 단방향 참조

(2) post.getBoard() 처럼 참조가 필요할 경우

Post → Board 단방향 참조

(3) 참조가 굳이 필요 없다면

굳이 참조를 하지 않아도 된다

3) 어떤 것을 사용할까

(1) 주의점

(일반적으로) User 엔티티의 경우, 많은 엔티티와 연관 관계를 갖는다.

따라서, 모든 엔티티를 양방향 관계로 설정하게 되면
User 엔티티는 엄청나게 많은 테이블과 연관 관계를 맺게 되어 오히려 복잡해질 수 있다.

(2) 해결책

기본적으로는 단방향 매핑을 하자.

만약, 역방향으로 객체 탐색이 필요할 때 추가하는 방향으로 매핑을 진행하면 된다.

3. 연관관계 주인

1) 양방향 관계에서의 필요성

Board(게시판) 객체, Post(게시글) 객체 가 양방향 연관관계를 갖는 상황을 가정해보자.

게시판에 있는 어느 게시글을 수정할 때, JPA 에게 혼란을 주게 된다.

  • Post 객체에서 setBoard() 메소드를 이용해서 수정하는게 맞는지?

  • Board 객체에서 getPosts() 메소드를 이용해서 List 의 게시글을 수정하는게 맞는지?

2) 사용법

단방향 매핑

엔티티를 단방향으로 매핑할 경우
참조를 하나만 사용하므로, 이 참조로 외래키를 관리하면 된다.

양방향 매핑

엔티티를 양방향으로 매핑할 경우
서로가 서로를 참조하므로, 연관 관계를 관리하는 포인트가 2개로 늘어난다.

  • 객체의 참조는 2개
  • 외래키는 1개

따라서, JPA 에서는 두 연관관계 中 어느 한 테이블을 정해서 외래 키를 관리하게 된다.
이렇게 외래 키를 관리하는 쪽이 '연관 관계의 주인' 이다.

  • 연관관계의 주인 O : mappedBy 사용 X
  • 연관관계의 주인 X : mappedBy 사용 O → 이를 통해 주인을 지정해준다

mappedBy
'읽기 전용'으로 만든다는 것이다.

00 테이블 기준이라는 것은, 00 테이블이 외래 키를 갖고있다는 뜻

4. 연관관계

1) 일대일 (1:1)

일대일은 거꾸로해도 동일한 일대일이기 때문에

  • 주 테이블에 외래 키를 넣을 수도 있고,
  • 대상 테이블에 외래 키를 넣을 수도 있다

(1) 일대일 단방향

Post

@Entity
public class Post {
	@Id
    @GeneratedValue
    @Column(name = "POST_ID")
    private Long id;
    
    @Column(name = "TITLE")
    private String title;
    
    @OneToOne							// @OneToOne
    @JoinColumn(name = "ATTACH_ID")		// @JoinColumn
    private Attach attach;
}

Attach

@Entity
public class Attach {
	@Id
    @GeneratedValue
    @Column(name = "ATTACH_ID")
    private Long id;
    
    private String name;
}
  • 주 테이블 : Post

    • FK로 Attach 테이블의 PK를 가진다
  • 대상 테이블 : Attach

(2) 일대일 양방향

Post

@Entity
public class Post {
	@Id
    @GeneratedValue
    @Column(name = "POST_ID")
    private Long id;
    
    @Column(name = "TITLE")
    private String title;
    
    @OneToOne							// @OneToOne
    @JoinColumn(name = "ATTACH_ID")		// @JoinColumn
    private Attach attach;
}

Attach

@Entity
public class Attach {
	@Id
    @GeneratedValue
    @Column(name = "ATTACH_ID")
    private Long id;
    
    private String name;
    
    @OneToOne(mappedBy = "attach")		// @OneToOne(mappedBy = "attach")
    private Post post;
}

  • 주 테이블 : Post

    • FK로 Attach 테이블의 PK를 가진다
  • 대상 테이블 : Attach

2) 일대다 (1:N)

주의점

주의 1

기본적으로 : 연관관계의 주인 쪽이 다(N)이지만,
일대다의 경우만 : 연관관계의 주인 쪽이 일(1) 이다

주의 2

실무에서는 거의 사용하지 않는 연관관계 방식이다.

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

    • 일대다 연관관계의 경우, 1 쪽이 주인이 된다.(즉, 1쪽이 외래 키를 가지게 된다)
    • 그래서, 작업한 Entity 가 아닌 다른 Entity 에서 쿼리문이 나가는 경우가 있다
  2. 불필요한 쿼리문 발생

    • 본인 테이블에 외래 키가 있으면, 엔티티의 저장 & 연관관계 처리를 INSERT SQL 로 한번에 끝낼 수 있다

    • 다른 테이블(1쪽)에 외래 키가 있으면, 연관관계 관리를 위해 추가로 UPDATE SQL 을 실행해야한다

      • 1쪽의 테이블에 외래 키를 저장할 방법이 없기 때문에, 직접 추가적으로 쿼리를 날려줘야만 한다

(1) 일대다 단방향

게시판 Board : 게시글 Post = 1 : N

  • 게시판에서 게시글 객체를 조작(생성, 수정, 삭제) 할 수 있다.

Board

// 게시판
@Entity
public class Board {
	@Id
    @GeneratedValue
    private Long id;
    private String title;
    
    @OneToMany						// @OneToMany
    @JoinColumn(name = "POST_ID")	// @JoinColumn
    List<Post> posts = new ArrayList<>();
}

Post

// 게시글
@Entity
public class Post {
	@Id
    @GeneratedValue
    @Column(name = "POST_ID")
    private Long id;
    
    @Column(name = "TITLE")
    private String title;    
}

(2) 일대다 양방향

일대다 양방향은 공식적으로 존재하지 않는다.

3) 다대일 (N:1)

가장 권장되는 방식이다.

게시글 Post : 게시판 Board = N : 1

  • 1개의 게시판에 N개의 게시글을 작성할 수 있다
  • 1개의 게시글은 1개의 게시판에만 작성할 수 있다

게시글이 연관관계의 주인이다.

(1) 다대일 단방향

Post

// 게시글
@Entity
public class Post {
	@Id
    @GeneratedValue
    @Column(name = "POST_ID")
    private Long id;
    
    @Column(name = "TITLE")
    private String title;
    
    @ManyToOne						// @ManyToOne
    @JoinColumn(name = "BOARD_ID")	// @JoinColumn
    private Board board;
}

Board

// 게시판
@Entity
public class Board {
	@Id
    @GeneratedValue
    private Long id;
    
    private String title;
}

(2) 다대일 양방향

예시 1

Post

// 게시글
@Entity
public class Post {
	@Id
    @GeneratedValue
    @Column(name = "POST_ID")
    private Long id;
    
    @Column(name = "TITLE")
    private String title;
    
    @ManyToOne						// @ManyToOne
    @JoinColumn(name = "BOARD_ID")	// @JoinColumn
    private Board board;
}

Board

// 게시판
@Entity
public class Board {
	@Id
    @GeneratedValue
    private Long id;
    private String title;
    
    @OneToMany(mappedBy = "board")			// @OneToMany(mappedBy = "board")
    List<Post> posts = new ArrayList<>();
}

예시 2 : 영속성 전이

게시글과 댓글이 있다고 가정해보자.
게시글을 삭제하면, 해당 게시글에 달린 댓글도 함께 삭제하고 싶은 경우 영속성 전이를 사용하면 된다.
(참고: JPA (Java Persistence API) - 4) 영속성 전이 (Cascade))

Board

public class Board extends Timestamped {        // board : user = N : 1 다대일 단방향
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "title", nullable = false)
    private String title;

    @Column(name = "username", nullable = false)
    private String username;

    @Column(name = "contents", nullable = false, length = 500)
    private String contents;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    private User user;

    @OneToMany(mappedBy = "board", cascade = CascadeType.REMOVE)		// 영속성 전이
    private List<Comment> commentList = new ArrayList<>();

Comment

public class Comment {      // comment : board = N : 1 다대일 단방향
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "username", nullable = false)
    private String username;

    @Column(name = "comment", nullable = false)
    private String comment;

    @ManyToOne
    @JoinColumn(name = "board_id", nullable = false)
    private Board board;
}
  • 영속성 전이를 사용하기 위해서는 @OneToMany 가 필요
    • 즉, 다대일 양방향 연관관계가 됨

4) 다대다 (N:M)

주의점

주의 1

@ManyToMany 를 이용한 다대다 연관관계는 실무에서는 거의 사용하지 않는 방식이다.
중간 테이블이 숨겨져 있어, 복잡한 조인의 쿼리가 발생할 수 있기 때문

주의 2

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

해결법
중간 테이블(연결 테이블) 을 연관 관계의 주인으로 두고 푼다.

단방향 & 양방향

Book, Member 엔티티가 있고, 이 두 테이블을 연결해주기 위한 Purchase 엔티티가 있다고 가정해보자.

Purchase 가 연관관계의 주인으로써 Book 엔티티와 Member 엔티티의 FK 를 모두 가지고 있다.
Purchase 를 중심으로 다대일 양방향 연관관계를 맺고 있다.

Book

@Entity
@Getter
@NoArgsConstructor
@Table(name = "BOOK")
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "book_id")
    private Long id;

    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private String author;

    @Column(nullable = false)
    private Integer price;

    private Integer stock;

    @OneToMany(mappedBy = "book")				// Purchase : Book = N : 1 다대일 양방향
    private List<Purchase> purchases = new ArrayList<>();

Purchase

@Entity
@Getter
@NoArgsConstructor
@Table(name = "PURCHASE")
public class Purchase {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "purchase_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)			// Purchase : Member = N : 1 다대일 양방향
    @JoinColumn(name = "member_id")
    private Member member;

    @ManyToOne(fetch = FetchType.LAZY)			// Purchase : Book = N : 1 다대일 양방향
    @JoinColumn(name = "book_id")
    private Book book;
}

Member

@Entity
@Getter
@NoArgsConstructor
@Table(name = "MEMBER")
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String email;

    @Column(nullable = false)
    private int password;

    @Column(nullable = false)
    private String address;

    @Column(nullable = false)
    private int phoneNumber;

    @Column(nullable = false)
    private String nickname;

    @ManyToOne
    private BookStore bookStore;

    @OneToMany(mappedBy = "member")				// Purchase : Member = N : 1 다대일 양방향
    private List<Purchase> purchases = new ArrayList<>();

    @ManyToMany
    private List<Book> books = new ArrayList<>();
}

참고: @ManyToOne 이용 회원, 게시글, 댓글 연관관계 설정
참고: JPA - One To Many 단방향의 문제점
참고: JPA OneToMany 단방향 맵핑의 단점 이해하기 삽질기
참고: @OneToMany 단방향을 @ManyToOne 양방향으로
참고: JPA 연관 관계 한방에 정리 (단방향/양방향, 연관 관계의 주인, 일대일, 다대일, 일대다, 다대다)
참고: [JPA] 연관관계 매핑 기초 #1 (연관관계의 필요성, 단방향 연관관계)
참고: [Database] 식별관계와 비 식별관계
참고: JPA 연관 관계 정리
참고: JPA - 다양한 연관관계 매핑(4) : 다대다 [N : M]
참고: querydsl에서 다대다 구현하기. 일대다-다대일(feat. Result
참고: [JPA] @ManyToMany, 다대다[N:M] 관계

profile
개발자로 거듭나기!

0개의 댓글