1_[spring jpa]m to n 연관관계 중간 테이블을 엔티티로 관리하지 않는 경우
M:N 연관관계-중간 테이블을 엔티티로 관리하지 않는 경우
실무에서는 거의 사용되지 않는 관계
위의 경우는 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를 통해 연결된 테이블 정보를 확인해볼 수 있음을 다시 한 번 살펴볼 수 있었다