1

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

Souryuusen
  • 11
  • 1
  • it tries to insert because PK is null/0, the constraint violation occurs (most likely) because there is already a movie with this "imdbUrl" .. – xerx593 May 11 '23 at 18:59
  • So in theory assinging PK (movieID field) to the one matching already existing entry would cause it call update instead of insert? i.e first search in db for imdbLink then modify the entity and save it again - am i thinking correctly here? – Souryuusen May 11 '23 at 19:04
  • Theory: yes that's how "spring data repository save" works (to distinguish between "create" or "update") ... especially true with "generated id's" ...with "manual id" it'd be more complex – xerx593 May 11 '23 at 19:34
  • Your thinking is also "ok", but you/someone should ensure: `a.id == b.id` <=> `a.imdbUrl == b.imdbUrl` – xerx593 May 11 '23 at 19:35
  • Easiest/best the "client" doesn't know/provide the "id" ...then you can treat "imdbUrl" as "natural id" ..with eventually doing custom "create or update logic" (i.e. a "select" up front) – xerx593 May 11 '23 at 19:42
  • One small (but important!) thing I miss/didn't find: `hashCode` and `equals` methods... (You use `movie.equals(other)`!) ..are they in "etc..."!? – xerx593 May 11 '23 at 19:51
  • 1
    Ah the hashCode and equals methods are defined inside "BaseEntity" `@EqualsAndHashCode(of = {"uuid"}) public abstract class BaseEntity { @Getter @Setter private UUID uuid = UUID.randomUUID(); }` – Souryuusen May 11 '23 at 19:58
  • It was one the the things I've tried based on some article i've digged up on web - the question remains if it sensible to be defined that way – Souryuusen May 11 '23 at 19:59
  • this hashCode and equals is problematic! (in jpa/orm "equals" should "align with pk"... .i.e. "identity") – xerx593 May 11 '23 at 23:22
  • Ok, so the proper way is to use condition like id == m.id && imdbURL.equalsIgnoreCase(m.getImdbUrl()) in order to ensure true equality between entities and calculate hash from those two fields. But in that case should there be method to check whenever entities are equal field-value-like? – Souryuusen May 12 '23 at 07:17
  • Id *and* imdbUrl (match) is too strict, i think... The equals method should consider all "possible" cases: persisted vs new entity, "times": neither matches (easy), both match (also easy), only one matches (this i would avoid! "times": id matches, or imdbUrl matches)... – xerx593 May 12 '23 at 11:39
  • 1
    Ah, ok. Thanks for bearing with me so the best way is define url with @NaturalId annotation and base my equals and hash on that value because I know that it has to be unique within all entities set. Your answers really helped me to "sort" all of the used mechanisms in proper places. Thanks again ;) – Souryuusen May 12 '23 at 11:50

0 Answers0