Search code examples
javahibernatejpamany-to-many

How to update link table with hibernate


I'm learning about hibernate and i wanted to try and experiment with relations. ManyToMany in particular is rather troublesome for me. Hibernate generates db schema for me. It creates movies and actors entities properly, but it's not updating link table (actors_movies). I'm using h2 database.

Actor

package Entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Cascade;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "actors")
@EqualsAndHashCode
public class Actor {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @GenericGenerator(
            name = "UUID",
            strategy = "org.hibernate.id.UUIDGenerator")
    @Column(name = "actor_id", columnDefinition = "VARCHAR(255)")
    private UUID id;
    @Column
    private String name;
    @Column(name = "last_name")
    private String lastName;
    @Column(name = "year_of_birth")
    private int yearOfBirth;
    @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinTable(
            name = "actors_movies",
            joinColumns = { @JoinColumn(name = "actor_id") },
            inverseJoinColumns = { @JoinColumn(name = "movie_id") }
    )
    //@Cascade(org.hibernate.annotations.CascadeType.ALL)
    private List<Movie> movies = new ArrayList<>();


}

Movie

package Entity;

        import lombok.AllArgsConstructor;
        import lombok.Data;
        import lombok.EqualsAndHashCode;
        import lombok.NoArgsConstructor;
        import org.hibernate.annotations.Cascade;
        import org.hibernate.annotations.GenericGenerator;

        import javax.persistence.*;
        import java.util.ArrayList;
        import java.util.List;
        import java.util.UUID;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "movies")
@EqualsAndHashCode
public class Movie {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @GenericGenerator(
            name = "UUID",
            strategy = "org.hibernate.id.UUIDGenerator")
    @Column(name = "movie_id", columnDefinition = "VARCHAR(255)")
    private UUID id;
    @Column
    private String title;
    @Column
    private int yearOfRelease;
    @Column
    private int genre;
    @ManyToMany(mappedBy = "movies",fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    //@Cascade(org.hibernate.annotations.CascadeType.ALL)
    private List<Actor> actors = new ArrayList<>();
}

Main

import Connection.HibernateUtil;
import Entity.Actor;
import Entity.Movie;
import org.hibernate.Session;
import org.hibernate.Transaction;

import java.util.List;

public class CinemaTest {
    public static void main(String[] args) {

        Actor actor = new Actor();
        actor.setLastName("aktorrrr");
        actor.setYearOfBirth(8837);

        Movie movie1 = new Movie();
        Movie movie2 = new Movie();
        movie1.setTitle("jakisTytul");
        movie2.setTitle("ffggggggggggggggggfgfg");

        actor.setMovies(List.of(movie1, movie2));


        Transaction transaction = null;
        try (Session session = HibernateUtil.getSessionFactory().openSession()) {
            transaction = session.beginTransaction();

            session.save(movie1);
            session.save(movie2);
            session.save(actor);

            transaction.commit();
        } catch (Exception e) {
            if (transaction != null) {
                transaction.rollback();
            }
            e.printStackTrace();
        }

    }
}

Solution

  • When you use bidirectional @ManyToMany association you should make both sides in-sync.

    So, I would suggest you to correct your mapping in this way:

    @Entity
    public class Actor {
        // ...
    
        @ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE})
        @JoinTable(
                name = "actors_movies",
                joinColumns = { @JoinColumn(name = "actor_id") },
                inverseJoinColumns = { @JoinColumn(name = "movie_id") }
        )
        private List<Movie> movies = new ArrayList<>();
    
        public void addMovie(Movie movie) {
           movies.add( movie );
           movie.getActors().add( this );
        }
    
        public void removeMovie(Movie movie) {
            movies.remove( movie );
            movie.getActors().remove( this );
        }
    }
    
    @Entity
    public class Movie {
    
        // ...
    
        @ManyToMany(mappedBy = "movies", fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE})
        private List<Actor> actors = new ArrayList<>();
    }
    

    and then you can do something like this:

    Actor actor = new Actor();
    // ...
    
    Movie movie1 = new Movie();
    Movie movie2 = new Movie();
    // ...
    
    actor.addMovie(movie1);
    actor.addMovie(movie2);
    
    
    // ...
    try (Session session = HibernateUtil.getSessionFactory().openSession()) {
       transaction = session.beginTransaction();
    
       // due to cascade = {CascadeType.PERSIST, CascadeType.MERGE}
       // movies will be saved as well
       session.save(actor);
    
       transaction.commit();
    }
    

    P.S.

    1. For @ManyToMany associations, the REMOVE entity state transition doesn’t make sense to be cascaded because it will propagate beyond the link table. Since the other side might be referenced by other entities on the parent-side, the automatic removal might end up in a ConstraintViolationException. That is why it's not recommended to use cascade = CascadeType.ALL with @ManyToMany associations.

    2. Look also at this article. This is not a good idea to use lombok @Data annotation with @Entity.