1. M:N ๊ด๊ณ์ ํน์ง
์ํ์ ํ์
๋งคํ ํ ์ด๋ธ
JPA์์ M:N ์ฒ๋ฆฌ
2. ํ๋ก์ ํธ ์์ฑ
์ํฐํฐ ํด๋์ค ์ค๊ณ
3. M:N Repository์ ํ ์คํธ
Repository ์์ฑ
ํ์ด์ง ์ฒ๋ฆฌ๋๋ ์ํ๋ณ ํ๊ท ๋ณ์ / ๋ฆฌ๋ทฐ ๊ฐ์ ๊ตฌํ๊ธฐ
ํน์ ์ํ์ ๋ชจ๋ ์ด๋ฏธ์ง์ ํ๊ท ๋ณ์ /๋ฆฌ๋ทฐ ๊ฐ์
ํน์ ์ํ์ ๋ชจ๋ ๋ฆฌ๋ทฐ์ ํ์์ ๋๋ค์
ํ์ ์ญ์ ๋ฌธ์ ์ ํธ๋์ญ์ ์ฒ๋ฆฌ
1. M:N ๊ด๊ณ์ ํน์ง
๋ ผ๋ฆฌ์ ์ธ ์ค๊ณ์ ์ค์ ํ ์ด๋ธ์ ์ค๊ณ๊ฐ ๋ค๋ฅด๊ฒ ๋๋ค๋ ํน์ง์ด ์์
์ํ์ ํ์
ํ์: ์ฌ๋ฌ ํธ์ ์ํ๋ฅผ ํ๊ฐ
์ํ: ํ ํธ์ ์ํ์ ์ฌ๋ฌ ํ์์ด ์กด์ฌ
์ํ๋ฒํธ | ์ํ ์ด๋ฆ | ํ์ |
1 | ํ์ง | a, b |
2 | ์ธํฐ์คํ ๋ผ | b, c, d |
3 | ์๋ํ ๊ฐ์ธ ๋น | a, c |
์์ ๊ฐ์ด ํด๋น ์ํ์ด ์ฌ๋ฌ ์นดํ ๊ณ ๋ฆฌ์ ์ํ๋ ๊ฒ์ ๊ณ ์ ๋ ์์ ์นผ๋ผ์ผ๋ก ํํํ๊ณ ์์ง๋ง ๊ทผ๋ณธ์ ์ธ ํด๊ฒฐ์ฑ ์ด ์๋
๋งคํ ํ ์ด๋ธ
๋ ํ ์ด๋ธ์ ์ค๊ฐ์์ ํ์ํ ์ ๋ณด๋ฅผ ์์ชฝ์์ ๋์ด ์ฐ๋ ๊ตฌ์กฐ
ex) ํ์์ด ์ํ์ ๋ํด์ ํ์ ์ ์ค๋ค - ์ฃผ์ด์ ๋ชฉ์ ์ด๋ฅผ ์ฐ๊ฒฐํ๋ ํ์ ์ ์ฃผ๋ ํ์๋ฅผ ๋งคํ ํ ์ด๋ธ์ด ๋ด๋น
ํ ์ด๋ธ์ Row๋ฅผ ๋๋ฆด ์ ์์ผ๋ Column์ ๋๋ฆด ์ ์์ -> ์ํ์ ํ์ฅ ๋ถ๊ฐ -> ๋งคํ ํ ์ด๋ธ์ ์ฌ์ฉํด ์์ง์ ํ์ฅ
JPA์์ M:N ์ฒ๋ฆฌ
- @ManyToMany ์ด๋ ธํ ์ด์
๋งคํ ํ ์ด๋ธ์ ์ ๋ณด๋ฅผ ์ ์ฅํ ์ ์๋ ๊ตฌ์กฐ (ex. ๋ฆฌ๋ทฐ ๊ฐ์ฒด์ ํ์ ์ ๋ณด๋ฅผ ์ ์ฅํ ๊ณต๊ฐ์ด ์์)
์ฃผ๋ก ์๋ฐฉํฅ ์ฐธ์กฐ๋ฅผ ์ด์ฉํ๋๋ฐ, ํ๋์ ๊ฐ์ฒด๋ฅผ ์์ ํ ๊ฒฝ์ฐ ๋ค๋ฅธ ๊ฐ์ฒด์ ์ํ๋ฅผ ๋งค๋ฒ ์ผ์น์ํค๋๋ก ๋ณ๊ฒฝํ๋ ์์ ํ์
- ๋ณ๋์ ์ํฐํฐ ์ค๊ณ ํ @ManyToOne ์ด์ฉ
์ค๊ฐ์ ์ง์ ๋งคํ ํ ์ด๋ธ ์ค๊ณํ์ฌ ์ง์ ๋งคํ ๊ด๊ณ๋ฅผ ์ฐ๊ฒฐ์ํด
์ฐ๊ด ๊ด๊ณ๋ฅผ ์ด์ฉํด ์กฐํํด์ผ ํ๋ ๋ฐ์ดํฐ๋ JPQL์ 'left (outer) join' ํ์ฉ
2. ํ๋ก์ ํธ ์์ฑ
์ํฐํฐ ํด๋์ค ์ค๊ณ
๐ M:N ๊ด๊ณ ์ฒ๋ฆฌ ์
1. '๋ช ์ฌ'์ ํด๋นํ๋ ํด๋์ค ์ค๊ณ
2. ๋งคํ ํ ์ด๋ธ ์ค๊ณ
Movie
package org.zerock.mreview.entity;
import lombok.*;
import javax.persistence.*;
import java.util.*;
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
public class Movie extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long mno;
private String title;
}
Member
package org.zerock.mreview.entity;
import lombok.*;
import javax.persistence.*;
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
@Table(name = "m_member")
public class Member extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long mid;
private String email;
private String pw;
private String nickname;
}
Review
package org.zerock.mreview.entity;
import lombok.*;
import javax.persistence.*;
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString(exclude = {"movie","member"}) //toString์ ๋ค๋ฅธ ์ํฐํฐ ์ ์ธ
public class Review extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long reviewnum;
@ManyToOne(fetch = FetchType.LAZY)
private Movie movie;
@ManyToOne(fetch = FetchType.LAZY)
private Member member;
private int grade;
private String text;
}
Movie, Member๋ฅผ ์์ชฝ์ผ๋ก ์ฐธ์กฐ
3. M:N Repository์ ํ ์คํธ
Repository ์์ฑ
MovieRepository
package org.zerock.mreview.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.zerock.mreview.entity.Movie;
public interface MovieRepository extends JpaRepository<Movie, Long> {
}
MemberRepository
package org.zerock.mreview.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.zerock.mreview.entity.Member;
public interface MemberRepository extends JpaRepository<Member, Long> {
}
ReviewRepository
package org.zerock.mreview.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.zerock.mreview.entity.Review;
public interface ReviewRepository extends JpaRepository<Review, Long> {
}
ํ์ด์ง ์ฒ๋ฆฌ๋๋ ์ํ๋ณ ํ๊ท ๋ณ์ / ๋ฆฌ๋ทฐ ๊ฐ์ ๊ตฌํ๊ธฐ
MovieRepository
package org.zerock.mreview.repository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.zerock.mreview.entity.Movie;
import java.util.List;
public interface MovieRepository extends JpaRepository<Movie, Long> {
// @Query("select m, avg(coalesce(r.grade,0)), count(r) from Movie m " +
// "left outer join Review r on r.movie = m group by m")
// Page<Object[]> getListPage(Pageable pageable);
@Query("select m, mi, avg(coalesce(r.grade,0)), count(r) from Movie m " +
"left outer join MovieImage mi on mi.movie = m " +
"left outer join Review r on r.movie = m group by m ")
Page<Object[]> getListPage(Pageable pageable);
}
N+1 ๋ฌธ์
- 1๋ฒ์ ์ฟผ๋ฆฌ๋ก N๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์๋๋ฐ, N๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ํด ํ์ํ ์ถ๊ฐ์ ์ธ ์ฟผ๋ฆฌ๊ฐ ๊ฐ N๊ฐ์ ๋ํด ์ํ๋๋ ์ํฉ
- ์ฑ๋ฅ์ ๋ฌธ์ ๊ฐ ์๊ธธ ์ ์์ผ๋ฏ๋ก ๋ฐ๋์ ํด๊ฒฐํด์ผ ํจ
- MovieRepository์ "left outer join MovieImage mi on mi.movie = m " + ๋ถ๋ถ์์ ํ๋์ ์ด๋ฏธ์ง๋ง ๊ฐ์ ธ์ค๊ฒ ํจ(select m, mi, ... -> ๊ฐ์ฅ ๋จผ์ ๋ฑ๋ก๋(id๊ฐ ์์) ์ด๋ฏธ์ง๋ฅผ ๊ฐ์ ธ์ด)
ํน์ ์ํ์ ๋ชจ๋ ์ด๋ฏธ์ง์ ํ๊ท ๋ณ์ /๋ฆฌ๋ทฐ ๊ฐ์
MovieRepository
package org.zerock.mreview.repository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.zerock.mreview.entity.Movie;
import java.util.List;
public interface MovieRepository extends JpaRepository<Movie, Long> {
@Query("select m, mi ,avg(coalesce(r.grade,0)), count(r)" +
" from Movie m left outer join MovieImage mi on mi.movie = m " + //์ด๋ฏธ์ง ์ ๋ถ ๊ฐ์ ธ์ค๊ธฐ
" left outer join Review r on r.movie = m "+ //๋ฆฌ๋ทฐ ์กฐ์ธํด์ avg, count ํจ์ ์ฌ์ฉ
" where m.mno = :mno group by mi") //์ํ ์ด๋ฏธ์ง๋ณ๋ก group by
List<Object[]> getMovieWithAll(Long mno);
}
ํน์ ์ํ์ ๋ชจ๋ ๋ฆฌ๋ทฐ์ ํ์์ ๋๋ค์
ReviewRepository
package org.zerock.mreview.repository;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.zerock.mreview.entity.Member;
import org.zerock.mreview.entity.Movie;
import org.zerock.mreview.entity.Review;
import java.util.List;
public interface ReviewRepository extends JpaRepository<Review, Long> {
@EntityGraph(attributePaths = {"member"}, type = EntityGraph.EntityGraphType.FETCH)
List<Review> findByMovie(Movie movie);
}
Review ํด๋์ค์ Member์ ๋ํ Fetch ๋ฐฉ์์ด LAZY์์
-> ํ ๋ฒ์ Review ๊ฐ์ฒด์ Member ๊ฐ์ฒด๋ฅผ ์กฐํํ ์ ์๋ค๋ ๋ฌธ์ ๋ฐ์
ํด๊ฒฐ๋ฒ 1) @Query๋ฅผ ์ด์ฉํด ์กฐ์ธ ์ฒ๋ฆฌ
ํด๊ฒฐ๋ฒ 2) @EntityGraph ์ด์ฉํด Review ๊ฐ์ฒด ๊ฐ์ ธ์ฌ ๋ Member๊ฐ์ฒด ๋ก๋ฉ
@EntityGraph: ์ํฐํฐ์ ํน์ ์์ฑ์ ๊ฐ์ด ๋ก๋ฉํ๋๋ก ํ์ํ๋ ์ด๋ ธํ ์ด์ , ๋ณดํต JPA์์๋ FETCH ์์ฑ๊ฐ์ LAZY๋ก ์ง์ ํ๋๋ฐ, ํน์ ๊ธฐ๋ฅ์ ์ํํ ๋๋ง EAGER๋ก๋ฉ์ ํ๋๋ก ์ง์ ํ ์ ์์
@EntityGraph(attributePaths = {"member"}, type = EntityGraph.EntityGraphType.FETCH)
- attributePaths: ๋ก๋ฉ ์ค์ ์ ๋ณ๊ฒฝํ๊ณ ์ถ์ ์์ฑ์ ์ด๋ฆ์ ๋ฐฐ์ด๋ก ๋ช ์
- type: @EntityGraph๋ฅผ ์ด๋ค ๋ฐฉ์์ผ๋ก ์ ์ฉํ ๊ฒ์ธ์ง ์ค์
ํ์ ์ญ์ ๋ฌธ์ ์ ํธ๋์ญ์ ์ฒ๋ฆฌ
package org.zerock.mreview.repository;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.zerock.mreview.entity.Member;
import org.zerock.mreview.entity.Movie;
import org.zerock.mreview.entity.Review;
import java.util.List;
public interface ReviewRepository extends JpaRepository<Review, Long> {
@EntityGraph(attributePaths = {"member"}, type = EntityGraph.EntityGraphType.FETCH)
List<Review> findByMovie(Movie movie);
@Modifying
@Query("delete from Review mr where mr.member = :member")
void deleteByMember(Member member);
}
M:N ๊ด๊ณ์์ ๋ช ์ฌ์ ํด๋นํ๋ ๋ฐ์ดํฐ๋ฅผ ์ญ์ ํ๋ ๊ฒฝ์ฐ ๋งคํ ํ ์ด๋ธ์์๋ ๋ฐ์ดํฐ ์ญ์ ํ์
๐ ๋จ, ๋ฐ์ดํฐ ์ญ์ ์ ๋งคํ ํ ์ด๋ธ์ ๋ฐ์ดํฐ ๋จผ์ ์ง์ด ํ ๋ช ์ฌ์ ํด๋นํ๋ ๋ฐ์ดํฐ๋ฅผ ์ง์์ผ ํจ!
๐ @Transactional, @Commit ์ด๋ ธํ ์ด์ ์ถ๊ฐ ํ์!
@Modifying
update๋ delete๋ฅผ ์ด์ฉํ๊ธฐ ์ํด ๋ฐ๋์ ํ์ํ ์ด๋ ธํ ์ด์
@Query("delete from Review mr where mr.member = :member")
Query๋ฅผ ์ด์ฉํด where์ ์ ์ง์ ํ์ฌ ํจ์จ์ ์ผ๋ก review ๋ฐ์ดํฐ(๋งคํ ํ ์ด๋ธ ๋ถ๋ถ)๋ฅผ ์ญ์ ํ๋๋ก ํจ
๋๊ธ