일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 인프런
- jpa
- 백준
- pointcut
- java
- 스프링
- 자바
- Servlet
- 스프링 핵심 기능
- QueryDSL
- db
- 스프링 핵심 원리
- transaction
- spring
- AOP
- Thymeleaf
- 그리디
- Exception
- 김영한
- JDBC
- SpringBoot
- kotlin
- 알고리즘
- Android
- http
- springdatajpa
- Proxy
- Greedy
- Spring Boot
- JPQL
- Today
- Total
개발자되기 프로젝트
1 : N 연관관계 - 1 본문
이전에 만들어둔 User entity와 User History는 1:N연관관계를 갖는다.
User는 현재 회원정보를 가지고 있는 테이블이고
UserHistory는 특정 user id에 저장된 값이 변경된 내역을 저장하는 table이다.
1. test
@Test
void userRelationTest(){
User user = new User();
user.setName("David");
user.setEmail("dabid@navergoogle.com");
user.setGender(Gender.MALE);
userRepository.save(user);
userHistoryRepository.findAll().forEach(System.out::println);
}
}
먼저 relation을 테스트 하기 위해 세팅부터 해주자.
이전에 리스너를 적용했기 때문에, 새로운 user의 정보가 insert되면 리스너에 의해 userHistoryRepository에 저장된다.
짜잔, 근데 user Id가 null이다. 왜지...?
UserHistory(
super=BaseEntity(createdAt=2021-06-20T14:45:19.869896, updatedAt=2021-06-20T14:45:19.869896),
id=1, userId=null, name=David, email=dabid@navergoogle.com)
리스너 코드를 살펴보자.
public class UserEntityListener {
// @Autowired
//private UserHistoryRepository userHistoryRepository;
@PrePersist
@PreUpdate
public void prePersistpreUpdate(Object o){
UserHistoryRepository userHistoryRepository = BeanUtils.getBean(UserHistoryRepository.class);
User user = (User) o;
UserHistory userHistory = new UserHistory();
userHistory.setUserId(user.getId());
userHistory.setName(user.getName());
userHistory.setEmail(user.getEmail());
userHistoryRepository.save(userHistory);
}
}
@PrePersist, @PreUpdate가 적용되어 있다. 따라서 삽입 전, 업데이트 전 에 작동하게 된다.
즉, user의 정보가 입력되기 전에 코드가 돌아가기 때문에 위에서 처럼 user id가 null인 것 이다.
@PostPersis, @PostUpdate로 변경해주자. 이렇게 해주면 db에 저장한 후 바로 history에 저장!
UserHistory(
super=BaseEntity(createdAt=2021-06-20T14:55:15.674905, updatedAt=2021-06-20T14:55:15.674905),
id=1, userId=6, name=David, email=dabid@navergoogle.com)
짠! user ID까지 잘 저장이되었다.
2. 같은 user에 대하여 history를 쌓아보자.
@Test
void userRelationTest(){
User user = new User();
user.setName("David");
user.setEmail("dabid@navergoogle.com");
user.setGender(Gender.MALE);
userRepository.save(user);
user.setName("daniel");
userRepository.save(user);
user.setEmail("daniel@navergoogle.com");
userRepository.save(user);
userHistoryRepository.findAll().forEach(System.out::println);
}
실행 결과. 저장/업데이트 할 때 마다 history에 저장이 되었다.
UserHistory(
super=BaseEntity(createdAt=2021-06-20T14:58:38.716325, updatedAt=2021-06-20T14:58:38.716325),
id=1, userId=6, name=David, email=dabid@navergoogle.com)
UserHistory(
super=BaseEntity(createdAt=2021-06-20T14:58:38.799525, updatedAt=2021-06-20T14:58:38.799525),
id=2, userId=6, name=daniel, email=dabid@navergoogle.com)
UserHistory(
super=BaseEntity(createdAt=2021-06-20T14:58:38.807498, updatedAt=2021-06-20T14:58:38.807498),
id=3, userId=6, name=daniel, email=daniel@navergoogle.com)
지금은 데이터 수가 얼마 안되니 findAll로 했는데.. 특정 id로 찾을 수 있도록 바꾸자.
UserHistoryRepository에 findByUserId를 추가해주자.
public interface UserHistoryRepository extends JpaRepository<UserHistory, Long> {
List<UserHistory> findByUserId(Long userId);
}
테스트에 findall을 변경해주자.
List<UserHistory> result = userHistoryRepository
.findByUserId(userRepository
.findByEmail("daniel@navergoogle.com").getId());
result.forEach(System.out::println);
id값을 통해서 잘 가져오는걸 확인할 수 있다.
UserHistory(
super=BaseEntity(createdAt=2021-06-20T15:05:18.871142, updatedAt=2021-06-20T15:05:18.871142),
id=1, userId=6, name=David, email=dabid@navergoogle.com)
UserHistory(
super=BaseEntity(createdAt=2021-06-20T15:05:18.941319, updatedAt=2021-06-20T15:05:18.941319),
id=2, userId=6, name=daniel, email=dabid@navergoogle.com)
UserHistory(
super=BaseEntity(createdAt=2021-06-20T15:05:18.948234, updatedAt=2021-06-20T15:05:18.948234),
id=3, userId=6, name=daniel, email=daniel@navergoogle.com)
table의 relation을 활용하긴 했지만 JPA에서 제공하는 기능은 아니다.
3. @OneToMany
이전에 1:1관계에서 @OneToOne을 사용했다. 이번에는 1:N이니까 @OneToMany 를 사용할 것이다.
User 1개에 대한 history는 n개이기 때문에 collection 타입으로 선언한다.
User class에 아래와 같이 선언해준다.
@OneToMany(fetch = FetchType.EAGER) //이후에 자세히
private List<UserHistory> userHistories = new ArrayList<>();
그리고 테스트 코드를 살짝 수정해준다.
이 전과의 차이점은 userRepository에서 email로 id를 찾은 다음 그 id를 가진 user의 history를 바로 불러온다.
@Test
void userRelationTest(){
User user = new User();
user.setName("David");
user.setEmail("dabid@navergoogle.com");
user.setGender(Gender.MALE);
userRepository.save(user);
user.setName("daniel");
userRepository.save(user);
user.setEmail("daniel@navergoogle.com");
userRepository.save(user);
//userHistoryRepository.findAll().forEach(System.out::println);
// List<UserHistory> result = userHistoryRepository.findByUserId(userRepository.findByEmail("daniel@navergoogle.com").getId());
List<UserHistory> result = userRepository.findByEmail("daniel@navergoogle.com")
.getUserHistories();
result.forEach(System.out::println);
}
결과를 보면..? table 생성 로그를 보면 이상한게 있다.
user_user_histories??? 난 이런거 만든 적 없는데?
Hibernate:
create table user_user_histories (
user_id bigint not null,
user_histories_id bigint not null
)
4. @JoinColumn
이를 해결하기 위해서 annotation을 추가해주자. @OneToMany 말고 @JoinColumn을 추가했다.
@JoinColumn : entity가 어떤 column으로 join을 할 지 지정해주는 annotation. 외래 키 맵핑에 사용된다.
- name : 이름에는 외래 키 이름을 어떻게 할지 정해줌. 맵핑하는 것이 아님.
FK는 N에 해당 하는 table에 들어가야 한다.
즉 해당 이름으로 N에 해당하는 Entity의 table에 FK로서 들어간다는 의미.
@OneToMany(fetch = FetchType.EAGER) //이후에 자세히
@JoinColumn //엔티티가 어떤 컬럼으로 조인을 하게 될지 지정해주는 annotation
private List<UserHistory> userHistories = new ArrayList<>();
- 결과
user_history table에 user_histories_id가 생성이 되었다. 얘가 @Joincolumn에 의해서 생긴 놈이다.
즉 user_histories는 필드명이고, id는 참조하는 테이블의 pk 컬럼 명이다.
Hibernate:
create table user_history (
id bigint generated by default as identity,
created_at timestamp,
updated_at timestamp,
email varchar(255),
name varchar(255),
user_id bigint,
user_histories_id bigint,
primary key (id)
)
@JoinColumn에서 default 값이 해당 필드의 이름을 활용하는 것 인데, 이름을 따로 지정할 수 있다.
UserHistory Table의 user_id와 맵핑(FK)
@OneToMany(fetch = FetchType.EAGER) //이후에 자세히
@JoinColumn(name = "user_id") //엔티티가 어떤 컬럼으로 조인을 하게 될지 지정해주는 annotation
private List<UserHistory> userHistories = new ArrayList<>();
이러면 에러가 발생한다.
Table [user_history] contains physical column name [user_id] referred to by multiple logical column names: [user_id], [userId]
user_history table이 user_id라는 column이름을 포함하는데, user_id인지 userId로 써야하는지 모르겠다는 뜻.
UserHistory class에서 userId의 column명을 "user_id"로 확실히 하자.
public class UserHistory extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "user_id")
private Long userId;
private String name;
private String email;
}
테스트 결과를 보면 user_history table에 user id가 생성이 된 것을 볼 수 있다.
Hibernate:
create table user_history (
id bigint generated by default as identity,
created_at timestamp,
updated_at timestamp,
email varchar(255),
name varchar(255),
user_id bigint,
primary key (id)
)
history도 잘 불러와진다.
UserHistory(super=BaseEntity(createdAt=2021-06-20T22:31:12.270279, updatedAt=2021-06-20T22:31:12.270279), id=1, userId=6, name=David, email=dabid@navergoogle.com)
UserHistory(super=BaseEntity(createdAt=2021-06-20T22:31:12.353338, updatedAt=2021-06-20T22:31:12.353338), id=2, userId=6, name=daniel, email=dabid@navergoogle.com)
UserHistory(super=BaseEntity(createdAt=2021-06-20T22:31:12.360418, updatedAt=2021-06-20T22:31:12.360418), id=3, userId=6, name=daniel, email=daniel@navergoogle.com)
그리고 userHistory는 user entity에서 수정할 수 없는 read only 값이다.
이러면 user entity에서 UserHistory에 대해 삽입/수정이 불가능 하다.
@OneToMany(fetch = FetchType.EAGER) //이후에 자세히
@JoinColumn(name = "user_id", insertable = false, updatable = false) //엔티티가 어떤 컬럼으로 조인을 하게 될지 지정해주는 annotation
private List<UserHistory> userHistories = new ArrayList<>();
이해가 안된다...ㅜㅜ
[1차 정리]
1. user.user_histories로 user_history table의 user_id 관리
2. 맵핑한 객체가 관리하는 fk가 다른 테이블에 있다.
3. 보통은 자신이 맵핑한 테이블의 외래키를 관리한다. 1:N 단방향의 경우 반대로 관리된다.
4. 외래키는 N(user_history)에 있지만 단방향으로 N쪽에 필드(List user_histories)가 없다.
5. 즉, User에서 관리해야할 FK user_if가 user_history에 있다??
6. N(user_history)은 1(user)에서 수정할 수 없다.
[2차 정리]
7. @OneToMany는 참조되는 값을 one에서 가지고 있지 않다. user history table에서 user id값을 가진다.
8. 그래서 one to many에서 참조하는 값은 one에 해당하는 PK id를 many쪽에서 FK로 가지고 있게 된다.
10. 즉 각각의 history는 user의 PK에 해당하는 user_id를 FK로 가지고 있어야 user를 찾을 수 있다.
11. user의 PK에 해당하는 user_id를 가진 user_history를 찾고 걔네들을 User의 userHistories에 넣는다.?
[3차 정리]
12. 한(one) user에 대해 여러개(many)의 history가 존재.
각 history에는 누구의 history인지 식별하도록 FK로 user_id필요.
즉 user(one)의 PK가 FK로서 user_history(many)에 존재.
이 때 user.userHistories(배열)와 userHistory.user_id가 맵핑되어
해당 user에 맞는 history가 user.userHistories에 맵핑됨.
즉 histories에서 해당 user의 PK와 일치하는 FK를 가진 history를 찾아오는 구조.
'JPA' 카테고리의 다른 글
N : 1 연관관계 #2 (0) | 2021.06.21 |
---|---|
N : 1 연관관계 - @ManyToOne (0) | 2021.06.21 |
1:1 연관관계 - 2 (0) | 2021.06.20 |
mappedBy() (0) | 2021.06.20 |
1:1 연관관계 - 1 (0) | 2021.06.17 |