JAVAIARY

M : N (다대다) 관계의 설계와 구현 본문

Project/2023.02~ ) Study toy 프로젝트

M : N (다대다) 관계의 설계와 구현

shiherlis 2023. 4. 3. 16:42

목표 : JPA를 이용해서 M:N(다대다) 관계의 구현 방법을 알아본다.

  • 예제
    영화(Movie), 회원(Member) 테이블이 존재하고, 회원이 영화에 대한 평점과 감상을 기록한다.
    - 한 편의 영화는 여러 회원의 평가가 행해질 수 있다.
    - 한 명의 회원은 여러 영화에 대해 평점을 줄 수 있다.

1. 다대다 관계의 특징

1) 논리적 설계와 실제 테이블 설계가 다름

  • 회원은 여러 편의 영화를 평가할 수 있음
  • 한 편의 영화는 여러 회원이 존재함

ex) 
- 학생 / 수업: 한 명의 학생은 여러 수업에 참여하고, 하나의 수업은 여러 학생이 수강한다.
- 상품 / 상품 카테고리: 하나의 상품은 여러 카테고리에 속하고, 하나의 카테고리는 여러 상품을 가지고 있다.
- 상품 / 회원 : 하나의 상품은 여러 회원이 구매할 수 있고, 한 명의 회원은 여러 상품을 구매할 수 있다.

  • 대부분의 관계형 데이터베이스는 컬럼을 지정하면서 최대 크기를 지정하기 때문에 수평적 확장이 불가능
  • But, 수직적 확장(Row)은 가능
  • ==> 매핑 (Mapping) 테이블을 사용하여 두 테이블의 중간에서 필요한 정보를 양쪽에서 받아옴

리뷰(Review)라는 매핑테이블의 관계

  • 매핑테이블의 특징
    • 매핑테이블의 작성 이전에 다른 테이블이 먼저 존재해야 함
    • 매핑 테이블은 주로 '명사'가 아닌 '동사'나 '히스토리'에 대한 데이터를 보관하는 용도
    • 중간에서 양쪽의 PK를 참조하는 형태로 사용됨

2) JPA에서 M:N(다대다) 처리

2가지 처리 방식

  • @ManyToMany를 이용해서 처리하는 방식
    • 각 엔티티와의 매핑 테이블이 자동으로 생성됨
    • 예제에서는 적용이 어려움(평점 정보는 리뷰 테이블에 있어야 하는데 @ManyToOne에서 만들어 줄 수 없기 때문)
    • 양방향 참조시 주의 필요(Context-현재 메모리상의 엔티티 객체의 상태-와 DB의 상태를 일치시키는 것의 어려움) 
  • 별도의 엔티티를 설계하고, @ManyToOne을 이용해서 처리하는 방식 

2. 예제 프로젝트 생성

1. 엔티티 클래스 설계

  • 다대다 관계의 처리시 반드시 '명사'에 해당하는 클래스 우선 설계
    매핑 테이블의 설계는 마지막 단계에서 처리

1) Movie / MovieImage 클래스 생성

@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@ToString
public class Movie extends BaseEntity{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long mno;

    private String title;
}
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@ToString(exclude = "movie")    // 연관 관계 주의
public class MovieImage {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long inum;

    private String uuid;

    private String imgName;

    private String path;

    @ManyToOne(fetch = FetchType.LAZY)
    private Movie movie;
}
  • MovieImage 클래스는 이전 예제의 댓글과 유사
  • 단방향 참조로 처리
  • 추후 사용할 이미지에 대한 정보 기록
    • java.util.UUID를 이용해서 고유한 번호를 생성해서 사용할 것
    • 이미지의 저장 경로(path)는 '연/월/일' 폴더 구조를 의미
  • Movie 테이블이 PK를 가지고, movieImage 테이블은 FK를 가지므로 @ManyToOne을 이용하여 표시

2) Member 클래스 변경하여 사용

@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 password;
    private String nickname;
    
}

💡💡💡 기존 BoardService 파일도 함께 수정

getName() ==> getNickname()

3) Review(매핑 테이블) 클래스 설계

@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString(exclude = {"movie", "member"})
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를 양쪽으로 참조하므로 @ManyToOne으로 설계
  • toString() 호출 시 다른 엔티티릘 사용하지 않도록 @ToString 에 exclude 속성 지정

3. 다대다 Repository와 테스트

1) MovieRepository 작성 & 테스트 데이터 추가하기

public interface MovieRepository extends JpaRepository<Movie, Long> {
    
}
public interface MovieImageRepository extends JpaRepository<MovieImage,Long> {
}
  • 테스트 추가

 

@SpringBootTest
public class MovieRepositoryTests {
    @Autowired
    private MovieRepository movieRepository;
    @Autowired
    private MovieImageRepository imageRepository;

    @Commit
    @Transactional
    @Test
    public void insertMovies(){
        IntStream.rangeClosed(1,100).forEach(i -> {
            Movie movie = Movie.builder()
                    .title("Movie..."+i)
                    .build();

            System.out.println("--------------------------");

            movieRepository.save(movie);
            int count = (int)(Math.random()*5)+1;   // 1, 2, 3, 4

            for (int j = 0; j <count; j++){
                MovieImage movieImage = MovieImage.builder()
                        .uuid(UUID.randomUUID().toString())
                        .movie(movie)
                        .imgName("test" + j + ".jpg")
                        .build();
                imageRepository.save(movieImage);
            }
            System.out.println("=======================================");
        });
    }
}

 

movie 테이블과 movieImage 테이블


2) MemberRepositoryTests에 테스트 추가

@Test
    public void insertMembers01(){
        IntStream.rangeClosed(1, 100).forEach(i ->{

            Member member = Member.builder()
                    .email("r" + i + "@naver.com")
                    .password("1111")
                    .nickname("reviewer" + i)
                    .build();

            memberRepository.save(member);
        });
    }

리뷰용 회원 추가


3) ReviewRepository 생성 및 테스트

public interface ReviewRepository extends JpaRepository<Review, Long> {
}

@SpringBootTest
public class ReviewRepositoryTests {
    @Autowired
    private ReviewRepository reviewRepository;
    @Test
    public void insertMovieReviews(){
        //200개 리뷰 등록
        IntStream.rangeClosed(1,200).forEach(i ->{
            //영화 번호
            Long mno = (long)(Math.random()*100)+1;
            // 리뷰어 번호
            Long mid = ((long)(Math.random()*100)+1);
            Member member = Member.builder().mid(mid).build();

            Review movieReview = Review.builder()
                    .member(member)
                    .movie(Movie.builder().mno(mno).build())
                    .grade((int)(Math.random()*5)+1)
                    .text("이 영화에 대한 느낌 ..." + i)
                    .build();

            reviewRepository.save(movieReview);
        });
    }
}

review 더미 생성


4. 필요한 데이터 처리

  • 목록 화면에서 영화의 제목과 이미지 하나, 영화 리뷰의 평점/리뷰 개수를 출력
  • 영화 조회 화면에서 영화와 영화의 이미지들, 리뷰의 평균점수/리뷰 개수를 같이 출력
  • 리뷰에 대한 정보에는 회원의 이메일이나 닉네임(nickname)과 같은 정보를 같이 출력

1) 영화별 평균 점수 / 리뷰 개수 출력하기

public interface MovieRepository extends JpaRepository<Movie, Long> {
    @Query("select m, avg(coalesce(r.grade, 0)), count(distinct r) from Movie m "
            + "left outer join Review r on r.movie = m group by m")
    Page<Object[]> getListPage(Pageable pageable);

}
 @Test
    public void testListPage(){
        PageRequest pageRequest  = PageRequest.of(0,10, Sort.by(Sort.Direction.DESC, "mno"));

        Page<Object[]> result = movieRepository.getListPage(pageRequest);

        for (Object[] objects : result.getContent()){
            System.out.println(Arrays.toString(objects));
        }
    }

영화별 평점, 리뷰 개수 출력

 

'Project > 2023.02~ ) Study toy 프로젝트' 카테고리의 다른 글

M:N 다대다 처리 02  (1) 2023.05.07
자잘한 이슈 고치기!  (0) 2023.03.13
@RestController와 JSON처리  (0) 2023.03.13
JPQL로 검색하기  (0) 2023.03.13
컨트롤러와 화면 처리  (0) 2023.03.06