6 minute read

M:N 연관관계-중간 테이블을 엔티티로 관리하지 않는 경우

https://raw.githubusercontent.com/hy6219/TIL/main/Spring/JPA/Entity/RelationShip/OneToOne.png

실무에서는 거의 사용되지 않는 관계

위의 경우는 Book:Author=M:N 관계

01. 먼저 지난시간에 만들었던 Author 엔티티를 점검해보자 @ManyToMany

🌟 ManyToMany는 컬렉션으로 참조할 필드를 두어야 한다!

package com.example.jpa_entity.domain;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Data
@NoArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@Entity
public class Author extends BaseEntity{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String country;

    @ManyToMany
    private List<Book> books=new ArrayList<>();
}

그리고 이와 연결되는 Repository인 AuthorRepository를 만들자(지난시간에 만들었음)

package com.example.jpa_entity.repository;

import com.example.jpa_entity.domain.Author;
import org.springframework.data.jpa.repository.JpaRepository;

public interface AuthorRepository extends JpaRepository<Author,Long> {
}

02. Book 측에도 Author와 연결되도록 필드를 추가- @ManyToMany

Book측에도 Author와 연결되는 필드를 추가해주자

package com.example.jpa_entity.domain;

import com.example.jpa_entity.domain.listener.Auditable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Data
@NoArgsConstructor
@Entity
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper=true)
///@EntityListeners(value=MyEntityListener.class)
//@EntityListeners(value= AuditingEntityListener.class)
public class Book extends BaseEntity/* implements Auditable*/ {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    //책 이름
    private String name;

    private String category;

    //책 저자
    private Long authorId;

//    private Long publisherId;

    @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;

    **@ManyToMany
    private List<Author> authors=new ArrayList<>(;**
}

03. DDL 살펴보기

우선, 여기까지 해서 지난 시간에 진행했던 테스트를 이용해서 DDL이 어떻게 진행되는 지 살펴보자

		@Test
    @Transactional
    public void bookRelationTest(){
        //Book정보와 Review 정보를 저장
        givenBookAndReview();
        User user=userRepository.findByEmail("martin@fastcampus.com");
        System.out.println("User : (userRepository)-"+user);
        System.out.println("Review<-User: "+user.getReviews());
        System.out.println("Book<-Review<-User: "+user.getReviews().get(0).getBook());
        System.out.println("Publisher<-Book<-Review<-User: "+user.getReviews().get(0).getBook().getPublisher());
    }
Hibernate: 
    
    create table author (
       id bigint generated by default as identity,
        created_at timestamp,
        updated_at timestamp,
        country varchar(255),
        name varchar(255),
        primary key (id)
    )
Hibernate: 
    
    create table author_books (
       author_id bigint not null,
        books_id bigint not null
    )
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_authors (
       book_id bigint not null,
        authors_id bigint not null
    )

다른 테이블은 뒤로 하고, 먼저 Author와 Book과 관련된 테이블들만 살펴보았다

그러면 Book 테이블에는 “author_id”가 생긴 것을 알 수 있고, Book_Authors라는 중간 테이블이 생긴 것을 확인해볼 수 있다

@OneToMany 에서는 Many 측에 FK키로써 참조되는 테이블 측의 PK값이 저장되지만

@ManyToMany에서는 FK로 구할 PK를 얻기가 어렵다

그래서 중간 테이블에서 매핑하게 되는 것을 피하기 어렵다

간단하게 테스트를 통해서 ManyToMany가 어떻게 동작하는지 살펴보도록 하자

package com.example.jpa_entity.repository;

import com.example.jpa_entity.domain.Author;
import com.example.jpa_entity.domain.Book;
import org.assertj.core.util.Lists;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.ArrayList;
import java.util.Arrays;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class AuthorRepositoryTest {

    @Autowired
    private AuthorRepository authorRepository;

    @Autowired
    private BookRepository bookRepository;

    private Book givenBook(String name){
        Book book=new Book();
        book.setName(name);

        return bookRepository.save(book);
    }

    private Author givenAuthor(String name){
        Author author=new Author();

        author.setName(name);
        return authorRepository.save(author);
    }

    @Test
    public void manyToManyTest(){
        Book book1=givenBook("책1");
        Book book2=givenBook("책2");
        Book book3=givenBook("개발책1");
        Book book4=givenBook("개발책2");

        Author author1=givenAuthor("martin");
        Author author2=givenAuthor("steve");

        //연관관계 넣어주기
        book1.setAuthors(Lists.newArrayList(author1));
        book2.setAuthors(Lists.newArrayList(author2));
        book3.setAuthors(Lists.newArrayList(author1,author2));
        book4.setAuthors(Lists.newArrayList(author1,author2));

        author1.setBooks(Lists.newArrayList(book1,book3,book4));
        author2.setBooks(Lists.newArrayList(book2,book3,book4));

        bookRepository.saveAll(Lists.newArrayList(book1,book2,book3,book4));
        authorRepository.saveAll(Lists.newArrayList(author1,author2));

        

    }
}

위의 코드를 실행해보면, LazyInitialization Exception이 발생하는 것을 확인해볼 수 있다

그리고 잠깐 보다 실질적으로 사용되는 형태를 잠깐 살펴보고 코드를 정리해보자


참고로, 지금은 테스트에서 진행할 때 given~이라는 메서드로 데이터를 추가해주고 있지만 현업에서는 아래처럼 엔티티 클래스 안에 메서드를 넣어서 관리한다고 한다

package com.example.jpa_entity.domain;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

@Data
@NoArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@Entity
public class Author extends BaseEntity{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String country;

    @ManyToMany
    private List<Book> books=new ArrayList<>();

    **public void addBook(Book...book){
        Collections.addAll(this.books,book);
    }**
}

package com.example.jpa_entity.domain;

import com.example.jpa_entity.domain.listener.Auditable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

@Data
@NoArgsConstructor
@Entity
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper=true)
///@EntityListeners(value=MyEntityListener.class)
//@EntityListeners(value= AuditingEntityListener.class)
public class Book extends BaseEntity/* implements Auditable*/ {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    //책 이름
    private String name;

    private String category;

    //책 저자
    private Long authorId;

//    private Long publisherId;

    @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;

    @ManyToMany
    private List<Author> authors=new ArrayList<>();

    **public void addAuthor(Author...author){
        Collections.addAll(this.authors,author);
    }**

}

이렇게 되면 위의 테스트코드는 아래처럼 정리될 수 있다

그리고 book으로 author정보들을 가져와보고, author로 book정보들을 가져와보자

package com.example.jpa_entity.repository;

import com.example.jpa_entity.domain.Author;
import com.example.jpa_entity.domain.Book;
import org.assertj.core.util.Lists;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.transaction.Transactional;
import java.util.ArrayList;
import java.util.Arrays;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class AuthorRepositoryTest {

    @Autowired
    private AuthorRepository authorRepository;

    @Autowired
    private BookRepository bookRepository;

    private Book givenBook(String name){
        Book book=new Book();
        book.setName(name);

        return bookRepository.save(book);
    }

    private Author givenAuthor(String name){
        Author author=new Author();

        author.setName(name);
        return authorRepository.save(author);
    }

    @Test
    public void manyToManyTest(){
        Book book1=givenBook("책1");
        Book book2=givenBook("책2");
        Book book3=givenBook("개발책1");
        Book book4=givenBook("개발책2");

        Author author1=givenAuthor("martin");
        Author author2=givenAuthor("steve");

        //연관관계 넣어주기
        **book1.addAuthor(author1);
        book2.addAuthor(author2);
        book3.addAuthor(author1,author2);
        book4.addAuthor(author1,author2);

        author1.addBook(book1,book3,book4);
        author2.addBook(book2,book3,book4);**

        bookRepository.saveAll(Lists.newArrayList(book1,book2,book3,book4));
        authorRepository.saveAll(Lists.newArrayList(author1,author2));

				System.out.println("authors through book: "+bookRepository.findAll().get(2).getAuthors());
        System.out.println("books through author: "+authorRepository.findAll().get(0).getBooks());

    }
}

이 테스트를 실행해보면 아직 해결하지 않았던 Lazy Initialization Exception이 뜬다

package com.example.jpa_entity.repository;

import com.example.jpa_entity.domain.Author;
import com.example.jpa_entity.domain.Book;
import org.assertj.core.util.Lists;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.transaction.Transactional;
import java.util.ArrayList;
import java.util.Arrays;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class AuthorRepositoryTest {

    @Autowired
    private AuthorRepository authorRepository;

    @Autowired
    private BookRepository bookRepository;

    private Book givenBook(String name){
        Book book=new Book();
        book.setName(name);

        return bookRepository.save(book);
    }

    private Author givenAuthor(String name){
        Author author=new Author();

        author.setName(name);
        return authorRepository.save(author);
    }

    @Test
    **@Transactional**
    public void manyToManyTest(){
        Book book1=givenBook("책1");
        Book book2=givenBook("책2");
        Book book3=givenBook("개발책1");
        Book book4=givenBook("개발책2");

        Author author1=givenAuthor("martin");
        Author author2=givenAuthor("steve");

        //연관관계 넣어주기
        book1.addAuthor(author1);
        book2.addAuthor(author2);
        book3.addAuthor(author1,author2);
        book4.addAuthor(author1,author2);

        author1.addBook(book1,book3,book4);
        author2.addBook(book2,book3,book4);

        bookRepository.saveAll(Lists.newArrayList(book1,book2,book3,book4));
        authorRepository.saveAll(Lists.newArrayList(author1,author2));

        System.out.println("authors through book: "+bookRepository.findAll().get(2).getAuthors());
        System.out.println("books through author: "+authorRepository.findAll().get(0).getBooks());

    }
}

그래서 테스트할 메서드 위에 @Transactional을 붙였더니 StackOverFlowError가 발생한다!

바로 toString으로 인한 순환참조가 있다는 것인 것 같다

04. 테스트 메서드 위에 @Transactional, 그리고 ToString.Exclude를 이용한 순환참조 막기

위에서 Transactional을 붙여주었으므로

이어서 ToString.Exclude를 각각의 엔티티에 붙여주자

package com.example.jpa_entity.domain;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

@Data
@NoArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@Entity
public class Author extends BaseEntity{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String country;

    @ManyToMany
    **@ToString.Exclude**
    private List<Book> books=new ArrayList<>();

    public void addBook(Book...book){
        Collections.addAll(this.books,book);
    }
}
package com.example.jpa_entity.domain;

import com.example.jpa_entity.domain.listener.Auditable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

@Data
@NoArgsConstructor
@Entity
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper=true)
///@EntityListeners(value=MyEntityListener.class)
//@EntityListeners(value= AuditingEntityListener.class)
public class Book extends BaseEntity/* implements Auditable*/ {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    //책 이름
    private String name;

    private String category;

    //책 저자
    private Long authorId;

//    private Long publisherId;

    @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;

    @ManyToMany
    **@ToString.Exclude**
    private List<Author> authors=new ArrayList<>();

    public void addAuthor(Author...author){
        Collections.addAll(this.authors,author);
    }

}
Hibernate: 
    insert 
    into
        book
        (id, created_at, updated_at, author_id, category, name, publisher_id) 
    values
        (null, ?, ?, ?, ?, ?, ?)
Hibernate: 
    insert 
    into
        book
        (id, created_at, updated_at, author_id, category, name, publisher_id) 
    values
        (null, ?, ?, ?, ?, ?, ?)
Hibernate: 
    insert 
    into
        book
        (id, created_at, updated_at, author_id, category, name, publisher_id) 
    values
        (null, ?, ?, ?, ?, ?, ?)
Hibernate: 
    insert 
    into
        book
        (id, created_at, updated_at, author_id, category, name, publisher_id) 
    values
        (null, ?, ?, ?, ?, ?, ?)
Hibernate: 
    insert 
    into
        author
        (id, created_at, updated_at, country, name) 
    values
        (null, ?, ?, ?, ?)
Hibernate: 
    insert 
    into
        author
        (id, created_at, updated_at, country, name) 
    values
        (null, ?, ?, ?, ?)
Hibernate: 
    select
        book0_.id as id1_3_,
        book0_.created_at as created_2_3_,
        book0_.updated_at as updated_3_3_,
        book0_.author_id as author_i4_3_,
        book0_.category as category5_3_,
        book0_.name as name6_3_,
        book0_.publisher_id as publishe7_3_ 
    from
        book book0_
authors through book: [Author(super=BaseEntity(cratedAt=2021-08-31T14:30:03.393, updatedAt=2021-08-31T14:30:03.393), id=1, name=martin, country=null), Author(super=BaseEntity(cratedAt=2021-08-31T14:30:03.401, updatedAt=2021-08-31T14:30:03.401), id=2, name=steve, country=null)]
Hibernate: 
    select
        author0_.id as id1_1_,
        author0_.created_at as created_2_1_,
        author0_.updated_at as updated_3_1_,
        author0_.country as country4_1_,
        author0_.name as name5_1_ 
    from
        author author0_
books through author: [Book(super=BaseEntity(cratedAt=2021-08-31T14:30:03.316, updatedAt=2021-08-31T14:30:03.316), id=1, name=책1, category=null, authorId=null), Book(super=BaseEntity(cratedAt=2021-08-31T14:30:03.386, updatedAt=2021-08-31T14:30:03.386), id=3, name=개발책1, category=null, authorId=null), Book(super=BaseEntity(cratedAt=2021-08-31T14:30:03.387, updatedAt=2021-08-31T14:30:03.387), id=4, name=개발책2, category=null, authorId=null)]

그러면 이번에는 제대로 잘 동작해서 book을 통해서 authors를, author를 통해 books를 살펴볼 수 있게 되었다

🌟 이를 통해서, 연관관계에서 getter를 통해 연결된 테이블 정보를 확인해볼 수 있음을 다시 한 번 살펴볼 수 있었다

Updated: