일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
Tags
- java
- 시큐리티 로그아웃
- sql
- springSecurity
- input태그
- Linux
- 반복문
- programmers
- JAVA11
- gradle
- 시큐리티 로그인
- 싱글톤
- 스프링 부트
- Spring boot
- 목록
- 2차원배열
- 코딩테스트
- 프로그래머스
- 리눅스
- html
- 로그인
- springboot
- 시큐리티로그인
- css
- codingtest
- 시큐리티
- StyleSheet
- 소스트리
- javascript
- security
Archives
- Today
- Total
JAVAIARY
M : N (다대다) 관계의 설계와 구현 본문
목표 : JPA를 이용해서 M:N(다대다) 관계의 구현 방법을 알아본다.
- 예제
영화(Movie), 회원(Member) 테이블이 존재하고, 회원이 영화에 대한 평점과 감상을 기록한다.
- 한 편의 영화는 여러 회원의 평가가 행해질 수 있다.
- 한 명의 회원은 여러 영화에 대해 평점을 줄 수 있다.
1. 다대다 관계의 특징
1) 논리적 설계와 실제 테이블 설계가 다름
- 회원은 여러 편의 영화를 평가할 수 있음
- 한 편의 영화는 여러 회원이 존재함
ex)
- 학생 / 수업: 한 명의 학생은 여러 수업에 참여하고, 하나의 수업은 여러 학생이 수강한다.
- 상품 / 상품 카테고리: 하나의 상품은 여러 카테고리에 속하고, 하나의 카테고리는 여러 상품을 가지고 있다.
- 상품 / 회원 : 하나의 상품은 여러 회원이 구매할 수 있고, 한 명의 회원은 여러 상품을 구매할 수 있다.
- 대부분의 관계형 데이터베이스는 컬럼을 지정하면서 최대 크기를 지정하기 때문에 수평적 확장이 불가능
- But, 수직적 확장(Row)은 가능
- ==> 매핑 (Mapping) 테이블을 사용하여 두 테이블의 중간에서 필요한 정보를 양쪽에서 받아옴

- 매핑테이블의 특징
- 매핑테이블의 작성 이전에 다른 테이블이 먼저 존재해야 함
- 매핑 테이블은 주로 '명사'가 아닌 '동사'나 '히스토리'에 대한 데이터를 보관하는 용도
- 중간에서 양쪽의 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 파일도 함께 수정

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("=======================================");
});
}
}



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);
});
}
}

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 |