Hibernate ManyToOne Deletes with Two Parents

Good afternoon everyone, I am using Spring Data JPA with Hibernate 6.6.8. I’m working on a project for course management, and as part of this we’ve set up a database with a fairly large number of tables and entities. Unfortunately, by now I think I am definitely in over my head. As part of a course, we have a set of teams and a set of students in the course. These are mapped together by a TeamMember.

Team.java
@Entity
@Table(name = "team", uniqueConstraints = @UniqueConstraint(columnNames = {"name", "course_id"}))
public class Team {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @Column(nullable = false)
  @NotBlank
  private String name;

  @ManyToOne
  @JoinColumn(name = "course_id")
  @JsonIgnore
  @ToString.Exclude
  private Course course;

  @OneToMany(cascade = CascadeType.ALL, mappedBy = "team")
  @Fetch(FetchMode.JOIN)
  private List<TeamMember> teamMembers;

  @Column(nullable = true)
  private Integer githubTeamId;

  private Integer canvasId;
}
RosterStudent.java
@Entity
@Table(
    uniqueConstraints = {
      @UniqueConstraint(
          name = "UK_ROSTER_STUDENT_COURSE_STUDENT",
          columnNames = {"course_id", "student_id"}),
      @UniqueConstraint(
          name = "UK_ROSTER_STUDENT_COURSE_EMAIL",
          columnNames = {"course_id", "email"})
    })
public class RosterStudent {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @ManyToOne
  @JoinColumn(name = "course_id")
  private Course course;

  private String studentId;
  private String firstName;
  private String lastName;
  private String email;
  @Builder.Default private String section = "";

  @ManyToOne
  @JoinColumn(name = "user_id")
  @ToString.Exclude
  private User user;

  @OneToMany(cascade = CascadeType.ALL, mappedBy = "rosterStudent")
  @Fetch(FetchMode.JOIN)
  @JsonIgnore
  @ToString.Exclude
  private List<TeamMember> teamMembers;

  @Enumerated(EnumType.STRING)
  private RosterStatus rosterStatus;

  @Enumerated(EnumType.STRING)
  private OrgStatus orgStatus;

  private Integer githubId;
  private String githubLogin;

  public List<String> getTeams() {
    if (teamMembers == null) {
      return List.of();
    } else {
      return teamMembers.stream().map(tm -> tm.getTeam().getName()).toList();
    }
  }
}
TeamMember.java
@Entity
@Table(
    name = "team_member",
    uniqueConstraints = @UniqueConstraint(columnNames = {"team_id", "roster_student_id"}))
public class TeamMember {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @ManyToOne
  @JoinColumn(name = "roster_student_id")
  private RosterStudent rosterStudent;

  @ManyToOne
  @JoinColumn(name = "team_id")
  @JsonIgnore
  @ToString.Exclude
  private Team team;

  @Enumerated(EnumType.STRING)
  @Column(nullable = true)
  private TeamStatus teamStatus;
}

Entities on GitHub (apologies, this is my first post and I’m limited to two links)

Specifically, in this case, we have a job that takes data from Canvas. If Canvas no longer lists these students as members of a team, the TeamMember associated with that is deleted. That is done like this:

Set<TeamMember> removedMembers = finalLinked.getTeamMembers().stream().filter(teamMember -> !processedEmails.contains(teamMember.getRosterStudent().getEmail())).collect(
          Collectors.toSet());
      ctx.log("Group members to be removed:" + removedMembers);
      removedMembers.forEach(teamMember -> {
        teamMember.getTeam().getTeamMembers().remove(teamMember);
        teamMember.getRosterStudent().getTeamMembers().remove(teamMember);
        teamMember.setTeam(null);
        teamMember.setRosterStudent(null);
      });
      teamMemberRepository.deleteAll(removedMembers);

Full code and context on GitHub

However, when doing this, Hibernate will throw an exception:

could not execute statement [NULL not allowed for column "ROSTER_STUDENT_ID"; SQL statement:
update team_member set roster_student_id=?,team_id=?,team_status=? where id=? [23502-232]] [update team_member set roster_student_id=?,team_id=?,team_status=? where id=?]; SQL [update team_member set roster_student_id=?,team_id=?,team_status=? where id=?]; constraint [null]

Notably, we do have rules in the database enforcing that neither foreign key can be null. I figure that Hibernate is attempting to update the TeamMembers by nulling out the associations first, then deleting them. In my mind, therefore, I(/we) must be doing something that is not considered good practice, and I’d love if someone could point out what that would be – I am an undergrad, and I certainly don’t have a large amount of experience with this.

If more information is necessary, I’m happy to answer questions. Alternately, the entire GitHub repo is available above.

Linked is the branch with the problematic code. I did try to look around and see what best practices are – I found a couple of Vlad’s posts and some Baeldung articles, but I’m still struggling. Thanks in advance!

If you’re going to delete the teamMember anyway, there is no need to call setTeam(null) and setRosterStudent(null).
I don’t know what Spring Data JPA does in its deleteAll implementation, but if it runs a HQL/Criteria query, managed entities will first be flushed before executing the query.
Now, with your null setting, you made the entities dirty, so the changes are flushed to the database.

I would recommend that you also tell Hibernate ORM about this not-null constraint by setting it on the @JoinColumn i.e.


  @ManyToOne(optional = false)
  @JoinColumn(name = "roster_student_id", nullable = false)
  private RosterStudent rosterStudent;

  @ManyToOne(optional = false)
  @JoinColumn(name = "team_id", nullable = false)
  @JsonIgnore
  @ToString.Exclude
  private Team team;