๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
Spring Boot/์ฝ”๋“œ๋กœ ๋ฐฐ์šฐ๋Š” ์Šคํ”„๋ง๋ถ€ํŠธ ์›น ํ”„๋กœ์ ํŠธ

[์ฝ”๋“œ๋กœ ๋ฐฐ์šฐ๋Š” ์Šคํ”„๋ง ๋ถ€ํŠธ ์›น ํ”„๋กœ์ ํŠธ] 7. M:N ๊ด€๊ณ„์˜ ์„ค๊ณ„์™€ ๊ตฌํ˜„

by oliviarla 2022. 4. 6.

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 ๋ฐ์ดํ„ฐ(๋งคํ•‘ ํ…Œ์ด๋ธ” ๋ถ€๋ถ„)๋ฅผ ์‚ญ์ œํ•˜๋„๋ก ํ•จ

๋Œ“๊ธ€