Recursive entity loading in Java entity class using NamedEntityGraph

Hello there. I have a many to many relationship that is as follows: MotionPicture → MotionPictureEmployee → Employee. I have all the relationship fields set to lazily loaded.

My goal is to use NamedEntityGraph annotations to choose when the relationship field in MotionPicture should be loaded eagerly.

@Entity
@Table(name = "motion_pictures")
@NamedEntityGraph(name = "motion_picture.employees", attributeNodes = @NamedAttributeNode("employees"))
public class MotionPicture {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) 
	private Integer id;
        ...
        ...
        ...
	@OneToMany(
			mappedBy = "motionPicture",
			orphanRemoval = true,
			fetch = FetchType.LAZY		
		)
	List<MotionPictureEmployee> employees = new ArrayList<>();

Using the NamedEntityGraph…

		Session session = sessionFactory.openSession();
    	SelectionQuery<MotionPicture> motionPictureQuery = session.createSelectionQuery("select m from MotionPicture m where m.originalTitle = :motionPictureOriginalTitle"
    			+  " and m.releaseDate = :releaseDate", MotionPicture.class).setParameter("motionPictureOriginalTitle", title)
    			.setParameter("releaseDate", releaseDate);
    	if (getEmployees) {
    		motionPictureQuery.setHint("jakarta.persistence.fetchgraph", session.getEntityGraph("motion_picture.employees"));
    	}
    	MotionPicture motionPicture = motionPictureQuery.getSingleResultOrNull();
    	session.close();

Note that session is closed. In my test I get this result:

		MotionPicture alien = this.motionPictureService.getMotionPicture("Alien", LocalDate.of(1979, 6, 22), true);
		// we expect the employees to not be present because they are lazily loaded and we didn't ask for them
		assertTrue(Hibernate.isInitialized(alien.getEmployees()));
		assertTrue(Hibernate.isInitialized(alien.getEmployees().getFirst()));
		assertTrue(Hibernate.isInitialized(alien.getEmployees().getFirst().getMotionPicture().getEmployees()
				                                               .getFirst().getMotionPicture().getEmployees()
				                                               .getLast().getMotionPicture().getEmployees()
				                                               .getFirst().getMotionPicture().getEmployees()));

Why are the relationships getting recursively set, motionPicture ->motionPictureEmployees->motionPicture->motionPictureEmployees?

This doesn’t happen when going from motionPicture->motionPictureEmployees->employee. Obviously the difference here being that Employee doesn’t have a namedEntityGraph like motionPicture.

For reference:
MotionPictureEmployee class, which represents the “junction” table in my database.

@Entity
@Table(name = "motion_pictures_employees")
public class MotionPictureEmployee {
	@Id
	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "motion_picture_id")
	private MotionPicture motionPicture;
	@Id
	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "employee_id")
	private Employee employee;
	public MotionPictureEmployee() {	
	}

Employee

@Entity	
@Table(name = "employees")
@NamedEntityGraph(name = "employee.motionPictures", attributeNodes = @NamedAttributeNode("motionPictures"))
public class Employee {
	@Column(name = "birth_date")		
	private LocalDate birthDate;
	@Column(name = "country_of_origin")
	private String countryOfOrigin;
	@Column(name = "date_of_death")
	private LocalDate dateOfDeath;
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) 
	private Integer id;
	@Column(name = "img_location")
	private String imgLocation;
	private String name;
	private String overview;	
	@Column(name = "popularity_ranking")
	private Integer popularityRanking;
	@OneToMany(
		mappedBy = "employee",
		orphanRemoval = true,				
		fetch = FetchType.LAZY
	)
	List<MotionPictureEmployee> motionPictures = new ArrayList<>();

I’m not sure what you’re asking here exactly, but within a persistence context, there is at most one entity object for every row in the database (not counting potential proxies).
This means that the MotionPictureEmployee objects within a MotionPicture#employees obviously all point to the same MotionPicture object.

Not sure what you mean there exactly. Since Employee was not loaded, you will see a proxy object instead for MotionPictureEmployee#employee, which will delegate to the actual entity once it is lazy loaded, but that actual entity object should never be observable through the object graph, because the proxy object already took the spot in the persistence context.

Thank you for the response. There is a key point you made: within a persistence context, there is at most one entity object for every row in the database (not counting potential proxies). What I thought was happening was that new objects (The MotionPicture object) were being created and that there was this endless cycle happening where we recursively dig deeper and deeper. But it’s not recursively digging, the behavior I observed was that I had a circular reference. This test passes:

		MotionPicture motionPicture0 = alien.getEmployees().get(0).getMotionPicture();
		MotionPicture motionPicture1 = alien.getEmployees().get(1).getMotionPicture()
				                            .getEmployees().get(2).getMotionPicture();
		MotionPicture motionPicture2 = alien.getEmployees().get(3).getMotionPicture()
				                            .getEmployees().get(4).getMotionPicture()
				                            .getEmployees().get(5).getMotionPicture();
		MotionPicture motionPicture3 = alien.getEmployees().get(6).getMotionPicture()
				                            .getEmployees().get(7).getMotionPicture()
				                            .getEmployees().get(8).getMotionPicture()
				                            .getEmployees().get(9).getMotionPicture();

		assertTrue(motionPicture0 == motionPicture1);
		assertTrue(motionPicture1 == motionPicture2);
		assertTrue(motionPicture2 == motionPicture3);

Can you expand on this please, what do you mean by the actual entity object should never be observable through the object graph?

Since the persistence context only has one object per row, instantiating a proxy for that row will take this spot. This means that if you load another object from the database that refers to this row through an association within that persistence context, it will refer to the proxy object. The actual entity behind this proxy object will be instantiated once you access the proxy the first time and every operation on the proxy delegates to that instance. This process is called lazy loading. Usually, this actual entity can not be accessed directly. If you want access to it, you have to use e.g. Hibernate.unproxy(), but adding the actual entity instance into the object graph is going to cause trouble, so it’s better not to do that.