[ JPA ] 5-5. Entity Relations ( N:N @ManyToMany )
백엔드/JPA

[ JPA ] 5-5. Entity Relations ( N:N @ManyToMany )

Reference. 한 번에 끝내는 Java/Spring 웹 개발 마스터 초격차 패키지 Online

이전 글

N:N

  • 실무에선 거의 사용되지 않음
  • @ManyToMany(N:N)One(N:1 or 1:N)이 존재하지 않으면 FK로 쓰일 PK를 구하기가 어려움
  • @ManyToMany 를 설정하면 중간 테이블이 생성 됌
    → ex) author - bookauthor_books

1. @ManyToMany 적용 (author - book)

<ERD>

Author.java 생성
@Entity
@NoArgsConstructor
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
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); //콜렉션 add 배열
    }
}
Book.java 수정
...
public class Book extends BaseEntity{
		...

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

    public void addAuthor(Author... author) {
        Collections.addAll(this.authors, author); //콜렉션 add 배열
    }
}
테스트 코드
@SpringBootTest
class AuthorRepositoryTest {
    @Autowired
    private AuthorRepository authorRepository;
    @Autowired
    private BookRepository bookRepository;

    @Test
    @Transactional
    void manyToManyTest(){
        //Book 객체 생성 및 저장
        Book book1 = givenBook("책1");
        Book book2 = givenBook("책2");
        Book book3 = givenBook("개발책1");
        Book book4 = givenBook("개발책2");

        //Author 객체 생성 및 저장
        Author author1 = givenAuthor("martin");
        Author author2 = givenAuthor("steve");

        //Book 객체에 List<Author>에 add
        book1.addAuthor(author1);
        book2.addAuthor(author2);
        book3.addAuthor(author1, author2);
        book4.addAuthor(author1, author2);

        //Author 객체에 List<Book>에 add
        author1.addBook(book1, book3, book4);
        author2.addBook(book2, book3, book4);

        //해당 객체를 데이터베이스에 저장 -> flush()
        bookRepository.saveAll(Arrays.asList(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());
    }

    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);
    }
}
실행결과 (DDL, 결과)
 Hibernate: 
    
    create table author_books (
       author_id bigint not null,
        books_id bigint not null
    )

 //결과 콘솔
 authors through book : [Author(super=BaseEntity(createdAt=2021-07-29T20:26:42.025524100, updatedAt=2021-07-29T20:26:42.025524100), id=1, name=martin, country=null), Author(super=BaseEntity(createdAt=2021-07-29T20:26:42.036527500, updatedAt=2021-07-29T20:26:42.036527500), id=2, name=steve, country=null)]
 books through author : [Book(super=BaseEntity(createdAt=2021-07-29T20:26:41.846525300, updatedAt=2021-07-29T20:26:41.846525300), id=1, name=책1, category=null), Book(super=BaseEntity(createdAt=2021-07-29T20:26:41.999525400, updatedAt=2021-07-29T20:26:41.999525400), id=3, name=개발책1, category=null), Book(super=BaseEntity(createdAt=2021-07-29T20:26:42.006524300, updatedAt=2021-07-29T20:26:42.006524300), id=4, name=개발책2, category=null)]

2. @ManyToMany
@OneToMany, @ManyToOne으로 변경 (book -bookAndAuthor - author)

<ERD>

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

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

    public void addBookAndAuthors(BookAndAuthor... bookAndAuthors) {
        Collections.addAll(this.bookAndAuthors, bookAndAuthors);
    }
}
BookAndAuthor.java
@Entity
@NoArgsConstructor
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class BookAndAuthor extends BaseEntity{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    private Book book;

    @ManyToOne
    private Author author;
}
Author.java
@Entity
@NoArgsConstructor
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class Author extends BaseEntity{
		...
	
    @OneToMany
    @JoinColumn(name="author_id")
    @ToString.Exclude
    private List<BookAndAuthor> bookAndAuthors = new ArrayList<>();

    public void addBookAndAuthors(BookAndAuthor... bookAndAuthors) {
        Collections.addAll(this.bookAndAuthors, bookAndAuthors);
    }
}
BookAndAuthorRepository.java
@Repository
public interface BookAndAuthorRepository extends JpaRepository<BookAndAuthor, Long> {
}
테스트 코드
@SpringBootTest
class AuthorRepositoryTest {
    @Autowired
    private AuthorRepository authorRepository;
    @Autowired
    private BookRepository bookRepository;
    @Autowired
    private BookAndAuthorRepository bookAndAuthorRepository;

    @Test
    @Transactional
    void manyToManyTest(){
        //Book 객체 생성 및 저장
        Book book1 = givenBook("책1");
        Book book2 = givenBook("책2");
        Book book3 = givenBook("개발책1");
        Book book4 = givenBook("개발책2");

        //Author 객체 생성 및 저장
        Author author1 = givenAuthor("martin");
        Author author2 = givenAuthor("steve");

        //BookAndAuthor 객체 생성 및 저장
        BookAndAuthor bookAndAuthor1 = givenBookAndAuthor(book1, author1);
        BookAndAuthor bookAndAuthor2 = givenBookAndAuthor(book2, author2);
        BookAndAuthor bookAndAuthor3 = givenBookAndAuthor(book3, author1);
        BookAndAuthor bookAndAuthor4 = givenBookAndAuthor(book3, author2);
        BookAndAuthor bookAndAuthor5 = givenBookAndAuthor(book4, author1);
        BookAndAuthor bookAndAuthor6 = givenBookAndAuthor(book4, author2);

        //Book 객체에 List<BookAndAuthor>에 add
        book1.addBookAndAuthors(bookAndAuthor1);
        book2.addBookAndAuthors(bookAndAuthor2);
        book3.addBookAndAuthors(bookAndAuthor3, bookAndAuthor4);
        book4.addBookAndAuthors(bookAndAuthor5, bookAndAuthor6);

        //Author 객체에 List<BookAndAuthor>에 add
        author1.addBookAndAuthors(bookAndAuthor1, bookAndAuthor3, bookAndAuthor5);
        author2.addBookAndAuthors(bookAndAuthor2, bookAndAuthor4, bookAndAuthor6);

        //해당 객체를 데이터베이스에 저장 -> flush()
        bookRepository.saveAll(Arrays.asList(book1, book2, book3, book4));
        authorRepository.saveAll(Lists.newArrayList(author1, author2));

        bookRepository.findAll().get(2).getBookAndAuthors().forEach(o -> System.out.println(o.getAuthor()));
        authorRepository.findAll().get(0).getBookAndAuthors().forEach(o -> System.out.println(o.getBook()));
    }

    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);
    }

    private BookAndAuthor givenBookAndAuthor(Book book, Author author){
        BookAndAuthor bookAndAuthor = new BookAndAuthor();
        bookAndAuthor.setBook(book);
        bookAndAuthor.setAuthor(author);

        return bookAndAuthorRepository.save(bookAndAuthor);
    }
}
실행결과 (DDL, 결과)
 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 book (
       id bigint generated by default as identity,
        created_at timestamp,
        updated_at timestamp,
        category varchar(255),
        name varchar(255),
        publisher_id bigint,
        primary key (id)
    )
 Hibernate: 
    
    create table book_and_author (
       id bigint generated by default as identity,
        created_at timestamp,
        updated_at timestamp,
        author_id bigint,
        book_id bigint,
        primary key (id)
    )

 //실행 결과
 Author(super=BaseEntity(createdAt=2021-08-01T15:16:30.329579800, updatedAt=2021-08-01T15:16:30.329579800), id=1, name=martin, country=null)
 Author(super=BaseEntity(createdAt=2021-08-01T15:16:30.343577600, updatedAt=2021-08-01T15:16:30.343577600), id=2, name=steve, country=null)

 Book(super=BaseEntity(createdAt=2021-08-01T15:16:30.186577400, updatedAt=2021-08-01T15:16:30.186577400), id=1, name=책1, category=null)
 Book(super=BaseEntity(createdAt=2021-08-01T15:16:30.312579, updatedAt=2021-08-01T15:16:30.312579), id=3, name=개발책1, category=null)
 Book(super=BaseEntity(createdAt=2021-08-01T15:16:30.315578900, updatedAt=2021-08-01T15:16:30.315578900), id=4, name=개발책2, category=null)