javaspringhibernatejpapersistence

Spring Hibernate is not removing rows from the join table


I have two main tables called Movie and Actor. A Movie can have multiple actors and vice versa. So I made a join table called MovieActor. The problem is that I can't seem to find a way to remove the old rows from the MovieActor join table when the references from the two Tables are updated. I'm gonna visualize this so it's easier to understand. Let's say I have a movie that has two actors.

id movie_id actor_id
1 1 1
2 1 2

This is how the join table will connect them. But let's say I want to change the actor list. I want actor 3 and 4 instead of actor 1 and 2.

id movie_id actor_id
1 1 1
2 1 2
3 1 3
4 1 4

This is the problem I'm facing. Hibernate won't remove the old rows. The first two rows should be deleted from the DB since I'm updating the movie with different actors.

public class Movie {
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name = "movie_id", columnDefinition = "bigint", unique = true, nullable = false)
    private Long id;

    @Column(name = "name", columnDefinition="varchar(255)", nullable=true)
    private String name;

    @OneToMany(mappedBy="movie", cascade = CascadeType.ALL, fetch=FetchType.EAGER)
    @JsonManagedReference
    private List<MovieActor> MovieActors;
}

public class MovieActor implements Serializable {
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name = "movie_actor_id", columnDefinition = "bigint", unique = true, nullable = false)
    private Long id;

    @ManyToOne()
    @JoinColumn(name="movie_id")
    @JsonBackReference
    private Movie movie;

    @ManyToOne()
    @JoinColumn(name="actor_id")
    @JsonBackReference
    private Actor actor;
}

Here is my Service method for update.

 public String updateMovie(String movieData) throws JsonProcessingException {
        
        try {
            JSONObject movieObj = new JSONObject(movieData);
            
            
            // Check if movie exist
            Optional<Movie> movieOpt = movieRepository
                    .findById(movieObj.getLong("id"));
            if (movieOpt.isEmpty())
                return "Movie does not exist";
            
            Movie movie = movieOpt.get();
            List<MovieActor> movieActors = new ArrayList<MovieActor>();
            // Check if actors exist
            JSONArray actorIdArray = movieObj.getJSONArray("actor_ids");
            for (int i = 0; i < actorIdArray.length(); i++) {
                Optional<Actor> actorOpt = actorRepository
                        .findById(actorIdArray.getLong(i));
                if (actorOpt.isEmpty())
                    return "actor doesn't exist";
                
                // Save MovieActor
                MovieActor movieActor = new MovieActor();                
                movieActor.setMovie(movie);
                movieActor.setActor(actorOpt.get());
                movieActors.add(movieActor);    
            }
  
            movie.setMovieActors(movieActors);
            
            Movie updatedMovie = movieRepository.saveAndFlush(movie);
            
            return CustomResponse(HttpStatus.ACCEPTED.value(),updatedMovie);

        } catch (Exception e) {
            log.error("Exception", e);
            return CustomResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
        }
    }
    

I have tried using

movieActorRepository.deleteAll(movie.getMovieActors());

before saving the new ones. And it still doesn't work. The old rows are still there in the database.


Solution

  • JPA has no indication from how you've mapped MovieActor that it isn't a completely independent entity that you've made some relationship to. This is different from putting in a Join table mapping, linking Movie directly to Actor:

    @ManyToMany 
    @JoinTable(
         name = "movie_actor",
         joinColumns = @JoinColumn(name = "movie_id"),
         inverseJoinColumns = @JoinColumn(name = "actor_id") ) 
    private List<Actor> actors;
    

    Such a mapping makes it obvious to JPA that entries in the actors list require it to add or remove rows from the movie_actor table. This is common in JPA tutorials and maybe why you are expecting the behavior you are.

    You instead mapped the table as a separate entity. There are many reasons to have a MovieActor entity, such as being able to store the actor's role in the movie. I'd be careful treating this then as a join table in the same sense, but you can still get that automatic cleanup by marking the relationship with "orphanRemoval=true". This will cause JPA to call em.remove on the entities when they are de-referenced from the relationship.

    @OneToMany(mappedBy="movie", cascade = CascadeType.ALL, orphanRemoval=true, fetch=FetchType.EAGER)
    @JsonManagedReference
    private List<MovieActor> MovieActors;
    

    You then still need to 'handle' the existing list of MovieActors better. Just creating a new list of MovieActor instances is wasteful for entities - you are completely deleting the previous list and creating new ones even when nothing has changed. Since MovieActor has an ID, you should check if what you are being passed has one too, and merge that instead. That is the point of JPA's merge after all, allowing you just to pass in the Movie object as is:

    Movie movie = movieOpt.get();
    Movie updatedMovie = movieRepository.saveAndFlush(movie);
    

    JPA will cascade the merge from movie to MovieActors and create them if neccessary - and throw exceptions if any reference Actors that don't exist in the DB because you don't have the cascade operation set on the MovieActor.actor relationship - which seems to be what you want.