2022. 7. 3. 14:37ㆍLanguage`/JPA
일대다 관계는 다대일 관계 반대 방향이다
일대다 관계는 엔티티를 하나 이상 참조할 수 있으므로 자바 컬렉션(Collection, List, Set, Map)중에 하나를 사용해서 관리해야 한다
현재 포스팅에서의 도메인간 관계는 다음과 같다
- 하나의 User는 여러개의 Locker를 가질 수 있다
- 하나의 Locker는 하나의 User에 의해서만 소유될 수 있다
1. 일대다 단방향
일대다 단방향은 특이하게 일(1)쪽에서 상대(N) 엔티티의 외래키를 관리하는 구조로 이루어져 있다
보통 자신이 매핑한 테이블의 FK를 관리하는데 이 경우는 반대쪽 테이블에 있는 FK를 관리하는 특이항 구조이다
왜냐하면 FK는 반드시 "다"쪽에 있어야 하는데 "다"쪽인 Locker를 보면 현재 FK를 매핑할 수 있는 참조 필드가 존재하지 않는다
User (1 - 주인)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "user_id")
private List<Locker> lockerList = new ArrayList<>();
}
List<Locker> lockerList는 실제 DB상에서 Locker Table의 "user_id"를 참조해야 매핑이 되기 때문에 @JoinColumn(name = "user_id")로 매핑시켜야 한다
Locker (N - 주인 X)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "locker")
public class Locker {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "locker_id")
private Long id;
private String lockerName;
}
일대다 단방향 관계를 매핑할 때는 반드시 @JoinColumn을 명시해줘야 한다
만약 이 @JoinColumn을 매핑하지 않는다면 중간에서 연관관계를 관리하는 "JoinTable" 전략을 default로 설계되었기 때문에 JoinTable이 생성된다
※ @JoinColumn 매핑 O
@OneToMany
@JoinColumn(name = "user_id")
private List<Locker> lockerList = new ArrayList<>();
Hibernate:
create table locker (
locker_id bigint not null auto_increment,
lockerName varchar(255),
user_id bigint,
primary key (locker_id)
) engine=MyISAM
Hibernate:
create table user (
user_id bigint not null auto_increment,
name varchar(255),
primary key (user_id)
) engine=MyISAM
# FK 제약조건 설정
Hibernate:
alter table locker
add constraint FKbh0f46ekjtm5ac0cg0vhn6d4i
foreign key (user_id)
references user (user_id)
※ @JoinColumn 매핑 X
@OneToMany
private List<Locker> lockerList = new ArrayList<>();
Hibernate:
create table locker (
locker_id bigint not null auto_increment,
lockerName varchar(255),
primary key (locker_id)
) engine=MyISAM
Hibernate:
create table user (
user_id bigint not null auto_increment,
name varchar(255),
primary key (user_id)
) engine=MyISAM
# JoinTable
Hibernate:
create table user_locker (
User_user_id bigint not null,
lockerList_locker_id bigint not null
) engine=MyISAM
# FK 제약조건 설정
Hibernate:
alter table user_locker
add constraint UK_svt7ncsflgvowhrsqky5jomq unique (lockerList_locker_id)
Hibernate:
alter table user_locker
add constraint FKd2c1j4mi8joaj1vmx0gvxc2a1
foreign key (lockerList_locker_id)
references locker (locker_id)
Hibernate:
alter table user_locker
add constraint FK21njhumvfu4grnarvr0mvvjfk
foreign key (User_user_id)
references user (user_id)
일대다 단방향 매핑의 단점
일대다 단방향 매핑은 엔티티가 관리하는 FK가 다른 테이블에 존재하기 때문에 "연관관계 관리"를 위해서 추가적으로 UPDATE SQL이 실행된다
Locker lockerA = new Locker("lockerA");
Locker lockerB = new Locker("lockerB");
em.persist(lockerA);
em.persist(lockerB);
User user = new User("userA");
user.getLockerList().add(lockerA);
user.getLockerList().add(lockerB);
em.persist(user);
-- 각 엔티티 persist(insert)
Hibernate:
/* insert OneToMany.domain.Locker
*/ insert
into
locker
(lockerName)
values
(?)
Hibernate:
/* insert OneToMany.domain.Locker
*/ insert
into
locker
(lockerName)
values
(?)
Hibernate:
/* insert OneToMany.domain.User
*/ insert
into
user
(name)
values
(?)
-- update query
Hibernate:
/* create one-to-many row OneToMany.domain.User.lockerList */ update
locker
set
user_id=?
where
locker_id=?
Hibernate:
/* create one-to-many row OneToMany.domain.User.lockerList */ update
locker
set
user_id=?
where
locker_id=?
따라서 일대일 단방향 매핑보다는 "다대일 양방향 매핑"을 사용하는 것을 더 권장한다
- 다대일 양방향이라면 insert query하나로 해결할 수 있기 때문이다
2. 일대다 양방향
"일대다 양방향"이라는 매핑은 사실 존재하지 않다
일대다 양방향은 그냥 "읽기 전용 필드"를 사용해서 양방향처럼 사용하는 것이다
User (1 - 주인)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "user_id")
private List<Locker> lockerList = new ArrayList<>();
}
Locker(N - 주인 X)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "locker")
public class Locker {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "locker_id")
private Long id;
private String lockerName;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", insertable = false, updatable = false)
private User user;
}
사실 이렇게 매핑을 설계하는 것보다는 그냥 "다대일 양방향"을 사용하자
※ 다대일 양방향 버전
User (1)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
private String name;
@OneToMany(mappedBy = "user")
private List<Locker> lockerList = new ArrayList<>();
}
Locker(N - 주인)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "locker")
public class Locker {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "locker_id")
private Long id;
private String lockerName;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
public void registerUser(User user){
if(this.user != null){
this.user.getLockerList().remove(this);
}
this.user = user;
user.getLockerList().add(this);
}
}
User user = new User("userA");
em.persist(user);
Locker lockerA = new Locker("lockerA");
Locker lockerB = new Locker("lockerB");
lockerA.registerUser(user);
lockerB.registerUser(user);
em.persist(lockerA);
em.persist(lockerB);
이 실행 결과로 도출된 query를 한번 유심히 살펴보자 (persist 제외)
Hibernate:
/* insert OneToMany.domain.User
*/ insert
into
user
(name)
values
(?)
Hibernate:
/* insert OneToMany.domain.Locker
*/ insert
into
locker
(lockerName, user_id)
values
(?, ?)
Hibernate:
/* insert OneToMany.domain.Locker
*/ insert
into
locker
(lockerName, user_id)
values
(?, ?)
이전에 일대다 양방향 매핑시 insert query + "update query"가 나감으로써 연관관계 관리를 해주었다.
그러나 "다대일 양방향 매핑"에서는 추가적인 update query없이 insert query만으로 연관관계 관리를 편리하게 할 수 있다