Jdbc 는 컬렉션 엔티티의 저장 시 객체 범위에서는 id값이 null이 된다. 즉, 매서드 필드에서 id 를 재사용 할 수 없다.
따라서 먼저 참조될 객체를 저장하고, 저장된 객체를 불러와서 벌크 인서트 해야한다.
CommandLineRunner, @PostConstruct 모두 애플리케이션 런 시점에 실행되지만, @PostConstruct는 빈의 초기화 이후 실행되므로 더 빠르다.
@PostConstruct는 빈의 초기화 이후에 실행되며,
CommandLineRunner 는 애플리케이션 실행 직후에 실행된다.
따라서 @PostConstruct 가 먼저 실행되고, 그 후 CommandLineRunner의 run 매서드가 실행된다.
@Component
@RequiredArgsConstructor
public class DummyDataLoader implements CommandLineRunner {
private final MovieRepository movieRepository;
private final JdbcBulkInsertRepository jdbcBulkInsertRepository;
@Override
public void run(String... args) throws Exception {
List<Movie> movies = movieRepository.findAll();
List<MovieImage> movieImages = new ArrayList<>();
for (Movie movie : movies) {
for (int i = 1; i <= 5; i++) {
MovieImage movieImage = MovieImage.builder()
.movie(movie)
.imageUrl("image_" + i + "_for_movie_" + movie.getId())
.build();
movieImages.add(movieImage);
}
}
jdbcBulkInsertRepository.bulkInsertMovieImage(movieImages);
List<MovieVideo> movieVideos = new ArrayList<>();
for (Movie movie : movies) {
for (int i = 1; i <= 3; i++) {
MovieVideo movieVideo = MovieVideo.builder()
.movie(movie)
.videoUrl("video_" + i + "_for_movie_" + movie.getId()).build();
movieVideos.add(movieVideo);
}
}
jdbcBulkInsertRepository.bulkInsertMovieVideo(movieVideos);
List<CastMember> castMembers = new ArrayList<>();
for (Movie movie : movies) {
for (int i = 1; i <= 3; i++) {
CastMember castMember = CastMember.builder()
.memberName("Cast Member " + i)
.movie(movie)
.build();
castMembers.add(castMember);
}
}
jdbcBulkInsertRepository.bulkInsertCastMember(castMembers);
}
@PostConstruct
public void afterRun(){
List<Movie> movies = IntStream.rangeClosed(1, 20)
.mapToObj(i -> Movie.builder()
.releaseDate((long) i)
.posterImageUrl("poster_" + i)
.movieName("Movie " + i)
.director("Director " + i)
.genre("Genre " + i)
.originalTitle("Original Title " + i)
.synopsis("Synopsis " + i)
.runningTime(120)
.build())
.collect(Collectors.toList());
jdbcBulkInsertRepository.bulkInsertMovies(movies);
}
}
위 매서드에 오기까지 많은 시행착오들, 그리고 전제 조건들이 있었는데 그 부분은 다음과 같다.
JpaRepository 에서 매서드로 기본 제공하는 saveAll()은 컬렉션 타입의 엔티티에 대한 저장이 N 개만큼 일어난다. 따라서 성능의 이슈가 있을 수 있다 (조사 결과 1000건 이상으로는 매우 낮은 성능을 보인다)
따라서 하나의 쿼리로 인서트 하는 게 필요한데, Jdbc Template 사용 시 이게 가능하다.
하나의 매서드에서 모든 엔티티를 저장 시, Movie 엔티티와 연관관계에 있는 컬렉션 엔티티들은 Movie 엔티티가 생성되고 나서야 생성 될 수 있다. (Forein key로 무비 엔티티가 필요하다. 그리고 단순히 id로의 참조가 불가능 한 게, 객체 타입으로 참조하고 있기 때문이다. id 참조 방식으로 엔티티 설계를 바꾸는 것 도 고려했지만, 이점보다 단점이 많다고 판단했다)
그런데 이 때 Jdbc 는 JPA 와 같은 영속성 컨텍스트를 사용하지 않는다는 특징을 가진다.
위 특성은 다음과 같은 문제를 가진다.
public void bulkInsertMovieImage(List<MovieImage> movieImages) {
String movieImageSql = "INSERT INTO movie_image (image_url, movie_id) VALUES (?, ?)";
jdbcTemplate.batchUpdate(movieImageSql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
MovieImage movieImage = movieImages.get(i);
ps.setString(1, movieImage.getImageUrl());
ps.setLong(2, movieImage.getMovie().getId());
}
@Override
public int getBatchSize() {
return movieImages.size();
}
});
}
위처럼 MovieImage는 무비 객체의 id값이 필요하다. 이 때 getMovie().getId(); 와 같은 방법이 아닌 아이디를 strict 하게 세팅하는 방법도 있겠지만, 더미 데이터를 위한 벌크 인서트 외에 사용은 제한된다(매번 설정해줘야 하므로).
에러메시지는 다음과 같다.
Caused by: java.lang.NullPointerException: Cannot invoke "java.lang.Long.longValue()" because the return value of "com.example.movie.movie.entity.Movie.getId()" is null
럴수럴수 이럴수!! 완전히 같다
안썼을 때
썼을 때
140%의 속도 차이
150%의 속도 차이
위 속도들은 aop 사용한 "매서드"의 실행 시간을 측정한 것이므로 오차가 있을 수 있다.
https://jojoldu.tistory.com/507
여기서 볼 수 있듯, id의 생성 전략을 다르게 가져가서 성능 최적화와 구조적 최적화를 둘 다 시도 해 볼 수 있을 듯 하다.