JPA는 편리하지만 원치 않는 쿼리가 작동할 수 있고, 이는 잠재적 성능 이슈를 야기한다.
@Data
@NoArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@Entity
public class BookReviewInfo extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// BookReviewInfo: Book
// 1:1 연결
// private Long bookId; // FK 역할.
@OneToOne(optional = false) // Book 과 1:1 로 relation
// optional = false <= Book(부모) 는 null 허용하지 않는다 (즉, 반드시 Book 을 참조)
private Book book; // 부모 Entity 참조
// 기본적으로 Entity 에선 직접적으로 다른 Entity 를 직접 참조 못한다
// @OneToOne 과 같은 relation 어노테이션을 지정해주어야 한다.
// NULL 을 허용하면 wrapper 객체 사용
// NULL 을 허용하지 않을거면 primitive 객체 사용 -> DDL 에 NOT NULL 부여됨
// 이번예제에서 아래 두개값은 기본값 0 을 사용하기 위해 primitive 를 사용합니다.
// --> 굳이 null check 안해도 된다.
private float averageReviewScore;
private int reviewCount;
}
@OneToOne(mappedBy = "book") // 해당 Entity 의 테이블에선 연관키를 가지지 않는다.
@ToString.Exclude // Lombok 의 ToString 에서 배제 (양방향에서의 순환참조 때문에)
private BookReviewInfo bookReviewInfo;
@Test
void crudTest3() {
givenBookReviewInfo();
// Book(부모)에서 BookReviewInfo(자식) 조회
System.out.println("🎅".repeat(20));
// @ OneToOne 양방향 참조
BookReviewInfo result2 = bookRepository
.findById(1L) // @OneToOne 연결된 entity 와의 join 문 발생
.orElseThrow(RuntimeException::new)
.getBookReviewInfo()
;
System.out.println(">>> " + result2);
}
Test 결과:
Hibernate:
create table book_review_info (
average_review_score float(24) not null,
review_count integer not null,
book_id bigint not null unique,
created_at timestamp(6),
id bigint generated by default as identity,
updated_at timestamp(6),
primary key (id)
)
Hibernate:
create table t_user (
created_at timestamp(6),
id bigint generated by default as identity,
updated_at timestamp(6),
email varchar(255) unique,
name varchar(255),
gender enum ('FEMALE','MALE'),
primary key (id)
)
Hibernate:
create table user_history (
created_at timestamp(6),
id bigint generated by default as identity,
updated_at timestamp(6),
user_id bigint,
email varchar(255),
name varchar(255),
primary key (id)
)
Hibernate:
create index IDXg8gqk4e142wekcb1t6d3v2mwx
on t_user (name)
Hibernate:
alter table if exists book_review_info
add constraint FKp5fhkokpbtoxmc3mxo8ay9e5l
foreign key (book_id)
references book
----------------------------------------
[ crudTest3() ] 호출
Hibernate:
select
bri1_0.id,
bri1_0.average_review_score,
bri1_0.book_id,
b1_0.id,
b1_0.author_id,
b1_0.category,
b1_0.created_at,
b1_0.name,
b1_0.publisher_id,
b1_0.updated_at,
bri1_0.created_at,
bri1_0.review_count,
bri1_0.updated_at
from
book_review_info bri1_0
join
book b1_0
on b1_0.id=bri1_0.book_id
where
bri1_0.id=?
>>> Book(super=BaseEntity(createdAt=2024-12-30T09:28:23.398898, updatedAt=2024-12-30T09:28:23.398898), id=1, name=JPA 완전정복, category=null, authorId=1, publisherId=1)
🎅🎅🎅🎅🎅🎅🎅🎅🎅🎅🎅🎅🎅🎅🎅🎅🎅🎅🎅🎅
Hibernate:
select
b1_0.id,
b1_0.author_id,
bri1_0.id,
bri1_0.average_review_score,
bri1_0.book_id,
bri1_0.created_at,
bri1_0.review_count,
bri1_0.updated_at,
b1_0.category,
b1_0.created_at,
b1_0.name,
b1_0.publisher_id,
b1_0.updated_at
from
book b1_0
left join
book_review_info bri1_0
on b1_0.id=bri1_0.book_id
where
b1_0.id=?
>>> BookReviewInfo(super=BaseEntity(createdAt=2024-12-30T09:28:23.422437, updatedAt=2024-12-30T09:28:23.422437), id=1, book=Book(super=BaseEntity(createdAt=2024-12-30T09:28:23.398898, updatedAt=2024-12-30T09:28:23.398898), id=1, name=JPA 완전정복, category=null, authorId=1, publisherId=1), averageReviewScore=4.5, reviewCount=2)
----------------------------------------
중간 테이블 생성 방식:
@OneToMany만 사용하면 자동으로 중간 테이블이 생성됩니다.
@OneToMany
private List<UserHistory> userHistories = new ArrayList<>(); // NPE 방지
생성되는 테이블 구조:
create table t_user_user_histories (
user_histories_id bigint not null unique,
user_id bigint not null
)
직접 외래키 방식:
@OneToMany
@JoinColumn(name = "user_id") // user_id 컬럼을 외래키로 사용
private List<UserHistory> userHistories = new ArrayList<>();
@OneToMany의 EAGER 사용시 주의사항:
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "user_id")
private List<UserHistory> userHistories = new ArrayList<>();
N+1 문제 발생 위험:
@Query("SELECT u FROM User u JOIN FETCH u.userHistories")
List<User> findAllWithHistories();
// User 엔티티
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "user_id", // Entity가 어떤 컬럼으로 join할지 지정
insertable = false, updatable = false // read only 설정
)
private List<UserHistory> userHistories = new ArrayList<>();
// UserHistory 엔티티
@Column(name="user_id", insertable = false, updatable = false)
private Long userId;
// Book 엔티티
class Book {
@OneToOne(mappedBy = "book")
private BookReviewInfo bookReviewInfo;
}
// BookReviewInfo 엔티티
class BookReviewInfo {
@OneToOne
private Book book;
}
순환참조 해결:
@ToString.Exclude // Lombok의 ToString에서 배제
private BookReviewInfo bookReviewInfo;
@OneToMany, @ManyToOne 어느 Entity에서 연관 Entity가 필요한가에 따라 둘 중 어느것을 쓸 지가 달라진다.
@OneToMany, @ManyToMany: LAZY
@ManyToOne, @OneToOne: EAGER
@Test
@Transactional
void bookRelationTest() {
// 테스트용 데이터 입력(Publisher, Book, Review)
givenBookAndReview();
// 특정 User
User user = userRepository.findByEmail("martin@redknight.com");
System.out.println("👇👇👇👇👇👇여기가 무시무시한 query 생성지 👇👇👇👇👇👇");
// 특정 User 가 남긴 Review 정보들 가져오기
System.out.println("Review: " + user.getReviews()); // getxxx() 시점에서 outer join들을 사용하여 읽어들임
System.out.println("😁".repeat(20));
// 특정 User 가 남긴 Review 중 첫번째 Review의 Book 정보 가져오기
System.out.println("Book: " + user.getReviews().get(0).getBook());
System.out.println("😇".repeat(20));
// 특정 User 가 남긴 Review 중 첫번째 Review의 Book의 Publisher 정보 가져오기
System.out.println("Publisher: " + user.getReviews().get(0).getBook().getPublisher());
}
👇👇👇👇👇👇여기가 무시무시한 query 생성지 👇👇👇👇👇👇
Hibernate:
select
r1_0.user_id,
r1_0.id,
b1_0.id,
b1_0.author_id,
bri1_0.id,
bri1_0.average_review_score,
bri1_0.book_id,
bri1_0.created_at,
bri1_0.review_count,
bri1_0.updated_at,
b1_0.category,
b1_0.created_at,
b1_0.name,
p1_0.id,
p1_0.created_at,
p1_0.name,
p1_0.updated_at,
b1_0.updated_at,
r1_0.content,
r1_0.created_at,
r1_0.score,
r1_0.title,
r1_0.updated_at
from
review r1_0
left join
book b1_0
on b1_0.id=r1_0.book_id
left join
book_review_info bri1_0
on b1_0.id=bri1_0.book_id
left join
publisher p1_0
on p1_0.id=b1_0.publisher_id
where
r1_0.user_id=?
Review: [Review(super=BaseEntity(createdAt=2024-12-30T12:18:33.006304, updatedAt=2024-12-30T12:18:33.006304), id=1, title=내 인생을 바꾼 책, content=너무너무 재미있고 즐거운 책이었어요, score=5.0, user=User(super=BaseEntity(createdAt=2024-12-30T12:18:32.669926, updatedAt=2024-12-30T12:18:32.669926), id=1, name=martin, email=martin@redknight.com, gender=null), book=Book(super=BaseEntity(createdAt=2024-12-30T12:18:33.003352, updatedAt=2024-12-30T12:18:33.003352), id=1, name=JPA 완전정복, category=null, authorId=null))]
😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁
Book: Book(super=BaseEntity(createdAt=2024-12-30T12:18:33.003352, updatedAt=2024-12-30T12:18:33.003352), id=1, name=JPA 완전정복, category=null, authorId=null)
😇😇😇😇😇😇😇😇😇😇😇😇😇😇😇😇😇😇😇😇
Publisher: Publisher(super=BaseEntity(createdAt=2024-12-30T12:18:32.990396, updatedAt=2024-12-30T12:18:32.990396), id=1, name=K-출판사, books=[])
JPA의 장점:
getter만 썼을 뿐인데, 알아서 query가 실행된다는 점
보통 잘 안 쓰고, 1:N, M:1로 별도의 테이블을 빼서 만든다.