I have a really simple project using Spring Data JPA with Hibernate consisting of:
Movie Entity:
@Entity
@Table(name = "MOVIES")
@NoArgsConstructor @RequiredArgsConstructor
@ToString(exclude = {"directors", "writers", "reviews"})
public class Movie extends BaseEntity{
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "movie_id") @Getter
private long movieID;
@Column(name = "title", nullable = false) @Getter @Setter @NonNull
private String title;
@Column(name = "imdb_url", unique = true) @Getter @Setter
private String imdbUrl;
@Column(name = "cover_url") @Getter @Setter
private String coverUrl;
@Column(name = "runtime") @Getter @Setter
private String runtime;
@Column(name = "language") @Getter @Setter
private String language;
@Column(name = "release_date") @Getter @Setter
private String releaseDate;
@Getter @Setter
@ManyToMany(cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
@JsonIgnoreProperties({"directors", "writers", "reviews"})
@JoinTable(
name = "MOVIE_DIRECTORS",
joinColumns = @JoinColumn(name = "movie_id"),
inverseJoinColumns = @JoinColumn(name = "director_id")
)
private Set<Director> directors;
@Getter @Setter
@ManyToMany(cascade = {CascadeType.MERGE, CascadeType.PERSIST})
@JoinTable(
name = "MOVIE_WRITERS",
joinColumns = @JoinColumn(name = "movie_id"),
inverseJoinColumns = @JoinColumn(name = "writer_id")
)
private Set<Writer> writers;
@Getter @Setter
@OneToMany(mappedBy = "reviewedMovie", cascade = {CascadeType.MERGE, CascadeType.PERSIST})
private Set<Review> reviews;
// Methods to add elements to directors etc...
}
Director Entity:
@Entity
@Table(name = "Directors")
@NoArgsConstructor @RequiredArgsConstructor
@ToString(includeFieldNames = false, onlyExplicitlyIncluded = true)
public class Director extends BaseEntity{
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "director_id") @Getter
private long directorID;
@Column(name = "name", nullable = false, unique = true) @Getter @Setter @NonNull @ToString.Include
private String name;
@Setter @Getter
@ManyToMany(mappedBy = "directors")
private Set<Movie> directedMovies;
}
Writer Entity:
@Entity
@Table(name = "Writers")
@NoArgsConstructor @RequiredArgsConstructor
@ToString(includeFieldNames = false, onlyExplicitlyIncluded = true)
public class Writer extends BaseEntity{
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "writer_id") @Getter
private long writerID;
@Column(name = "name", nullable = false, unique = true) @Getter @Setter @NonNull @ToString.Include
private String name;
@ManyToMany(mappedBy = "writers")
private Set<Movie> writtenMovies;
}
Review Entity:
@Entity
@Table(name = "REVIEWS")
@NoArgsConstructor @RequiredArgsConstructor
@ToString
public class Review extends BaseEntity{
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Setter @Getter
@Column(name = "review_id")
private long reviewID;
@Getter @Setter @NonNull
@Column(name = "rating", nullable = false)
private int rating;
@Getter @Setter @NonNull
@Column(name = "review")
private String review;
@Getter @Setter @NonNull
@Column(name = "creation_time")
private ZonedDateTime creationData;
@Getter @Setter @NonNull @ToString.Exclude
@ManyToOne
@JoinColumn(name = "movieID", nullable = false)
private Movie reviewedMovie;
}
The problem is occuring after initial persisting of 1 movie entity with set directors/writers/reviews app throws
Caused by: org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException:
Unique index or primary key violation: "PUBLIC.UK_BAQ304EOVCBM0CVQ2QNDI1W2N_INDEX_8 ON PUBLIC.MOVIES(IMDB_URL NULLS FIRST) VALUES ( /* 1 */ 'xxx' )"; SQL statement:
insert into movies (movie_id, cover_url, imdb_url, language, release_date, runtime, title) values (default, ?, ?, ?, ?, ?, ?) [23505-214]
I've created test code for above:
@Component
public class DataInitializer implements CommandLineRunner {
private final MovieService movieService;
private final ReviewService reviewService;
private final DirectorService directorService;
public DataInitializer(MovieService movieService, ReviewService reviewService, DirectorService directorService) {
this.movieService = movieService;
this.reviewService = reviewService;
this.directorService = directorService;
}
@Override
public void run(String... args) throws Exception {
addRandomMovies(2);
try{
Thread.sleep(5000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
fetchMovies();
}
private void addRandomMovies(int amount) {
for(int i = 1; i <= amount; i++) {
Movie m = new MovieBuilderImplementation().withTitle("Test Movie: " + (i)).withReleaseDate("" + (1970+i))
.withRuntime("1h " + i + "m " + i + "s").withLanguage("en")
.withImdbUrl("www.imdb.com/" + i).withCoverUrl("www.img.com/" + i).build();
m.addDirector(new Director("a" + i + " b"));
m.addWriter(new Writer("c" + i + " d"));
Review review = new Review(55, "xxx" + i + "!!", ZonedDateTime.now(), m);
Review review2 = new Review(55, "xxx" + i+1 + "!!", ZonedDateTime.now(), m);
Review review3 = new Review(55, "xxx" + i+2 + "!!", ZonedDateTime.now(), m);
m.addReview(review);
m.addReview(review2);
m.addReview(review3);
movieService.save(m); // <-- HERE LIES THE PROBLEM
}
}
}
Code runs only once then throws error. I understand that "unique" keyword is responsible for errors so after trying to remove it database gets filled with duplicate entries which is problematic.
Tried changing repository / service code for the movie:
@Repository
public interface MovieRepository extends JpaRepository<Movie, Long> {
public Movie getMovieByImdbUrlEqualsIgnoreCase(String imdbUrl);
@Query("SELECT m FROM Movie m " +
"JOIN FETCH m.directors " +
"JOIN FETCH m.writers " +
"JOIN FETCH m.reviews " +
"WHERE m.movieID = :id")
public Movie getMovieByIdWithAll(@Param("id") long id);
}
package com.souryuu.catalogit.service;
import com.souryuu.catalogit.entity.Movie;
import com.souryuu.catalogit.entity.Writer;
import com.souryuu.catalogit.repository.MovieRepository;
import jakarta.transaction.Transactional;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@Transactional
public class MovieService {
private final MovieRepository repository;
public MovieService(MovieRepository repository, DirectorService directorService, WriterService writerService, ReviewService reviewService) {
this.repository = repository;
}
// THISE METHOD IS CHANGED - AT FIRST IT WAS ONLY "this.repository.save(...)"
public Movie save(Movie movie) {
// Persist Movie
Movie fetchedMovie = getMovieByImdbUrl(movie.getImdbUrl());
if(fetchedMovie == null || !fetchedMovie.equals(movie)) {
return this.repository.save(movie);
} else {
return fetchedMovie;
}
}
public List<Movie> findAll() {
return this.repository.findAll();
}
public Movie getMovieWithAll(long movieId) {
return this.repository.getMovieByIdWithAll(movieId);
}
public Movie getMovieByImdbUrl(String imdbUrl) {
return this.repository.getMovieByImdbUrlEqualsIgnoreCase(imdbUrl);
}
}
I really do not know what to do with it next - in theory calling "save" should try to update persisted entity but well it tries to do insert...