JPA기초 OneToOne Bidirection
OneToOne Bidirection 매핑
OneToOne Unidirection 매핑을 지금까지 해보았다.
- UniDirection
- User가 UserDetail 를 포함하고 있는구조.
- User 에서 UserDetail 을 참조할 수 있다.
- UserDetail 에서 User를 접근할 수 없음
- DB 조회시 User 를 조회하고, 필요시 UserDetail 을 Lazy Loading 해서 조회할 수 있다.
- Bidirection
- User가 UserDetail 를 포함하는 구조.
- UserDetail 이 User 을 포함하는 구조.
- User 에서 UserDetail 을 참조할 수 있다.
- UserDetail에서 User 을 참조할 수 있다.
- DB 조회시 User 를 조회하고, 필요시 UserDetail 을 Lazy Loading 해서 조회할 수 있다.
- DB 조회시 UserDetail 을 조회하고, 필요시 User을 Lazy Loading 해서 조회할 수 있다.
위와 같이 Bidirection 연관을 맺어주면 어느쪽으로 조회하든 편리하게 필요한 정보를 함께 가져올 수 있다.
이제 Bidirection 연관을 맺어보자.
User 연관 설정
User.java 를 다음과 같이 작성하자.
package com.schooldevops.practical.simpleboard.entity;
import com.schooldevops.practical.simpleboard.dto.UserDto;
import com.schooldevops.practical.simpleboard.entity.converter.LocalDateTimeConverter;
import lombok.*;
import javax.persistence.*;
import java.time.LocalDateTime;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "user")
public class User {
@Id
@Column(name = "id", unique = true)
private String id;
@Column
private String name;
@Column
private String birth;
@Column
@Convert(converter = LocalDateTimeConverter.class)
private LocalDateTime createdAt;
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private UserDetail userDetail;
public void setUserDetail(UserDetail userDetail) {
this.userDetail = userDetail;
userDetail.setUser(this);
}
@Transient
public UserDto getDTO() {
UserDto userDTO = UserDto.builder()
.id(this.id)
.name(this.name)
.birth(this.birth)
.createdAt(this.createdAt)
.build();
if (userDetail != null) {
userDTO.setUserDetail(userDetail.getDTO());
}
return userDTO;
}
}
위 내용중 핵심은 @OneToOne 어노테이션 부분이다.
User 객체에 UserDetail 을 포함하도록 하였고.
- @OneToOne: 명시적으로 User와 UserDetail 은 1:1임을 선언했다.
- cascade=CascadeType.ALL: User의 오퍼레이션이 UserDetail 에 전파되는 것을 전부다로 설정했다.
- orphanRemoval=true: 부모인 User가 삭제되면 자식인 UserDetail 역시 함께 삭제된다.
그리고 다음으로 편의 메소드를 작성했다.
public void setUserDetail(UserDetail userDetail) {
this.userDetail = userDetail;
userDetail.setUser(this);
}
이것은 userDetail 을 User에 설정할때 자동으로 객체간 관계를 맺어주도록 해준다.
개발할때 편리하다.
UserDetail 관계 매핑
이제 UserDetail 에 대한 매핑을 다음과 같이 만들어준다.
package com.schooldevops.practical.simpleboard.entity;
import com.schooldevops.practical.simpleboard.constants.Role;
import com.schooldevops.practical.simpleboard.dto.UserDetailDto;
import com.schooldevops.practical.simpleboard.entity.converter.LocalDateTimeConverter;
import lombok.*;
import javax.persistence.*;
import java.time.LocalDateTime;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
@Table(name = "UserDetail")
public class UserDetail {
@Id
@Column(name = "id", unique = true)
private String id;
@Column
private String nick;
@Column
private String avatarImg;
@Column
private String category;
@Enumerated(EnumType.STRING)
@Column
private Role role;
@Column
@Convert(converter = LocalDateTimeConverter.class)
private LocalDateTime joinedAt;
@Column
@Convert(converter = LocalDateTimeConverter.class)
private LocalDateTime modifiedAt;
@OneToOne
@MapsId
private User user;
@Transient
public UserDetailDto getDTO() {
return UserDetailDto.builder()
.id(this.id)
.nick(this.nick)
.avatarImg(this.avatarImg)
.category(this.category)
.role(this.role)
.joinedAt(this.joinedAt)
.modifiedAt(this.modifiedAt)
.build();
}
}
내용은 길지만 중요한 부분은 @OneToOne 과 @MapsId 부분이다.
- @OneToOne: UserDetail 과 User 관계는 1:1임을 선언한다.
- @MapId: 이것은 UserDetail 이 User와 연관을 맺을때 User의 기본키와 매핑됨을 의미한다. 1:1 관계는 일반적으로 기본키를 동일하게 저장한다. 이를 기본키 공유라고도 부른다.
생성된 테이블 살펴보기.
Hibernate:
create table user (
id varchar(255) not null,
birth varchar(255),
created_at datetime(6),
name varchar(255),
user_detail_user_id varchar(255),
primary key (id)
) engine=InnoDB
user 테이블이 생성되었다.
Hibernate:
create table user_detail (
avatar_img varchar(255),
category varchar(255),
joined_at datetime(6),
modified_at datetime(6),
nick varchar(255),
role varchar(255),
user_id varchar(255) not null,
primary key (user_id)
) engine=InnoDB
이제 userDetail 테이블이 생성이 되었다.
우선 이상한 점은 user_id 라고 해서 기본키가 설정이 되었다. user 테이블은 id 이고, user_detail 테이블은 user_id 가 기본키로 생성되었다.
Hibernate:
alter table user
add constraint FK64xvo1yt6454tgj3ysjtuym4e
foreign key (user_detail_user_id)
references user_detail (user_id)
이제는 Foreign key 생성부분이다.
user 테이블에 foreign key 가 생성이 되었고, user_detail 의 user_id 를 참조하도록 생성이 되었다.
Hibernate:
alter table user_detail
add constraint FKc2fr118twu8aratnm1qop1mn9
foreign key (user_id)
references user (id)
마지막으로 user_detail 테이블에 foreign key 가 생성되었고, 참조는 user 테이블에 id를 참조하도록 생성이 되었따.
비고하기.
우리가 원하는 그림이 이것이 맞는 것인지 생각해보자.
사실 1:1 매핑이지만 우리가 원하는 그림은 위와같이 테이블과 foreign key 가 생성되는 것이 아니다.
우리에게 필요한 것은 다음과 같다.
- user 테이블
- user_detail 테이블
- user_detail 테이블의 기본키는 id 로 생성
- user_detail 테이블에 기본키를 생성하고, 이는 user테이블의 id 를 참조하도록 생성
매핑 변경하기.
mappedBy 이용하기.
이제 mappedBy 를 이용할 차례이다. 좀전에 테이블 생성시에 양쪽에 foreign key 가 생성되었음을 알 수 있다.
테이블 릴레이션에는 주, 종 관계가 존재한다. 우리가 생성하고자 하는 테이블은 다음과 같은 주 종 관계가 있다.
- 주: user테이블, user 데이터가 존재해야 user_detail 데이터도 존재가 가능하다.
- 종: user_detail테이블, user가 없다면 user_detail 도 없으므로 user_detail 테이블에 foreign_key 로 user 테이블의 id 를 참조하도록 관계가 설정이 된다.
위와 같이 DBMS 입장에서는 종에 해당하는 테이블이 주의 primary key 를 참조키로 저장하게 된다.
예)
- 학생 - 수 이 있다고 생각하자. 학생 1명이 여러 수강을 하게 된다.
- 여기서 주는 학생이다. 종은 수이다. 수강은 학생 id 를 가지게 된다. 즉 연관관계를 소유한 쪽은 수강쪽이다.
위 예와 같이 이러한 관계가 무엇이고, 이 관계의 정보를 가진 쪽이 어디인지를 알려주는 것이 mappedBy 속성이다.
참고: 일반적으로 구글링 해보면 연관관계 주인이라는 말이 나오는데, 틀린말은 아니지만 매우 헛갈리는 용어라고 생각한다. 좀더 이해가 쉽게 하기 위해서는 연관관계 소유자 혹은 Foreign Key 를 소유자 라고 하는 것이 어떨지 싶다.
mappedBy 는 User 측에 설정한다. 이 의미는 User 은 주에 해당하는 엔터티 이고, 이 주와 관계를 저장하고 있는 측은 UserDetail 엔터티임을 JPA 에게 알려준다.
User 매핑 변경하기.
그럼 User.java 내용을 다음과 같이 수정해주자.
@OneToOne(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private UserDetail userDetail;
추가된 것은 mappedBy=”user” 이다.
이 의미는 UserDetail 에 user 라는 속성에 연관관계가 매핑되어 있다는 의미로 해석하면 될것같다.
생성 결과 확인하기.
Hibernate:
create table user (
id varchar(255) not null,
birth varchar(255),
created_at datetime(6),
name varchar(255),
primary key (id)
) engine=InnoDB
Hibernate:
create table user_detail (
avatar_img varchar(255),
category varchar(255),
joined_at datetime(6),
modified_at datetime(6),
nick varchar(255),
role varchar(255),
user_id varchar(255) not null,
primary key (user_id)
) engine=InnoDB
Hibernate:
alter table user_detail
add constraint FKc2fr118twu8aratnm1qop1mn9
foreign key (user_id)
references user (id)
위와 같이 테이블과 user_detail에 Foreign Key 가 생성이 되었다.
우리가 원했던 그림을 다시한번 보자.
- user 테이블 (OK)
- user_detail 테이블 (OK)
- user_detail 테이블의 기본키는 id 로 생성 (X)
- user_detail 테이블에 기본키를 생성하고, 이는 user테이블의 id 를 참조하도록 생성 (OK)
user_detail 테이블의 기본키를 id 로 생성해보자.
UserDetail.java 내용을 다음과 같이 수정하자.
@JoinColumn(name = "id", foreignKey = @ForeignKey(name = "FK_UserDetail_to_user"))
@OneToOne
@MapsId
private User user;
추가한 부분은 @JoinColumn 이다.
- @JoinColumn: foreign key 칼럼의 이름을 지정해준다.
- foreignKey = … : foreign key 는 위 결과처럼 FKc2fr118twu8aratnm1qop1mn9 이름이 이렇게 생성이 된다. 어것도 나쁘지 않지만 명시적으로 지정해 줄 수 있다. 우리 예제에서는 FK_UserDetail_to_user 로 잡아보자.
결과 확인하기
Hibernate:
create table user (
id varchar(255) not null,
birth varchar(255),
created_at datetime(6),
name varchar(255),
primary key (id)
) engine=InnoDB
Hibernate:
create table user_detail (
id varchar(255) not null,
avatar_img varchar(255),
category varchar(255),
joined_at datetime(6),
modified_at datetime(6),
nick varchar(255),
role varchar(255),
primary key (id)
) engine=InnoDB
Hibernate:
alter table user_detail
add constraint FK_UserDetail_to_user
foreign key (id)
references user (id)
우리가 원하는 대로 생성이 되었다.
mappedBy의 사용법을 확인했고, 그리고 JoinColumn 으로 테이블을 생성해 보았다.
JPA 의 매핑역시 우리의 상식에 벗어나지 않는다. 일반적인 테이블 생성 원리를 그대로 JPA 매핑도 가져가고 있음을 알 수 있다.