member 의 team 을 찾는 상황을 가정해보자.
Member findMember = em.find(Member.class, member.getId());
Long findTeamId = findMember.getId();
Team findTeam = em.find(Team.class, findTeamId);
이런 긴 과정은 전혀 객체 지향스럽지 않은 코드이며, 비용 多
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);
따라서, 데이터베이스는 단방향, 양방향을 구분할 필요가 없다.
객체는 참조용 필드가 있는 객체만 다른 객체를 참조하는 것이 가능
단방향 관계 : 두 객체 中 '하나의 객체만' 참조용 필드를 가지고 참조
양방향 관계 : '두 객체 모두' 각각 참조용 필드를 가지고 서로를 참조
Board → Post 단방향 참조
Post → Board 단방향 참조
굳이 참조를 하지 않아도 된다
(일반적으로) User 엔티티의 경우, 많은 엔티티와 연관 관계를 갖는다.
따라서, 모든 엔티티를 양방향 관계로 설정하게 되면
User 엔티티는 엄청나게 많은 테이블과 연관 관계를 맺게 되어 오히려 복잡해질 수 있다.
기본적으로는 단방향 매핑을 하자.
만약, 역방향으로 객체 탐색이 필요할 때 추가하는 방향으로 매핑을 진행하면 된다.
Board(게시판) 객체, Post(게시글) 객체 가 양방향 연관관계를 갖는 상황을 가정해보자.
게시판에 있는 어느 게시글을 수정할 때, JPA 에게 혼란을 주게 된다.
Post 객체에서 setBoard() 메소드를 이용해서 수정하는게 맞는지?
Board 객체에서 getPosts() 메소드를 이용해서 List 의 게시글을 수정하는게 맞는지?
엔티티를 단방향으로 매핑할 경우
참조를 하나만 사용하므로, 이 참조로 외래키를 관리하면 된다.
엔티티를 양방향으로 매핑할 경우
서로가 서로를 참조하므로, 연관 관계를 관리하는 포인트가 2개로 늘어난다.
따라서, JPA 에서는 두 연관관계 中 어느 한 테이블을 정해서 외래 키를 관리하게 된다.
이렇게 외래 키를 관리하는 쪽이 '연관 관계의 주인' 이다.
mappedBy
'읽기 전용'으로 만든다는 것이다.
00 테이블 기준이라는 것은, 00 테이블이 외래 키를 갖고있다는 뜻
일대일은 거꾸로해도 동일한 일대일이기 때문에
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
대상 테이블 : Attach
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
대상 테이블 : Attach
기본적으로 : 연관관계의 주인 쪽이 다(N)이지만,
일대다의 경우만 : 연관관계의 주인 쪽이 일(1) 이다
실무에서는 거의 사용하지 않는 연관관계 방식이다.
엔티티가 관리하는 외래 키가 다른 테이블에 있다.
불필요한 쿼리문 발생
본인 테이블에 외래 키가 있으면, 엔티티의 저장 & 연관관계 처리를 INSERT SQL 로 한번에 끝낼 수 있다
다른 테이블(1쪽)에 외래 키가 있으면, 연관관계 관리를 위해 추가로 UPDATE SQL 을 실행해야한다
게시판 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;
}
일대다 양방향은 공식적으로 존재하지 않는다.
가장 권장되는 방식이다.
게시글 Post : 게시판 Board = N : 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;
}
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<>();
}
게시글과 댓글이 있다고 가정해보자.
게시글을 삭제하면, 해당 게시글에 달린 댓글도 함께 삭제하고 싶은 경우 영속성 전이
를 사용하면 된다.
(참고: 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;
}
@ManyToMany 를 이용한 다대다 연관관계는 실무에서는 거의 사용하지 않는 방식이다.
중간 테이블이 숨겨져 있어, 복잡한 조인의 쿼리가 발생할 수 있기 때문
관계형 데이터베이스는 정규화된 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] 관계