Bi-directional many to many with link entity

Hi folks. I am attempting to create a many to many relationship in Hibernate. The model is as follows:

Movie - MovieEmployee - Employee

Movie, movieEmployee, and employee are all tables in my database, where movieEmployee serves as the “join” table. I’m running tests for retrievals of some data that I manually inserted into my database (my “seed” data);

This is how I’m querying for a particular move in my database:

    	Movie returnedMovie = session.createSelectionQuery("select m from Movie m where m.moviePrimaryKey.originalTitle = :movie", Movie.class)
    			.setParameter("movie", "Alien 3")
    			.getSingleResultOrNull();

The associations are lazyily loaded, and when I’m debugger mode I notice that when I look at my

returnedMovie

variable and dig into the associated entity “employees” (MovieEmployee entity) I’ll notice that I can recursively keep going from MovieEmployee to Employee to MovieEmployee to Movie to MovieEmployee to Employee etc. How do I stop this from happening? When I create additional tests I want to control how many “levels” the queries retrieve. For example I’d like to only return the Movie, and MovieEmployees and stop there. I don’t want to see the entire Employe entity in MovieEmployees.

@Entity
@Table(name = "movies")
public class Movie {
	@EmbeddedId
	private MoviePrimaryKey moviePrimaryKey;
	@Column(name = "alternative_title")
	private String alternativeTitle;
	@Column(name = "motion_picture_rating")
	private String motionPictureRating;
	@Column(name = "motion_picture_rating_desc")
	private String motionPictureRatingDesc;
	@Column(name = "running_time")
	private Integer runningTime;
	private String synopsis;
	private String hook;
	private String themes;
	private Integer budget;
	@Column(name = "box_office")
	private Integer boxOffice;
	private Double score;
	@Column(name = "img_location")
	private String imgLocation;
	
	@OneToMany(
			mappedBy = "movie",
			orphanRemoval = true
		)
	List<MovieEmployee> employees = new ArrayList<>();	
	
	public Movie() {
	}
        /** constructors and getter/setters omitted for brevity **/
	public void addEmployee(Employee employee, String jobPosition, String role) {
		MovieEmployee movieEmployee = new MovieEmployee(this, employee, jobPosition, role);
		employees.add(movieEmployee);
		employee.getMovieEmployees().add(movieEmployee);
	}
	
	public void removeEmployee(Employee employee) {
		MovieEmployee movieEmployee = new MovieEmployee(this, employee);
		employees.remove(movieEmployee);
		employee.getMovieEmployees().remove(movieEmployee);

		movieEmployee.setEmployee(null);
		movieEmployee.setMovie(null);
	}

// Movie composite key class
@Embeddable
public class MoviePrimaryKey implements Serializable {

	private static final long serialVersionUID = -1333454840943927255L;
	@Column(name = "original_title")
	private String originalTitle;
	@Column(name = "release_date")
	private LocalDate releaseDate;

Join/reference entity

@Entity
@Table(name = "movies_employees")
public class MovieEmployee {
	
	@Id
	@ManyToOne
	private Movie movie;
	@Id
	@ManyToOne
	private Employee employee;
	@Column(name = "job_position")
	private String jobPosition;
	private String role;
	
	public MovieEmployee() {
	}

Employee entity

@Entity
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property="employeePrimaryKey")
@Table(name = "employees")
public class Employee {
	@EmbeddedId
	private EmployeePrimaryKey employeePrimaryKey;
	@Column(name = "birth_date")
	private LocalDate birthDate;
	@Column(name = "country_of_origin")
	private String countryOfOrigin;
	private String synopsis;
	@Column(name = "popularity_ranking")
	private Integer popularityRanking;
	@Column(name = "img_location")
	private String imgLocation;
	
	@OneToMany(
			mappedBy = "employee",
			orphanRemoval = true,
			cascade = CascadeType.ALL
		)
	List<MovieEmployee> movieEmployees = new ArrayList<>();

@Embeddable
public class EmployeePrimaryKey implements Serializable {
	
	private static final long serialVersionUID = 367980327517397257L;
	@Column
	private String name;
	@Column(name = "identical_name_id")
	private int identicalNameId;

	public EmployeePrimaryKey() {
	}

Hello @blessedmaker, if I understand your need correctly you might want to look at the max_fetch_depth configuration property.

Are you sure? In your mapping for MovieEmployee, both @ManyToOne associations are not defined as FetchType.LAZY. Also, if your query dereferences an association path, this will need to be (inner) joined for the query to work.

Hi there, thank you for your response. I did a little digging into the max_fetch_depth config and it looks like it determines how many associations hibernate will traverse by join when fetching data.
So, hibernate will still recursively retrieve the associations if they are requested, they will just do in a subsequent query.

To reiterate, my problem is an infinite circular reference with a many to many relationship, which is modeled as three entities in Hibernate. Movie, MovieEmployee (“join” table), and Employee. So Movie and Employee each have a one-to-many association to MovieEmployee. In the code, how do I “control” or “prevent” a recursive traversal? Am I even modeling this in a correct (or maybe not correct, but at least not a poor design, or ideal) way?

hi @mbladel bumping this for a response

Hello @blessedmaker, your mappings look fine: this is the “correct” way to map a join-table if you have additional properties in it, the default way of mapping a many-to-many association would be the @ManyToMany annotation with @JoinTable, but as I said what you’re doing is fine.

You can try making the @ManyToOne associations in MovieEmployee lazy, by adding fetch = FetchType.LAZY, which is not the default for to-ones. This should allow to prevent loading the employee as soon as the parent entity is loaded. If it is still loaded, than that might mean you have something accessing that association in your application logic.

1 Like