JPA 연관관계 (N:1)

이종윤·2022년 2월 9일
0

Spring JPA

목록 보기
11/23
post-thumbnail

😎N:1 연관관계 알아보자~~

OneToMany 에서는 참조되는 값을 One에서 가지고 있지 않다. 만약 가져야 한다면UserHistoryId가 1,2,3,4,5 즉 어떤 값의 배열이 된다. 그래서 One에 해당하는 PK ID를 Many쪽에서 FK이다.
일반적인 상황에서는 @ManyToOne이 더 자주 사용된다. 해당 Entity가 필요한 FK 값을 Entity가 함께 가지고 있기 때문이다.

실습으로 알아보자.

지난시간에 User와 UserHistory를 수정하여 보자.

@ManyToOne

UserHistory

@Entity
@NoArgsConstructor
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
//@EntityListeners(value = AuditingEntityListener.class)
public class UserHistory extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // user의 insert update에는 영향을 주지 않아야함. // ManyToOne할때는 불필요한 컬럼이다.
//    @Column(name = "user_id", insertable = false,updatable = false)
//    private Long userId;

    private String name;
    private String email;

    @ManyToOne  // 다 대 일
    private User user;
}

UserEntityListener

    @PostPersist
    @PostUpdate
    public void prePersistAndPreUpdate(Object o) {
        /*
        * 여기서 주의해야할 점은 Listener는 @Autowired로 빈을 가져오지 못한다. 그래서 BeanUtils클래스를 이용해서 주입해 줘야 한다.
        * */
        UserHistoryRepository userHistoryRepository = BeanUtils.getBean(UserHistoryRepository.class);
        System.out.println("User 생성 및 수정 하기전에 History남기는 쿼리");
        User user = (User) o;

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

        userHistoryRepository.save(userHistory);
    }

User

    @OneToMany(fetch = FetchType.EAGER)
    @JoinColumn(name = "user_id", insertable = false, updatable = false)
    @ToString.Exclude //순환참조 예방.
    private List<UserHistory> userHistories
            = new ArrayList<>(); //getUserHistoryes를 했을때 NullPointException이 뜨지 않게 기본리스트 넣어주자.
    // Jpa에서 해당값이 존재하지 않으면 빈 리스트를 자동으로 넣어주기는 하지만, persist하기 전에 해당 값이 Null이기 때문에 로직에 따라 오류가 생길수있다.

TEST

        //결론은 User와 UserHistory데이터를 모두 가져오기 위헤서는 N : 1을 사용하여 userHistory 즉 N쪽에서 getUser를 사용하여 검색한다.
        System.out.println("UserHistory.getUser() - (N : 1) : " + userHistoryRepository.findAll().get(0).getUser());

DDL

create table user (
       id bigint generated by default as identity,
        created_at timestamp,
        updated_at timestamp,
        email varchar(255),
        gender varchar(255),
        name varchar(255),
        primary key (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, //FK
        primary key (id)
    )

Query

    select
        user0_.id as id1_3_0_,
        user0_.created_at as created_2_3_0_,
        user0_.updated_at as updated_3_3_0_,
        user0_.email as email4_3_0_,
        user0_.gender as gender5_3_0_,
        user0_.name as name6_3_0_,
        userhistor1_.user_id as user_id6_4_1_,
        userhistor1_.id as id1_4_1_,
        userhistor1_.id as id1_4_2_,
        userhistor1_.created_at as created_2_4_2_,
        userhistor1_.updated_at as updated_3_4_2_,
        userhistor1_.email as email4_4_2_,
        userhistor1_.name as name5_4_2_,
        userhistor1_.user_id as user_id6_4_2_ 
    from
        user user0_ 
    left outer join
        user_history userhistor1_ 
            on user0_.id=userhistor1_.user_id 
    where
        user0_.id=?
UserHistory.getUser() - (N : 1) : User(super=BaseEntity(createdAt=2022-02-08T21:56:07.620, updatedAt=2022-02-08T21:56:07.761), id=6, name=danielll, email=daniel@fast.com, gender=MALE)

✅ User-review-Book 테이블 관계 만들어보자.

Entity / Review

@Entity
@NoArgsConstructor
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
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;
}

Entity / User

@NoArgsConstructor
@AllArgsConstructor
@RequiredArgsConstructor
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@Builder
@Entity // 해당 객체가 JPA에서 관리하고 있는 객체인것을 정의.
@EntityListeners(value = {UserEntityListener.class})
//@Table(name = "user", indexes = {@Index(columnList = "name")}, uniqueConstraints = {@UniqueConstraint(columnNames = {"email"})})
public class User extends BaseEntity {

    @Id // 엔티티에는 식별자가 필요한데 @ID로 표현.
    @GeneratedValue(strategy = GenerationType.IDENTITY) // GenerationType (IDENTITY, SEQUENCE, TABLE, AUTO)
    private Long id;

    @NonNull
    private String name;

    @NonNull
    private String email;

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

    @OneToMany(fetch = FetchType.EAGER)
    @JoinColumn(name = "user_id", insertable = false, updatable = false)
    @ToString.Exclude //순환참조 예방.
    private List<UserHistory> userHistories
            = new ArrayList<>(); //getUserHistoryes를 했을때 NullPointException이 뜨지 않게 기본리스트 넣어주자.
    // Jpa에서 해당값이 존재하지 않으면 빈 리스트를 자동으로 넣어주기는 하지만, persist하기 전에 해당 값이 Null이기 때문에 로직에 따라 오류가 생길수있다.

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

Entity / Publisher

@Entity
@NoArgsConstructor
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
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<>();

}

Entity / Book

@Data
@ToString(callSuper = true)                //상속받은 클래스에 대해 처리해줘야한다. ToString을 재정의 한다.
@EqualsAndHashCode(callSuper = true)       //EqualsAndHashCode를 재정의해준다.
//@EntityListeners(value = AuditingEntityListener.class)
public class Book extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String category;
    private Long authorId;

    @OneToMany  // 1 : N \ Book : Review
    @JoinColumn(name = "book_id")   //중간 테이블 방지
    @ToString.Exclude               //ToString 순환참조 방지
    private List<Review> reviews = new ArrayList<>();

    @ManyToOne  // N : 1 \ Book : publisher
    @ToString.Exclude
    private Publisher publisher;


    @OneToOne(mappedBy = "book")
    @ToString.Exclude // ToString 순환참조 걸린다. 릴레이션은 단방향으로 걸고 ToString은 제외해야한다.
    private BookReviewInfo bookReviewInfo;

Repository

ReviewRepository
PublisherRepository

public interface PublisherRepository extends JpaRepository<Publisher, Long> {
}
public interface ReviewRepository extends JpaRepository<Review, Long> {
}

DDL 잘 나오는지 한번 볼까?

    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)
    )
Hibernate: 
    
    create table book_review_info (
       id bigint generated by default as identity,
        created_at timestamp,
        updated_at timestamp,
        average_review_score float not null,
        review_count integer not null,
        book_id bigint not null,
        primary key (id)
    )
Hibernate: 
    
    create table publisher (
       id bigint generated by default as identity,
        created_at timestamp,
        updated_at timestamp,
        name varchar(255),
        primary key (id)
    )
Hibernate: 
    
    create table review (
       id bigint generated by default as identity,
        created_at timestamp,
        updated_at timestamp,
        content varchar(255),
        score float not null,
        title varchar(255),
        book_id bigint,
        user_id bigint,
        primary key (id)
    )
Hibernate: 
    
    create table user (
       id bigint generated by default as identity,
        created_at timestamp,
        updated_at timestamp,
        email varchar(255),
        gender varchar(255),
        name varchar(255),
        primary key (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)
    )

TEST 해볼까?

@SpringBootTest
public class BookRepositoryTest {
    @Autowired
    private BookRepository bookRepository;
    @Autowired
    private PublisherRepository publisherRepository;
    @Autowired
    private ReviewRepository reviewRepository;
    @Autowired
    private UserRepository userRepository;
    
    @Test
    @Transactional //트랜젝션할때 배운다.
    void bookRelationTest() {
        // 북 정보 저장.
        givenBookAndReview();

        //유저 값 불러오자. 원래는 인증 Data에서 받아오겠지??
        User user = userRepository.findByEmail("martin@fast.com");

        System.out.println("Review : " + user.getReviews());    // 유저에 리뷰 보기
        System.out.println("Book : " + user.getReviews().get(0).getBook()); // 유저의 리뷰의 책정보 불러오기
        System.out.println("Publisher : " + user.getReviews().get(0).getBook().getPublisher()); // 유저의 리뷰의 책정보에 출판사 정보
    }

    private void givenBookAndReview() {
        givenReview(givenUser(), givenBook(givenPublisher()));
    }

    private User givenUser() {
        //유저 값 불러오자.
        return userRepository.findByEmail("martin@fast.com");
    }

    private void givenReview(User user, Book book) {
        // 리뷰
        Review review = new Review();
        review.setTitle("내 인생을 바꾼 책");
        review.setContent("너무너무 재미있고 즐거운 책이었어요.");
        review.setScore(5.0f);
        review.setUser(user);   // user와 관계
        review.setBook(book);   // book과 관계

        reviewRepository.save(review);
    }

    private Book givenBook(Publisher publisher) {
        // 책 하나 만들어 주자.
        Book book = new Book();
        book.setName("JPA 초격차 패키지");
        book.setPublisher(publisher); // 출판사는 publisher를 생성해서 받아와야겠지?

        return bookRepository.save(book);
    }

    private Publisher givenPublisher() {
        // 출판사 하나 생성해주자.
        Publisher publisher = new Publisher();
        publisher.setName("패스트캠퍼스");

        return publisherRepository.save(publisher);
    }
}

DDL은 위에서 봤고

getter를 호출하는 것 만으로 값 들을 조회 할 수 있다.!!!!!!!😮😮😮

    select
        reviews0_.user_id as user_id8_4_0_,
        reviews0_.id as id1_4_0_,
        reviews0_.id as id1_4_1_,
        reviews0_.created_at as created_2_4_1_,
        reviews0_.updated_at as updated_3_4_1_,
        reviews0_.book_id as book_id7_4_1_,
        reviews0_.content as content4_4_1_,
        reviews0_.score as score5_4_1_,
        reviews0_.title as title6_4_1_,
        reviews0_.user_id as user_id8_4_1_,
        book1_.id as id1_1_2_,
        book1_.created_at as created_2_1_2_,
        book1_.updated_at as updated_3_1_2_,
        book1_.author_id as author_i4_1_2_,
        book1_.category as category5_1_2_,
        book1_.name as name6_1_2_,
        book1_.publisher_id as publishe7_1_2_,
        publisher2_.id as id1_3_3_,
        publisher2_.created_at as created_2_3_3_,
        publisher2_.updated_at as updated_3_3_3_,
        publisher2_.name as name4_3_3_,
        bookreview3_.id as id1_2_4_,
        bookreview3_.created_at as created_2_2_4_,
        bookreview3_.updated_at as updated_3_2_4_,
        bookreview3_.average_review_score as average_4_2_4_,
        bookreview3_.book_id as book_id6_2_4_,
        bookreview3_.review_count as review_c5_2_4_ 
    from
        review reviews0_ 
    left outer join
        book book1_ 
            on reviews0_.book_id=book1_.id 
    left outer join
        publisher publisher2_ 
            on book1_.publisher_id=publisher2_.id 
    left outer join
        book_review_info bookreview3_ 
            on book1_.id=bookreview3_.book_id 
    where
        reviews0_.user_id=?

너무 잘 나오죠?

Review : [Review(super=BaseEntity(createdAt=2022-02-09T13:54:56.037, updatedAt=2022-02-09T13:54:56.037), id=1, title=내 인생을 바꾼 책, content=너무너무 재미있고 즐거운 책이었어요., score=5.0, user=User(super=BaseEntity(createdAt=2022-02-09T13:54:55.503, updatedAt=2022-02-09T13:54:55.503), id=1, name=martin, email=martin@fast.com, gender=null), book=Book(super=BaseEntity(createdAt=2022-02-09T13:54:56.026, updatedAt=2022-02-09T13:54:56.026), id=1, name=JPA 초격차 패키지, category=null, authorId=null))]
Book : Book(super=BaseEntity(createdAt=2022-02-09T13:54:56.026, updatedAt=2022-02-09T13:54:56.026), id=1, name=JPA 초격차 패키지, category=null, authorId=null)
Publisher : Publisher(super=BaseEntity(createdAt=2022-02-09T13:54:55.986, updatedAt=2022-02-09T13:54:55.986), id=1, name=패스트캠퍼스, books=[])

위의 쿼리가 물론 어려운 쿼리가 아니다. Join여러게 붙이고 값 불러오는건데, 와.... 이렇게 쉽게 자동으로 만들어 준다고??

profile
OK가자

0개의 댓글