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:1 연관관계 - 2 본문

JPA

1:1 연관관계 - 2

Seung__ 2021. 6. 20. 14:38

 

본격적으로 entity의 관계를 코드로 구현해보장.

1. Book 과 Book Review Info는 1:1 관계


이번에는 test코드에 BookRepository에 대한 의존성도 추가해보자.

 

Book Review repository에서 아이디가 1인 리뷰를 찾고 그에 해당하는 book id를 가져와서 해당 book에 대한 정보를 출력해보자.

@SpringBootTest
class BookReviewInfoRepositoryTest {

    @Autowired
    private BookReviewInfoRepository bookReviewInfoRepository;

    @Autowired
    private BookRepository bookRepository;

    @Test
    void curdTest2(){

        Book book =  new Book();
        book.setName("JPA haha");
        book.setAuthorId(1L);
        book.setPublisherId(1L);

        bookRepository.save(book);


        System.out.println(">>>>>>>" + bookRepository.findAll());

        BookReviewInfo bookReviewInfo = new BookReviewInfo();
        bookReviewInfo.setBookId(1L);
        bookReviewInfo.setAverageReviewScore(4.5f);
        bookReviewInfo.setReviewCount(2);

        bookReviewInfoRepository.save(bookReviewInfo);

        System.out.println(">>>>>>>" + bookReviewInfoRepository.findAll());

        Book result=  bookRepository.findById(
                bookReviewInfoRepository
                        .findById(1L)
                        .orElseThrow(RuntimeException::new)
                        .getBookId()).orElseThrow(RuntimeException::new);

        System.out.println(">>>>" + result);

    }
}

- 결과

null point exception도 발생하고,  왜 Book의 id가 왜 6,,,??, reviewinfo의 아이디는 왜 또 7..?

>>>>>>>[Book(super=BaseEntity(createdAt=2021-06-17T22:05:02.280422, 
updatedAt=2021-06-17T22:05:02.280422), id=6, name=JPA haha, authorId=1, publisherId=1, category=null)]

>>>>>>>[BookReviewInfo(super=BaseEntity(createdAt=2021-06-17T22:05:02.533606, 
updatedAt=2021-06-17T22:05:02.533606), id=7, bookId=1, averageReviewScore=4.5, reviewCount=2)]

- 원인

왜냐  sql파일을 처음 만들 때 아래와 같이 만들었기 때문에 id가 6부터 적용된 것.

즉,  hibernate_sequence값을 5번 증가시켰기 때문에 6부터 등장.

 

좀 더 자세히 살펴보면 

1) entity에서 @GeneratedValue 정의의 기본값은 Auto, 

2) test DB로 H2 DB사용하고 Auto의 기본 값은 Sequence이다.

3)그래서 Entity에서 id값을 생성하면서 call next value for hibernate_sequence가 매번 적용됨.

4) 이 때 hibernate sequence를 공용으로 사용해서 발생한 문제.

 

- 해결 : sequence를 분리하거나,  @generatedValue의 전략을 identity로 수정

   MySQL의 기본 전략이 identity이다. table마다 해당 id값을 autoincreasement build로 지정하여 지속적으로 증가시킴.

 

   모든 @generatedValue 의 전략을 수정해주자.

@GeneratedValue(strategy = GenerationType.IDENTITY)

- 결과 

 IDENTITY로 바꿔 TABLE마다 ID를 다르게 했더니, HIBERNATE_SEQUENCE를 찾을 수 없다고 한다.

- 해결 : SQL에서 HIBERNATE를 증가시키는 쿼리를 지워주자. SEQUENCE TYPE을 안쓸거니깤ㅋㅋ

 

- 결과 : 성공~

 

id가 1인 Book Review info에 해당하는 Book id는 1이고 이에 해당하는 book의 정보를 불러왔다.

>>>>>>>[BookReviewInfo(
       super=BaseEntity(createdAt=2021-06-17T22:26:26.815618, 
                       updatedAt=2021-06-17T22:26:26.815618), \
        id=1, bookId=1, averageReviewScore=4.5, reviewCount=2)]

>>>>Book(
       super=BaseEntity(createdAt=2021-06-17T22:26:26.626822, 
                       updatedAt=2021-06-17T22:26:26.626822),
        id=1, name=JPA haha, authorId=1, publisherId=1, category=null)

 

그리고 table생성되는 것을 좀 더 살펴보자.

 

@generatedValue의 전략은 IDENTITY로 변경한 결과를 볼 수 있다.

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,
        book_id bigint not null,
        review_count integer not null,
        primary key (id)
    )

 

 


관계형 DB는 특정 table의 PK를 다른 table에서 FK로 활용하게 되어있다. 

현재까지 구현한 방식이 RDB를 기반으로 하는 관계를 그대로 ~ 구현했다.

 

 ??? : 짜잔~ 미리 준비해뒀어요

 

2. @OneToOne

<<단방향>> 

 

1) 먼저 BookReviewInfo의 코드를 살짝 수정해주자.

 

Book과 BookReviewInfo는 1대1 연관관계이다. 

 

Book ReviewInfo 클래스에서 사용하는 Book과 1:1 연관관계 맵핑을 하기 위해서는 Book에 @OneToOne을 붙인다.

 

이를 통해 BookReviewInfo와 Book이 1대1 연관관계를 가질 수 있게된다.

@Entity
@NoArgsConstructor
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class BookReviewInfo extends BaseEntity{

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

    //private long bookId;
    @OneToOne //1:1 연관관계 맵핑
    private Book book;
    //테이블에는 book id라는 long값이 실제로 존재하지만
    //jpa에서 entity로 set/get을 하면 relation을 자동으로 맺어줌.


    private float averageReviewScore;
    //flot, Flot 차이? null 허용 여부

    private int reviewCount;
    

}

2) 테스트 코드도 수정이 필요하다.

 

목적은 bookReviewInfoRepository에서 id로 review info를 찾은 다음 바로 Book entity를 가져오는 것이다.

 

코드를 조금 정리를 위해서 Book, BookReviewInfo를 생성하는 메소드를 별도로 만들었다.

 

그러면 실질적으로 Book과 BookReviewInfo를 1대1로 맵핑을 하려면 어떻게 해야할까?

 

set을 통해서 relation을 만들 수 있다. 즉 bookReviewInfo에서 book을 set해주면 된다.

BookReviewInfo bookReviewInfo = new BookReviewInfo();
        bookReviewInfo.setBook(givenBook());

이를 통해 실질적으로 relation이 생기고 get을 통해 바로 불러올 수 있다.

 

이전에는 review info repository에서 id로 review info찾고 거기서 book id찾고 그 id로 book repository에서 book을 찾다.. 많이 개선되었다.

 

 @Test
    void curdTest2(){

        givenBookReviewInfo();

        Book result= bookReviewInfoRepository
                        .findById(1L)
                        .orElseThrow(RuntimeException::new)
                        .getBook();

        System.out.println(">>>>" + result);

    }

    private Book givenBook(){
        Book book =  new Book();
        book.setName("JPA haha");
        book.setAuthorId(1L);
        book.setPublisherId(1L);

        return bookRepository.save(book);


    }

    private void givenBookReviewInfo(){

        BookReviewInfo bookReviewInfo = new BookReviewInfo();
        bookReviewInfo.setBook(givenBook());
        bookReviewInfo.setAverageReviewScore(4.5f);
        bookReviewInfo.setReviewCount(2);

        bookReviewInfoRepository.save(bookReviewInfo);

        System.out.println(">>>>>>>" + bookReviewInfoRepository.findAll());
    }

 

- 결과

 

book_review_info table생성을 보자.

 

위에서 private Long bookId를 지워버렸는데, table에 book_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,
        primary key (id)
    )

 

이번에는 select 쿼리를 보자.

 

left outer join을 보면 book과 book review info를 조회하는 것을 볼 수 있다. 매칭시켜서 보여준당.

Hibernate: 
    select
        bookreview0_.id as id1_1_0_,
        bookreview0_.created_at as created_2_1_0_,
        bookreview0_.updated_at as updated_3_1_0_,
        bookreview0_.average_review_score as average_4_1_0_,
        bookreview0_.book_id as book_id6_1_0_,
        bookreview0_.review_count as review_c5_1_0_,
        book1_.id as id1_0_1_,
        book1_.created_at as created_2_0_1_,
        book1_.updated_at as updated_3_0_1_,
        book1_.author_id as author_i4_0_1_,
        book1_.category as category5_0_1_,
        book1_.name as name6_0_1_,
        book1_.publisher_id as publishe7_0_1_ 
    from
        book_review_info bookreview0_ 
    left outer join
        book book1_ 
            on bookreview0_.book_id=book1_.id 
    where
        bookreview0_.id=?

 

3. @OneToOne의 속성


 

1) optional

디폴트는 true로 falst로 하는 경우 무조건 값이 있어야 한다.

//(Optional) Whether the association is optional. 
//If set * to false then a non-null relationship must always exist. 
boolean optional() default true;

바꿔서 테스트를 돌려보자.

  @OneToOne(optional = false)

select쿼리를 보면 차이점이 있다.

left outer join이 inner join으로 변경되었다.

Hibernate: 
    select
        bookreview0_.id as id1_1_0_,
        bookreview0_.created_at as created_2_1_0_,
        bookreview0_.updated_at as updated_3_1_0_,
        bookreview0_.average_review_score as average_4_1_0_,
        bookreview0_.book_id as book_id6_1_0_,
        bookreview0_.review_count as review_c5_1_0_,
        book1_.id as id1_0_1_,
        book1_.created_at as created_2_0_1_,
        book1_.updated_at as updated_3_0_1_,
        book1_.author_id as author_i4_0_1_,
        book1_.category as category5_0_1_,
        book1_.name as name6_0_1_,
        book1_.publisher_id as publishe7_0_1_ 
    from
        book_review_info bookreview0_ 
    inner join
        book book1_ 
            on bookreview0_.book_id=book1_.id 
    where
        bookreview0_.id=?

즉 자바 객체 속성에 따라 최적의 쿼리를 생성시키는 것이 JPA의 역할!

 

여기까지는 이해하기 어렵지 않다..

4. 반대로 Book 에서 BookReviewInfo정보를 가져오자.(양방향)


이전에는 Repository --> Book 의 단방향 관계였는데,

Book --> Reposioty의 단방향 관계를 추가하여 양방향 관계를 만들어 보자.

 

아래와 같이 테스트를 진행하고 싶다. 

book review info repository에서 book을 불러오고

그 book에서 다시 book review info를 불러오고 싶다!

    @Test
    void curdTest2(){

        givenBookReviewInfo();

        Book result= bookReviewInfoRepository
                        .findById(1L)
                        .orElseThrow(RuntimeException::new)
                        .getBook();

        System.out.println(">>>>" + result);

        BookReviewInfo result2 = bookRepository
                .findById(1L)
                .orElseThrow(RuntimeException::new)
                .getBookReviewInfo();

        System.out.println(">>>>" + result2);
    }

 

어떻게 할가?

 

일단 직관적으로 Book에서 BookReviewInfo에 대해 @OneToOne지정을 하면 될 것 같다.

@Entity
@NoArgsConstructor
@Data
@EntityListeners(value = AuditingEntityListener.class)
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class Book extends BaseEntity {

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

    private String name;

    private Long authorId;

    private Long publisherId;

    private String category;

    @OneToOne
    private BookReviewInfo bookReviewInfo;

    
}

실행해보자.

 

book Table에 book_review_info_id bigint가 생성되었고

book_review_info table에 book_id가 있는 것을 볼 수 있다.

Hibernate: 
    
    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,
        book_review_info_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)
    )

select쿼리를 보면

 

from
        book book0_ 
    left outer join
        book_review_info bookreview1_ 
            on book0_.book_review_info_id=bookreview1_.id 
    left outer join
        book book2_ 
            on bookreview1_.book_id=book2_.id 

book 에서 left outer join을 해서 book_review_info를 가져오고 다시 left outer join을 해서 

book에 book review info를 또 join을 하고 있다.??????뭔말이지

 

이렇게 left outer join이 여러번 이루어지고 있다.

이때 사용할 수 있는 것이 mappedBy() option이다.

 

mappedBy() : 연관 관계의 주인을 설정한다. 왜래키가 있는 곳을 주인으로 보면 된다.

2021.06.20 - [JPA] - mappedBy()

 

mappedBy()

단방향 객체 관계 2개를 사용하여 양방향 관계 1개를 만들 경우 필요하다. * 객체 연관관계 BookReviewInfo --> Book : 단방향, bookReviewInfo.get(book) Book --> BookReviewInfo: 단방향, book.get(bookReview..

bsh-developer.tistory.com

    /** (Optional) The field that owns the relationship. This 
      * element is only specified on the inverse (non-owning) 
      * side of the association.
     */
    String mappedBy() default "";

 

mappedBy()을 적용하면 연관키를 해당 테이블에서 더이상 가지지 않음.

 

BookReviewInfo class에서 Book에다가 적용을 해보자.

public class BookReviewInfo extends BaseEntity{

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

    //private long bookId;
    @OneToOne(optional = false, mappedBy = "bookReviewInfo") //1:1 연관관계 맵핑
    private Book book;

 

book table에는 book_review_info_id가 있지만

book_review_info table에는 book_id가 없다!!!!

   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,
        book_review_info_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,
        primary key (id)
    )

 

반대로 Book table에서 BookReviewInfo에 적용을 해보자.

@OneToOne(mappedBy = "book")
    private BookReviewInfo bookReviewInfo;

 

짠! stack over flow error가 발생했다.

엔티티 릴레이션을 사용하는 경우 특히 ToString 이 순환참조가 걸리게 된다.

따라서 단방향으로 설정하거나, ToString에서 제외시켜야 한다.

    @OneToOne(mappedBy = "book")
    @ToString.Exclude
    private BookReviewInfo bookReviewInfo;

- 결과

 

book에 book review info id가 없어지고, book review info에 book id가 존재하는 것을 알 수 있당.

Hibernate: 
    
    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)
    )

 

book에서 book review info를 불러오기 성공

>>>>BookReviewInfo(
   super=BaseEntity(createdAt=2021-06-17T23:39:31.617396, 
                   updatedAt=2021-06-17T23:39:31.617396), id=1, 
   book=Book(super=BaseEntity(createdAt=2021-06-17T23:39:31.557572, 
              updatedAt=2021-06-17T23:39:31.557572), 
              id=1, name=JPA haha, authorId=1, publisherId=1, category=null),
              averageReviewScore=4.5, reviewCount=2)

즉 relation을 양쪽으로 걸어서 양방향으로 불러오기가 가능해졌다.

'JPA' 카테고리의 다른 글

N : 1 연관관계 - @ManyToOne  (0) 2021.06.21
1 : N 연관관계 - 1  (0) 2021.06.21
mappedBy()  (0) 2021.06.20
1:1 연관관계 - 1  (0) 2021.06.17
Entity Listener : 4(실제 사용하는 방법)  (0) 2021.06.16
Comments