[Spring] 1:N(oneToMany), N:1(ManyToOne)

WOOK JONG KIM·2022년 11월 20일
0

패캠_java&Spring

목록 보기
56/103
post-thumbnail

1:N

User

public class User extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NonNull
    private String name;

    @NonNull
    private String email;

    @Enumerated(value = EnumType.STRING)
    private Gender gender;

    @OneToMany
    //persist전에는 null로 인해 nullPointerException이 발생할 수 있으므로
    // 기본 생성자 넣어주는 것도 좋음
    private List<UserHistory> userHistories = new ArrayList<>();

}

UserHistory

public class UserHistory extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long userId;

    private String name;

    private String email;

    @Enumerated(value = EnumType.STRING)
    private Gender gender;
}

test


User user = new User();
        user.setName("david");
        user.setEmail("david@fastcampus.com");
        user.setGender(Gender.MALE);

        userRepository.save(user);

        user.setName("daniel");
        userRepository.save(user);

        user.setEmail("daniel@naver.com");
        userRepository.save(user);
        
List<UserHistory> result = userRepository.findByEmail("daniel@naver.com").getUserHistories();

result.forEach(System.out::println);

처음에 이렇게 테스트 진행 시 LazyInitializationException이 뜸 -> @OneToMany(fetch = FetchType.EAGER)

이렇게 적용하면 테스트는 통과하지만 inner Join을 사용하고 있음

또한 중간 매핑 테이블이 생성됨

create table user_user_histories (
       user_id bigint not null,
        user_histories_id bigint not null
    )

@JoinColumn 엔티티 속성이 조인에 대상인지 지정, 외래키 매핑시 사용

name: 참조할 테이블의 어떤 컬럼으로 조인 할 것인지 지정

insertable, updatable: 참조 테이블을 insert, update 처리 여부 (default: true)
→ insertable, updatable을 설정하지 않으면 cascade, 트랜잭션 등
JPA에서 자동으로 처리하는 것이 많아, 예상치 못한 오작동과 쿼리를 발생

@JoinColumn 지정 후

create table user_history (
       id bigint generated by default as identity,
        created_at timestamp,
        updated_at timestamp,
        email varchar(255),
        gender varchar(255),
        name varchar(255),
        user_id bigint,
        user_histories_id bigint,
        primary key (id)
    )

이 경우에도 forEach로 프린트는 안됨

// user.java
	@OneToMany(fetch = FetchType.EAGER)
    @JoinColumn(name="user_Id")
    private List<UserHistory> userHistories = new ArrayList<>();
    
// userHistory.java

	@Column(name="user_Id")
    private Long userId;

위와 같이 name 지정 시 정상 출력

하지만 보통 History라는 값은 UserEntity에서 수정하거나 추가하면 안됨!
-> 즉 Read Only로 되게끔 해야함

// 디폴트 값은 true
	
    @OneToMany(fetch = FetchType.EAGER)
    @JoinColumn(name = "user_id", insertable = false, updatable = false)
    private List<UserHistory> userHistories = new ArrayList<>();

N:1

@OneToMany에서 참조하는 값은 One에 해당하는 PK값을 Many쪽에서 FK로 가짐
→ UserHistory 테이블에서는 User에 Id값을 가짐

일반적인 상황에서는 @ManyToOne이 깔끔하게 엔티티를 구성
→ 해당 엔티티가 필요로 하는 FK 엔티티가 함께 가지고 있기 때문

UserHistory.java

@Entity
...
public class UserHistory extends BaseEntity{
		@Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String email;
		
		@ManyToOne
    private User user;
}

User.java

@Entity
...
public class User extends BaseEntity{
		...

		@OneToMany(fetch = FetchType.EAGER)
    @JoinColumn(name = "user_Id", insertable = false, updatable = false)
    @ToString.Exclude
    private List<UserHistory> userHistories = new ArrayList<>();
}

UserEntityListener.java

public class UserEntityListener {
    @PostPersist
    @PostUpdate
    public void postPersistAndPostUpdate(Object o){
        UserHistoryRepository userHistoryRepository = BeanUtils.getBean(UserHistoryRepository.class);

        User user = (User) o;
        UserHistory userHistory = new UserHistory();
        userHistory.setName(user.getName());
        userHistory.setEmail(user.getEmail());
        userHistory.setUser(user);

        userHistoryRepository.save(userHistory);
    }
}
System.out.println("userHistory.getUser" + userHistoryRepository.findAll().get(0).getUser());

위와 같이 양방향 설정시 히스토리 리파지토리를 통해서도 getUser 할 수 있다

userHistory.getUserUser(super=BaseEntity(createdAt=2022-11-20T14:07:53.384675, updatedAt=2022-11-20T14:07:53.424240), id=6, name=daniel, email=daniel@naver.com, gender=MALE)

JPA는 연관된 객체를 FK로 조회하는 것이 아니라 getter를 통해 가져오게 됌
@OneToMany, @ManyToOne, 양방향 중 선택은 어느 엔티티에 연관 엔티티가 필요한지 알아야함

  • UserHistory에 값을 조회해서 보면서 User 를 조회할 일은 거의 없음
  • User에서 변경이력을 보기 위해서 UserHistory를 조회할 일은 많음
  • User에서 @OneToMany를 활용해서 UserHistory에 연관관계를 맺는 것이 나은 방법

연관관계 차근차근 이해해보기!

book.java

...
public class Book extends BaseEntity{
		...
	@OneToOne(mappedBy = "book")
    @ToString.Exclude
    private BookReviewInfo bookReviewInfo;

    @OneToMany
    @JoinColumn(name="book_id")
    @ToString.Exclude
    private List<Review> reviews = new ArrayList<>();

    @ManyToOne
    @ToString.Exclude
    private Publisher publisher;
}
create table book (
       id bigint generated by default as identity,
        created_at timestamp,
        updated_at timestamp,
        author_id bigint,
        category varchar(255),
        name varchar(255),
        publisher_id bigint,
        primary key (id)
    )

위 3개 칼럼중 publisher_id만을 DDL에서 다루고 있음

bookReviewInfo의 경우 mappedBy를 통해 BookReviewInfo에서만 외래키로 다루도록(테이블에 book_id 존재) -> Owner가 BookReviewInfo

reviews의 경우 @JoinColumn(name = "book_id")로 외래키 매핑 해줌 -> Book 테이블에는 review id 존재 X

review에는 book_id(bigint)로 칼럼 존재

@ManyToOne인 Publisher는 자신이 Owner이면서 외래키를 가지고 있음

	//publisher -> 에는 book_id 칼럼이 존재하지 않음
	@OneToMany
    @JoinColumn(name = "publisher_id")
    private List<Book> book = new ArrayList<>();

결론적으로 두 테이블중 외래키를 가지고 있는 테이블은 한 테이블이고, 외래키를 가진 테이블이 Owner라고 이해해자!

BookReviewInfo

...
public class BookReviewInfo extends BaseEntity{
	@Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToOne(optional = false)
    private Book book;

    private float averageReviewScore;

    private int reviewCount;
}

Publisher

...
public class Publisher extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany
    @JoinColumn(name = "publisher_id")
    private List<Book> books = new ArrayList<>();
}

Review

...
public class Review extends BaseEntity{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    private String content;

    private float score;

    @ManyToOne
    private User user;

    @ManyToOne
    private Book book;
}

User

public class User extends BaseEntity{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NonNull
    private String name;

    @NonNull
    private String email;

    @Enumerated
    private Gender gender;

    @OneToMany(fetch = FetchType.EAGER)
    @JoinColumn(name = "user_Id", insertable = false, updatable = false)
    @ToString.Exclude
    private List<UserHistory> userHistories = new ArrayList<>();

    @OneToMany
    @JoinColumn(name = "user_id")
    @ToString.Exclude
    private List<Review> reviews = new ArrayList<>();
}

UserHistory

public class UserHistory extends BaseEntity{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String email;

    @ManyToOne
    private User user;
}
profile
Journey for Backend Developer

0개의 댓글