이번에는 1:N, N:1 관계를 봅니다. 이전에 이어서 생각해보자면, 이전의 어노테이션이 @OneToOne 이었으니까 이번엔 @OneToMany, @ManyToOne 이겠죠.
다만, 이번에는 나중에 배운다고 넘어간 좀 석연찮은 부분들이 있습니다. 특히 트랜젝션 관련 부분인데 관련해서 설명을 미루다보니 좀 이해가 안 가는 오류를 맞닥뜨릴 수 밖에 없었습니다. 영속성 얘기하면서 뭐 나오겠죠. 그래서 이번엔 답답해서 잠깐 쉬기도 하고, 결국 그냥 가벼운 마음으로 진행하기로 했습니다.
@OneToMany
이전에 만들었던 User와 UserHistory 의 관계를 @OneToMany 와 @ManyToOne 을 사용하는 것으로 바꿉니다.
먼저 @OneToMany 입니다. 앞부분의 "One"이 이 객체(User)이고, 뒷부분의 Many 가 UserHistory 입니다. 일단 붙여볼까요.
생략
public class User {
생략
// Eager fetching 은 실습에서 결과를 보기 위함입니다.
// 이걸 이해하려면 영속성 / Transaction 관련 설명이 필요해서
// 이번에는 이렇게 때운 것 같습니다.
// vvvvvv
@OneToMany(fetch = FetchType.EAGER)
private List<UserHistory> userHistories = new ArrayList<>();
생략
}
이렇게만 만들면, DDL에 매핑 테이블이 만들어집니다. User 와 UserHistory 를 있는 user_user_histories 가 만들어지네요.
create table user_user_histories (
user_id bigint not null,
user_histories_id bigint not null
)
하지만 UserHistory 에 user_id 로 foreign key 가 있으면 굳이 이런 테이블이 필요하지 않습니다. 그래서 @JoinColumn 어노테이션을 붙여줍니다.
@JoinColumn 은 이 필드가 join 할 대상 칼럼을 지정합니다. 이 경우에는 UserHistory 테이블에 칼럼을 하나 만들겠네요.
생략
public class User {
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "user_id", insertable = false, updatable = false)
private List<UserHistory> userHistories = new ArrayList<>();
생략
}
이 엔티티에서 수정하지 않을 것이므로 insertable과 updatable 을 false 로 줍시다. 강의에서는 약간 트러블슈팅식으로 세팅된 값이라...
초기화가 되어있는 이유는 빌더로 만들었을 때 등 JPA를 아직 거치지 않은 경우 null 일 수 있기 때문이라고 합니다. 오류를 방지하자는 차원이라네요. 직접 해보니 @Builder 가 있는 클래스의 경우 뭘 더 넣으라던데...
이 시점에서는 UserHistory를 고치지 않았으므로, 이전에 UserHistory 에 만들어둔 user_id 를 대상 칼럼으로 삼습니다.
다만 이렇게 하면 JPA 쪽에서 user_id 인지 userId 인지 모르겠다고 하므로 이 때 @Column 을 써서 정확한 칼럼명을 지정해주면 됩니다.
생략
class UserHistory 생략 {
생략
@Column(name = "user_id", insertable = false, updatable = false)
private Long userId;
생략
}
근데 차라리 지워버리는 게 편했을거에요. 이후에 @ManyToOne 으로 UserHistory 에서도 User 를 매핑하거든요. 지울거면 지금 지우고 실행해봐도 돌아갈 겁니다.
@ManyToOne
ManyToOne 은 OneToMany 입니다. 현재 객체가 Many (이 경우 UserHistory) 이고, 필드로 참조하는 객체가 One (이 경우 User) 입니다.
위에랑 똑같이 해주면 돼요.
생략
public class UserHistory 생략 {
생략
// private Long user_id; // << 삭제
@ManyToOne
@ToString.Exclude
@JoinColumn(name = "user_id")
private User user;
생략
}
이 경우에는 @ManyToOne 이랑 @OneToMany 를 상호참조하게 만들어놓은 거니, @ToString.Exclude는 꼭 잊지말고 넣어줍니다.
JoinColumn 이름은 아무거나 넣어도 되더라구요. 다만 직접 해보니 @ManyToOne 이랑 @OneToMany 랑 다르면 칼럼이 둘 다 만들어집니다.
생략
public class User 생략 {
@OneToMany
@JoinColumn(name = "fk_from_user_table", insertable = false, updatable = false)
private List<UserHistory> userHistories = new ArrayList<>();
}
생략
public class UserHistory extends BaseAuditingEntity {
생략
@ManyToOne
@JoinColumn(name = "fk_from_user_history_table")
private User user;
}
create table user_history (
생략
fk_from_user_history_table bigint, # <<<<
fk_from_user_table bigint, # <<<<
생략
)
User 쪽의 @OneToMany 에서 JoinColumn 이름을 기본 규칙에 맞추어 설정해두면 Many 쪽에서 별도로 설정해줄 필요는 없습니다. 이 경우에는 book_id 로 설정하시면 되겠네요.
히스토리를 저장하는 쪽에서 User 필드를 설정해서 저장해주도록 바꾸면 됩니다.
class UserHistoryListener {
@PostPersist
@PostUpdate
fun preUserPersistAndUpdate(o: Any) {
val repo = BeanUtils.getBean(UserHistoryRepo::class.java)
(o as? User)?.let {
val history = UserHistory().from(it)
/* from 의 코드
public UserHistory from(User user) {
name = user.getName();
email = user.getEmail();
this.user = user; // <<<<
return this;
}
*/
repo.save(history)
}
}
}
History 동작 트러블슈팅
이전 실습 코드에서 UserHistoryListener 에서 아무 생각 없이 saveAndFlush 를 했는데, 생뚱맞은 에러가 나왔습니다. user.id 가 null 이면 안 된대요. 에러 텍스트는 킵해두지 않았지만 이런 문구가 들어있었습니다.
뭐시기 null 임 (don't flush the Session after an exception occurs)
살짝 검색해보니 flush 가 일어나기 전에 오류가 난 거라네요. 구체적인 원인은 파봐야 안다고...
이번에 있었던 오류는 UserHistoryListener 에서 @Post flush 를 실행한 것 때문이었습니다. 강의 앞부분에서 @PrePersist를 User의 ID가 없다고 @PostPersist 로 바꿨거든요.
class UserHistoryListener {
@PostPersist // <<< PrePersist 에서 ID가 없다고 바꿈
@PostUpdate
fun preUserPersistAndUpdate(o: Any) {
val repo = BeanUtils.getBean(UserHistoryRepo::class.java)
(o as? User)?.let {
val history = UserHistory().from(it)
repo.save(history)
// repo.flush() 에러
}
}
}
이 안에서 user.id 를 못 찾는 문제는 추측컨데 DB에 User가 아직 삽입되기 이전의 시점이어서 그럴겁니다. @ManyToOne 으로 매핑된 User 정보를 넣어야 하는데 아직 DB에 업데이트가 안 되어서 ID가 실제 DB에는 존재하지 않는 거죠. 이 문제는 userRepo.saveAndFlush(user) 를 한다고 해도 해결되지 않아서 결국 flush 를 주석처리해야 했습니다.
이런 문제들은 영속성 문제들을 좀 배우면 왜 그런지 이해할 수 있겠죠. 아직은... 일단 넘기구요...
본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.
패스트캠퍼스: https://bit.ly/37BpXiC
#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #한번에끝내는JavaSpring웹개발마스터초격차패키지Online
아니 왜 졸린데 저녁시간인데
강의에서는 추가 실습으로 Review 같은 추가적인 엔티티들도 넣었습니다.
'FastCampus - 한번에 끝내는 Java|Spring 웹 개발 > 04 JPA' 카테고리의 다른 글
JPA Ch 7 영속성 - 개요 & 리얼 DB - 패스트캠퍼스 챌린지 29일차 (0) | 2022.02.21 |
---|---|
JPA Ch 6 릴레이션 M:N - 패스트캠퍼스 챌린지 28일차 (0) | 2022.02.20 |
JPA Ch 6 릴레이션, ERD, 1:1 - 패스트캠퍼스 챌린지 26일차 (0) | 2022.02.18 |
JPA Ch 5 Entity Listener (2) - 패스트캠퍼스 챌린지 25일차 (0) | 2022.02.17 |
JPA Ch 5 Entity Listener - 패스트캠퍼스 챌린지 24일차 (0) | 2022.02.16 |