[JPA] 연관관계 매핑(@ManyToOne, @OneToMany, @OneToOne, @ManyToMany)
자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의
이 글은 인프런에서 제공하는 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 참고했고
강의 내용을 다시 복습하면서 정리하려는 목적으로 작성합니다.
앞에서 우리는 연관관계 매핑에 관련해서 찍먹해봤습니다.
이번 포스팅에서는 연관관계에 대해서 본격적으로 알아봅시다!
매핑에 사용되는 주요 애노테이션
@JoinColumn
외래 키를 매핑할 때 사용합니다.
@ManyToOne
다대일 관계 매핑시 사용합니다.
@OneToMany
일대다 관계 매핑시 사용합니다.@ManyToOne
는 mappedBy
가 없는것을 확인할수 있습니다.
이말은 @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_id
가 1
이기 때문에 SK T1
소속이고,
매드라이프는 team_id
가 2
이기 때문에 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;
}
이렇게 한 후에 반드시 Member
에 Team
의 연관관계를 주입해줘야 합니다.
(member.setTeam(team);
같은 로직을 넣으셔야 합니다.)
양방향
외래 키가 있는 쪽이 연관관계의 주인 입니다.
양쪽을 서로 참조하도록 개발(단방향이 2개)하면 됩니다.
Member
에서 이미 Team
을 참조하고 있으니, Team
의 Entity
만 수정하면 됩니다.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<>();
}
Member
의 Entity
에서 반드시 연관관계를 주입해줘야 합니다.Member
에서 Team
의 연관관계를 주입하고, Team
에서 Member
의 연관관계를 주입해야 합니다.
(객체 지향적으로 생각해보세요.! Team
에 Member
를 주입해줘야 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;
}
대상 테이블에 외래 키 단방향
이렇게는 불가합니다.
하지만 양방향 관계는 지원 합니다.
양방향 이기 때문에 Locker
의 Member
를 연관관계의 주인으로 잡아서 매핑하면 됩니다.
주 테이블에 외래 키
- 주 객체가 대상 객체의 참조를 가지는 것 처럼
- 주 테이블에 외래 키를 두고 대상 테이블을 탐색
- 객체지향 개발자가 선호
JPA
매핑 편리- 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능
- 값이 없으면 외래 키에
null
- 값이 없으면 외래 키에
대상 테이블에 외래 키
- 대상 테이블에 외래 키가 존재
- 데이터베이스 개발자 선호
- 주 테이블과 대상 테이블을 일대일(1:1)에서 일대다(1:N) 관계로 변경할 때 테이블 구조 유지 가능
- 대상 테이블의 유니크 제약 조건을 해제하면 됨(하나의 회원에 여러개의 라커)
- 프록시 기능의 한계로 즉시로딩 됨(지연로딩이 불가)
- 주 테이블과 대상 테이블을 일대일(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
Member
와 Product
사이에 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)
)
'0+ 스프링 > 0 + 스프링 ORM(JPA)' 카테고리의 다른 글
[JPA] @MappedSuperclass(공통 매핑 정보 해결 + 스프링 적용) (0) | 2023.02.20 |
---|---|
[JPA] 상속관계 매핑 (0) | 2023.02.20 |
[JPA] 연관관계 매핑 개념(패러다임 불일치 해결) (0) | 2023.02.18 |
[JPA] 엔티티 매핑(@Entity, @Table) (0) | 2023.02.16 |
[JPA] JPA 영속성 컨텍스트(+ flush, 준영속 상태) (0) | 2023.02.09 |