Why is unique keyword in column definition causes IntegrityConstraintViolationException when trying to update entity

50 Views Asked by At

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...

0

There are 0 best solutions below