Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
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
Archives
Today
Total
관리 메뉴

개발자되기 프로젝트

1 : N 연관관계 - 1 본문

JPA

1 : N 연관관계 - 1

Seung__ 2021. 6. 21. 19:55

이전에 만들어둔 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
Comments