목차
- 데이터베이스의 관계
- 엔티티 관계
- M:N 관계의 특징
- 매핑 테이블의 특징
- JPA의 연관관계 설정과 처리
- JPA에서 M:N 다대다 처리 방식
- @ManyToMany를 이용한 처리의 문제
- 연관관계가 있는 엔티티의 DTO 처리
- 엔티티 클래스 설계
- REST방식의 데이터 처리
엔티티 관계
- 영화와 회원 엔티티
- 회원이 영화에 대한 평점과 감상을 기록
- 한 편의 영화에 여러 회원의 평가
- 한 명의 회원은 여러 영화에 평점
- 게시물과 댓글의 경우엔, 하나의 댓글은 하나의 게시물에만 속하지만
- 영화와 회원은 각각 독립적인 존재
M:N 관계의 특징
- 논리적 설계와 실제 테이블 설계가 다르다.
- 개념적으로 다대다를 사용하지만, 실제 테이블 설계에서는 불가능하다.
- 테이블 구조의 RDBMS에서는, 칼럼을 지정할 때 크기를 지정
- 따라서, 칼럼의 개수를 늘리는 수평적 확장이 불가능
- 튜플의 개수를 늘리는 수직적 확장이 가능한 점을 이용해, 연결 테이블로 다대다 관계를 구현
- 매핑 테이블이라고도 불리는 연결 테이블은 두 테이블에서 필요한 정보들을 가져오는 구조
매핑 테이블의 특징
- 다른 테이블의 정보를 이용하므로, 이전 테이블 정보가 존재해야 한다.
- '동사'나 '히스토리'에 대한 데이터를 보관하는 용도
- 양쪽의 PK를 참조하는 형태
JPA에서 M:N 다대다 처리 방식
- @ManyToMany를 이용한 처리
- 별도 엔티티를 설계하고, @ManyToOne을 이용한 처리
@ManyToMany를 이용한 처리의 문제
- 각 엔티티와의 매핑 테이블이 자동으로 생성되는 방식
- 새로운 필드를 만드는 게 불가능한 문제점
- 리뷰테이블에서 필요한 영화의 평점 정보를 생성 불가
- 양방향 참조를 이용하는 어노테이션으로 발생가능한 문제점
- 영속성 컨텍스트에 존재하는 엔티티 객체의 상태와 DB의 상태를 동기화시켜야 한다.
- 하나의 객체 수정시 다른 객체 상태 일치시키는 작업의 어려움
- 따라서 실무에서는 단반향 참조 위주로 프로젝트 설계
진행 순서
- 매핑 테이블 직접 설계
- 직접 매핑 관계 연결
- 연관 관계를 이용한 조회가 필요한 데이터는 JPQL을 이용해 추출
1. 매핑 테이블 직접 설계
1-1. 엔티티 클래스 설계
(1) entity패키지에 날짜/시간 클래스인 BaseEntity 클래스 추가
package com.movie.boot4.entity;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@MappedSuperclass
@EntityListeners(value = { AuditingEntityListener.class })
@Getter
abstract class BaseEntity {
@CreatedDate
@Column(name = "regdate", updatable = false)
private LocalDateTime regDate;
@LastModifiedDate
@Column(name ="moddate")
private LocalDateTime modDate;
}
-> 리스너 사용을 위해 appplication에 @EnableJpaAuditing 어노테이션 추가
(2) Movie 엔티티 설계
- M:N 관계 처리
- 명사에 해당하는 클래스 먼저 설계 -> 영화와 회원 엔티티 클래스 설계
- 매핑 테이블 설계는 마지막 단계에서 수행
- 영화 Movie 엔티티, 영화 이미지 MovieImage 엔티티
- Movie 엔티티의 경우 BaseEntity클래스를 상속해서 작성
Movie 엔티티
package com.movie.boot4.entity;
import com.fasterxml.jackson.databind.ser.Serializers;
import lombok.*;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
public class Movie extends Serializers.Base {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long mno;
private String title;
}
MovieImage 엔티티
package com.movie.boot4.entity;
import lombok.*;
import javax.persistence.*;
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@ToString(exclude = "movie")
//연관 관계가 존재할 경우에, ToString을 사용할 때에 반복 주의
public class MovieImage {
//값 객체가 아닌 엔티티객체로 설정하는 이유는 페이지 처리나 조인 처리가 많으므로
//단방향 참조로 처리해 @Query를 통해 조인을 사용하므로, JPQL에서 엔티티일 경우 사용이 자유롭다.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long inum;
//uuid클래스를 이용해 고유한 번호를 생성해 사용
private String uuid;
private String imgName;
//이미지 저장경로는 년/월/일 구조를 의미한다.
private String path;
//movie 테이블이 PK를 가지며, movieimage테이블이 FK를 가지는 구조
//다대일의 경우 지연로딩 설정 필요
@ManyToOne(fetch = FetchType.LAZY)
private Movie movie;
}
(3) Member 엔티티 설계
: 고유한 번호, 이메일, 아이디와 패스워드, 닉네임을 가진 클래스
BaseEntity를 상속하는 Member 엔티티
package com.movie.boot4.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;
}
1-2. 매핑 테이블을 위한 Review 클래스 설계
: 회원이 영화에 대해서 평점을 준다.
-> 평점을 준다는 행위에 필요한 매핑 테이블
: 두 테이블 사이에서 양쪽의 PK를 참조하는 구조로, 단방향 참조의 경우에 FK테이블 기준으로 설계
Review 엔티티
package com.movie.boot4.entity;
import lombok.*;
import javax.persistence.*;
@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;
}
1-3. M:N (다대다) Repository와 테스트
: 리포지토리 구성 후, 필요한 데이터 구하는 테스트 코드 수행
- MovieRepository / MovieImageRepository
- MovieRepositoryTests
- MemberRepository
- MemberRepositoryTests
(1) Repository 작성과 테스트 데이터 추가
:repository 패키지 작성 후, MovieRepository와 MovieImageRepository 인터페이스 구성
MovieRepository 인터페이스
package com.movie.boot4.repository;
import com.movie.boot4.entity.Movie;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MovieRepository extends JpaRepository<Movie, Long> {
}
MovieImageRepository 인터페이스
package com.movie.boot4.repository;
import com.movie.boot4.entity.MovieImage;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MovieImageRepository extends JpaRepository<MovieImage, Long> {
}
MovieRepositoryTests
package com.movie.boot4.repository;
import com.movie.boot4.entity.Movie;
import com.movie.boot4.entity.MovieImage;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Commit;
import org.springframework.transaction.annotation.Transactional;
import java.util.UUID;
import java.util.stream.IntStream;
@SpringBootTest
public class MovieRepositoryTests {
//테스트를 위한 의존성 주입
@Autowired
private MovieRepository movieRepository;
@Autowired
private MovieImageRepository movieImageRepository;
//영화와 영화 이미지는 같은 시점에 삽입되어야 하므로
//(1) Movie 객체를 먼저 save
//(2) Movie 객체에 해당하는 PK를 이용해 영화 이미지들을 추가
//(3) 영화 이미지는 최대 5개까지 임의로 저장 [특정 영화 이미지는 많을 수 있으므로 임의의 수로 처리]
//트랜잭션이 끝나면 처리하도록 해야 하고, 커밋을 수행하도록 테스트 작성
@Commit
@Transactional
@Test
public void insertMovies(){
//스트림과 빌더패턴을 이용한 영화 객체 생성
IntStream.rangeClosed(1,100).forEach(i -> {
//100까지 포함
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 클래스를 이용한 고유한 번호를 만들고 -> toString
.uuid(UUID.randomUUID().toString())
.movie(movie)
.imgName("test"+j+".jpg")
.build();
movieImageRepository.save(movieImage);
System.out.println("====================================");
}
});
}
}
MemberRepository
package com.movie.boot4.repository;
import com.movie.boot4.entity.Member;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.stream.IntStream;
@SpringBootTest
public class MemberRepositoryTests {
@Autowired
private MemberRepository memberRepository;
@Test
public void insertMembers(){
IntStream.rangeClosed(1,100).forEach(i->{
Member member= Member.builder()
.email("r"+i+"@naver.com")
.pw("1234")
.nickname("reviewer"+i)
.build();
memberRepository.save(member);
});
}
}
ReviewRepository
package com.movie.boot4.repository;
import com.movie.boot4.entity.Review;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ReviewRepository extends JpaRepository<Review, Long> {
}
ReviewRepositoryTests
package com.movie.boot4.repository;
import com.movie.boot4.entity.Member;
import com.movie.boot4.entity.Movie;
import com.movie.boot4.entity.Review;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.stream.IntStream;
@SpringBootTest
public class ReviewRepositoryTests {
@Autowired
private ReviewRepository reviewRepository;
@Test
public void insertMovieReviews(){
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);
});
}
}
화면 출력을 위한 진행 순서
- 화면에 필요한 데이터 처리
- 목록 화면에서 영화의 제목과 이미지 하나, 영화 리뷰의 평점/리뷰 개수 출력
- 영화 조회화면에서 영화와 영화 이미지들, 리뷰의 평균점수/리뷰 개수를 같이 출력
- 리뷰에 대한 정보에는 회원의 이메일이나 닉네임과 같은 정보를 같이 출력
- 데이터 처리 방식은 @Query를 이용한 JPQL로 처리
- @EntityGraph나 서브쿼리를 활용
- 일대다 관계로 놓인 영화와 영화 이미지 엔티티에서 리뷰를 조인
진행 순서
- 페이지 처리되는 영화별 평균 점수/리뷰 개수
- 특정 영화의 모든 이미지와 평균 평점/ 리뷰 개수
1. 페이지 처리되는 영화별 평균 점수/리뷰 개수 구하기
: JPQL에서 group by를 적용해, 리뷰의 개수와 리뷰의 평균 평점을 구한다.
(1) 영화와 리뷰를 이용해 페이징 처리
MovieRepository - 비오라클 사용시
: only full group by 사용
package com.movie.boot4.repository;
import com.movie.boot4.entity.Movie;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;
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);
}
MovieRepository - 오라클 사용시
: only full group by 사용 불가
package com.movie.boot4.repository;
import com.movie.boot4.entity.Movie;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;
public interface MovieRepository extends JpaRepository<Movie, Long> {
//영화와 리뷰를 이용한 페이징 처리
@Query("select min(r.movie), avg(NVL(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);
}
+) MovieRepositoryTest의 testListPage에 @Transactional 추가 필요
: Review엔티티가 Movie객체를 지연로딩으로 참조하기 때문에, 프록시 객체를 못찾는 문제가 발생하기 때문
package com.movie.boot4.repository;
import com.movie.boot4.entity.Movie;
import com.movie.boot4.entity.MovieImage;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.test.annotation.Commit;
import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.IntStream;
@SpringBootTest
public class MovieRepositoryTests {
//테스트를 위한 의존성 주입
@Autowired
private MovieRepository movieRepository;
@Autowired
private MovieImageRepository movieImageRepository;
//중략
@Transactional
@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));
}
}
}
(2) 페이징 처리 후, 영화 이미지를 추가
package com.movie.boot4.repository;
import com.movie.boot4.entity.Movie;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;
public interface MovieRepository extends JpaRepository<Movie, Long> {
//영화와 리뷰를 이용한 페이징 처리
//영화 기준
@Query("select min(r.movie), max(mi), avg(NVL(r.grade, 0)), count (distinct 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문제 발생
: 영화 이미지 테이블에서 해당하는 모든 영화의 이미지를 다 가져오기 때문에, 비효율적인 SQL문
[max() 함수에서 문제 발생]
N+1 문제
:1번의 쿼리로 N개의 쿼리를 가져오는데 N개의 데이터를 처리하기 위해 필요한 추가적인 쿼리가 각 N개에 대해서 수행
1페이지에 해당하는 10개의 데이터를 가져오는 쿼리 1번과 각 영화의 모든 이미지를 가져오기 위한 10번의 추가적인 쿼리 실행
-> 한 페이지 출력시 11번의 쿼리를 실행하는 비효율적인 쿼리
(3) N+1문제 해결 : 영화마다의 이미지를 1개로 줄여서 처리하는 방법
-> 문제가 되는 max()를 없애고, MovieImage를 출력한다.
: 중간에 반복적으로 실행되는 부분 없이, 목록을 구하는 쿼리와 개수를 구하는 쿼리만 실행
MovieRepository - 비오라클
package com.movie.boot4.repository;
import com.movie.boot4.entity.Movie;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;
public interface MovieRepository extends JpaRepository<Movie, Long> {
//영화와 리뷰를 이용한 페이징 처리
//영화 기준
@Query("select m,mi, avg(coalesce(r.grade, 0)), count (distinct 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);
}
MovieRepository - 오라클
package com.movie.boot4.repository;
import com.movie.boot4.entity.Movie;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;
public interface MovieRepository extends JpaRepository<Movie, Long> {
//영화와 리뷰를 이용한 페이징 처리
//영화 기준
@Query("select min(r.movie), min(mi), avg(NVL(r.grade, 0)), count (distinct 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);
}
-> Movie와 MovieImage와의 관계는 가장 먼저 입력된 이미지 번호와 연결
가장 나중에 입력된 이미지 번호와 연결하는 방법
@Query("select min(r.movie), min(mi), avg(NVL(r.grade, 0)), count (distinct r) from Movie m "
//영화 이미지 외부 조인
+"left outer join MovieImage mi on mi.movie =m "
//나중에 입력된 이미지를 선택하는 쿼리
+"and mi.inum=(select max(i2.inum) from MovieImage i2 where i2.movie=m)"
//리뷰 외부 조인 + 영화 기준 그룹바이
+ "left outer join Review r on r.movie=m group by m")
2. 특정 영화의 모든 이미지와 평균 평점 / 리뷰 개수
: 영화 조회시 해당 영화의 평균 평점 / 리뷰 개수를 화면에서 사용하므로, MovieRepository에 해당 기능 추가
package com.movie.boot4.repository;
import com.movie.boot4.entity.Movie;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
public interface MovieRepository extends JpaRepository<Movie, Long> {
//영화와 리뷰를 이용한 페이징 처리
//중략
//특정 영화 조회 처리
@Query("select mi.movie,mi from Movie m "
+"left outer join MovieImage mi on mi.movie = m where mi.movie.mno = :mno")
List<Object[]> getMovieWithAll(Long mno);
}
: 리뷰와 관련된 내용처리 : left join 이용
-> 리뷰와 조인 후, count(), avg()등의 함수를 이용하는데, 이 때 영화 이미지 별로 group by 실행
package com.movie.boot4.repository;
import com.movie.boot4.entity.Movie;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
public interface MovieRepository extends JpaRepository<Movie, Long> {
//영화와 리뷰를 이용한 페이징 처리
//중략
//특정 영화 조회 처리
//: Group by를 이용해 영화 이미지별로 그룹을 생성해 이미지 개수만큼 데이터를 만든다.
@Query("select min(mi.movie),min(mi), avg(NVL(r.grade, 0)), count(r) from Movie m "
//리뷰 테이블과 외부조인
+"left outer join Review r on r.movie =m "
//영화이미지 테이블과 외부조인
+"left outer join MovieImage mi on mi.movie = m where mi.movie.mno = :mno "
+"group by mi ")
List<Object[]> getMovieWithAll(Long mno);
}
: 영화 이미지별로 그룹을 만들어 이미지 개수만큼 데이터를 만든다.
3. 특정 영화의 모든 리뷰와 회원의 닉네임
: 영화 조회 화면 기능
- 영화에 대한 리뷰 조회
- 영화 리뷰 등록
- 영화 리뷰 수정/삭제
영화 조회 화면 기능을 구현하기 위해 필요한 데이터
- 특정한 영화의 번호
- 영화 번호를 이용한 해당 영화의 리뷰 정보
package com.movie.boot4.repository;
import com.movie.boot4.entity.Member;
import com.movie.boot4.entity.Movie;
import com.movie.boot4.entity.Review;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import java.util.stream.IntStream;
@SpringBootTest
public class ReviewRepositoryTests {
@Autowired
private ReviewRepository reviewRepository;
@Test
public void insertMovieReviews() {
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);
});
}
//특정 영화의 모든 리뷰와 회원의 닉네임 조회 테스트
@Test
public void testGetMovieReviews(){
//필요한 정보
//(1) 영화 객체
//(2) 찾은 영화의 리뷰 정보를 담은 리뷰 리스트
Movie movie=Movie.builder().mno(92L).build();
List<Review> result = reviewRepository.findByMovie(movie);
//reuslt를 돌면서 리뷰 정보 출력하는 forEach문
result.forEach(movieReview -> {
System.out.println(movieReview.getReviewnum());
System.out.println("\t"+movieReview.getGrade());
System.out.println("\t"+movieReview.getText());
System.out.println("\t"+movieReview.getMember());
System.out.println("\t"+movieReview.getMember().getEmail());
System.out.println("---------------------------");
});
}
}
-> 지연로딩으로 인한 한 번에 Review 객체와 Member 객체를 조회하지 못하는 문제
: @Transactional로 해결
: Rolled back transaction for test 즉, 예외로 인한 롤백처리
지연로딩으로 인한 문제를 @Transactional로 해결 못하는 경우
: 각 Review 객체에서 필요한 정보를 가져오기 위해 Member객체를 로딩해야하는 경우
-> 즉, 지연로딩이 두 번 걸리는 경우
객체를 여러개 조회할 때, 특정 데이터를 위해 객체를 로딩해야 할 경우
- @Query를 이용한 조인 처리
- @EntityGraph를 이용해 엔티티의 필요한 속성만 함께 로딩하는 방법
- 한 객체 처리시 필요한 데이터를 처리하기 위한 객체 또한 로딩하도록 변경하는 방법
@EntityGraph 특징
- attributePaths 속성과 type 속성
- attributePaths 속성
- 로딩 설정을 변경하고 싶은 속성의 이름을 배열로 명시
- type 속성
- @EntityGraph를 어떤 방식으로 적용할 것인지 설정
- FETCH : 명시한 속성을 EAGER로 처리, 나머지 LAZY로 처리
- LOAD : 명시한 속성을 EAGER로 처리, 나머지 엔티티 클래스에 명시 OR 기본 방식으로 처리
- @EntityGraph를 어떤 방식으로 적용할 것인지 설정
- attributePaths 속성
@EntityGraph 적용한 ReviewRepository
package com.movie.boot4.repository;
import com.movie.boot4.entity.Movie;
import com.movie.boot4.entity.Review;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface ReviewRepository extends JpaRepository<Review, Long> {
//특정 영화의 모든 리뷰와 회원의 닉네임
//: 특정 객체의 엔티티 속성만을 먼저 로딩하는 방법
@EntityGraph(attributePaths = {"member"}, type= EntityGraph.EntityGraphType.FETCH)
List<Review> findByMovie(Movie movie);
}
4. 회원 삭제 문제와 트랜잭션 처리
: 다대다 관계에서 명사에 해당하는 데이터 삭제시 중간에 매핑 테이블에서의 삭제도 필요하다.
-> 특정 회원 삭제시, 해당 회원이 등록한 모든 영화 리뷰 또한 삭제해야 하기 때문에
1번 회원 삭제시
(1) 1번 회원이 작성한 리뷰 3개를 먼저 삭제
(2) 리뷰 삭제 후 회원 삭제 진행
(3) 이 두가지 작업은 하나의 트랜잭션으로 수행
-> ReviewRepository에 회원을 이용해 삭제하는 메서드 추가
ReviewRepository
void deleteByMember(Member member);
package com.movie.boot4.repository;
import com.movie.boot4.entity.Member;
import com.movie.boot4.entity.Review;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Commit;
import org.springframework.transaction.annotation.Transactional;
import java.util.stream.IntStream;
@SpringBootTest
public class MemberRepositoryTests {
@Autowired
private MemberRepository memberRepository;
@Autowired
private ReviewRepository reviewRepository;
@Test
public void insertMembers(){
IntStream.rangeClosed(1,100).forEach(i->{
Member member= Member.builder()
.email("r"+i+"@naver.com")
.pw("1234")
.nickname("reviewer"+i)
.build();
memberRepository.save(member);
});
}
//회원 삭제시 리뷰 삭제후 회원 삭제하는 메서드 테스트
@Test
public void testDeleteMember(){
//삭제할 멤버의 번호와 해당 멤버객체 생성
Long mid = 1L;
Member member = Member.builder()
.mid(mid)
.build();
//FK위배의 경우
memberRepository.deleteById(mid);
reviewRepository.deleteByMember(member);
}
}
문제점
(1) 참조 무결성 제약조건 위배
(2) 트랜잭션 조건 위배
해결
(1) @Transactional, @Commit 추가
(2) 한번에 삭제하기위한 쿼리로 변경
package com.movie.boot4.repository;
import com.movie.boot4.entity.Member;
import com.movie.boot4.entity.Movie;
import com.movie.boot4.entity.Review;
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 java.util.List;
public interface ReviewRepository extends JpaRepository<Review, Long> {
//특정 영화의 모든 리뷰와 회원의 닉네임
//: 특정 객체의 엔티티 속성만을 먼저 로딩하는 방법
@EntityGraph(attributePaths = {"member"}, type= EntityGraph.EntityGraphType.FETCH)
List<Review> findByMovie(Movie movie);
//회원 삭제시 리뷰 먼저 삭제 후 회원 삭제하는 메서드
//: 비효율적인 쿼리를 개선하기 위한 어노테이션 필요 -> update/delete할 경우 필요
//-> 해당 회원의 리뷰를 한번에 삭제하는 쿼리
@Modifying
@Query("delete from Review mr where mr.member=:member ")
void deleteByMember(Member member);
}
MemberRepositoryTests
package com.movie.boot4.repository;
import com.movie.boot4.entity.Member;
import com.movie.boot4.entity.Review;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Commit;
import org.springframework.transaction.annotation.Transactional;
import java.util.stream.IntStream;
@SpringBootTest
public class MemberRepositoryTests {
@Autowired
private MemberRepository memberRepository;
@Autowired
private ReviewRepository reviewRepository;
@Test
public void insertMembers(){
IntStream.rangeClosed(1,100).forEach(i->{
Member member= Member.builder()
.email("r"+i+"@naver.com")
.pw("1234")
.nickname("reviewer"+i)
.build();
memberRepository.save(member);
});
}
//회원 삭제시 리뷰 삭제후 회원 삭제하는 메서드 테스트
//리뷰가 존재할 때 회원 삭제 오류 발생
//(1) 참조 무결성 제약조건 위배
//-> FK를 가지는 Review 쪽을 먼저 삭제하지 않았기 때문에
//(2) 트랜잭션 관련 처리 미비
//-> 회원 삭제시 회원이 작성한 리뷰와 회원 삭제는 동시에 일어나야하는 조건
@Transactional
@Commit
@Test
public void testDeleteMember(){
//삭제할 멤버의 번호와 해당 멤버객체 생성
Long mid = 1L;
Member member = Member.builder()
.mid(mid)
.build();
//FK위배의 경우
//memberRepository.deleteById(mid);
//reviewRepository.deleteByMember(member);
//@Transactional과 @Commit 추가 후에도
// 비효율적인 SQL -> 하나씩 삭제하기 위해 리뷰 개수만큼 반복 실행
//: @Query를 이용해 where절 지정
reviewRepository.deleteByMember(member);
memberRepository.deleteById(mid);
}
}
인텔리제이 콘솔창 한글 깨짐
https://www.lesstif.com/java/intellij-file-console-encoding-121012310.html
'Server Programming > Spring Boot Backend Programming' 카테고리의 다른 글
[Spring 부트 - 영화 리뷰 프로젝트] 2. 파일 업로드 처리 (2) 섬네일 이미지를 통한 화면 처리와 파일 삭제 (0) | 2022.10.18 |
---|---|
[Spring 부트 - 영화 리뷰 프로젝트] 2. 파일 업로드 처리 (1) Ajax를 통한 JSON으로 이미지 업로드 (0) | 2022.10.18 |
[Spring 부트 - 댓글 프로젝트] 3-4. 댓글 비동기처리를 위한 @RestController와 JSON 처리 (1) | 2022.10.04 |
[Spring 부트 - 댓글 프로젝트] 3-2. 게시물과 댓글, 컨트롤러와 화면 처리 [자바스크립트] (1) | 2022.10.03 |
[Spring 부트 - 댓글 프로젝트] 3-1. N:1 연관관계의 게시물과 댓글 CRUD (1) | 2022.10.03 |