Как Spring DataJpa работает со связанными таблицами?
Есть две сущности со связью ManyToMany: Student и Course. У студента может быть несколько курсов, а один курс может проходить несколько студентов. Эту связь я отображаю в такой таблице:
CREATE TABLE students_courses
(
student_id INTEGER NOT NULL,
course_id INTEGER NOT NULL,
CONSTRAINT student_course_idx UNIQUE (student_id, course_id),
FOREIGN KEY (student_id) REFERENCES students (id) ON DELETE CASCADE,
FOREIGN KEY (course_id) REFERENCES courses (id) ON DELETE CASCADE
);
Сущность Student:
@Entity
@Table(name = "students")
public class Student {
@ManyToMany
@JoinTable(
name = "students_courses",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
List<Course> courses;
// другие поля
}
Сущность Course:
@Entity
@Table(name = "courses")
public class Course {
@ManyToMany(mappedBy = "courses")
List<Student> students;
// другие поля
}
Работа с базой данных идёт через Spring DataJpa. Я использую имплементацию JpaRepository, которая принимает сущности и автоматически обновляет/удаляет/сохраняет. Например, вот репозиторий для курса (для студента такой же, только называется CrudStudentRepository):
@Transactional(readOnly = true)
public interface CrudCourseRepository extends JpaRepository<Course, Integer> {
}
У сущности Student есть поле List<Course> courses. В моём понимании, если попытаться обновить сущность Student, передав в CrudStudentRepository#save() студента с пустым списком курсов, то в таблице student_courses будут удалены все записи, в которых фигурирует обновляемый студент, и это так.
Однако, когда я обновляю сущность Course, у которой свою очередь есть поле List<Student> students, то как бы я не менял содержимое списка students перед сохранением сущности в базу, эти изменения никак не отображаются на таблице student_courses. Как мне тогда их удалять/обновлять?
По какому принципу JpaRepository обновляет/удаляет/добавляет записи в таблице связей?
Ответы (1 шт):
Всё кроется в аннотации @JoinTable
Итак, есть две сущности Student и Course, связанные отношением ManyToMany. Это отношение отображается в таблице students_courses. И ещё сущность Student содержит список курсов, а сущность Course, соответственно, содержит список студентов. Так почему же список курсов в Student может влиять на записи students_courses, а список студентов в Course никак на неё них не влияет?
Причина кроется в аннотации @JoinTable. А именно - в её расположении.
Несмотря на то, что отношение ManyToMany в моём понимании предполагает равенство сущностей, в реальности это не совсем так. Сущность, в которой список аннотирован @JoinTable является ведущей. То есть именно она определяет записи в таблице связей (таблица students_courses в нашем случае), а связанная сущность никак на эту таблицу не влияет.
На Baeldung владелец аннотации определяется как "владелец отношения". А вот что написано в официальной документации в @JoinTable:
When a join table is used in mapping a relationship with an embeddable class on the owning side of the relationship, the containing entity rather than the embeddable class is considered the owner of the relationship.
перевод:
Когда объединяемая таблица используется для сопоставления отношения с встраиваемым классом на стороне-владельце отношения, владельцем отношения считается содержащая сущность, а не встраиваемый класс.
Из чего я также делаю вывод о том, что мой ответ верен.
Может возникануть вопрос: а что, если мы хотим управлять записями в таблице связей со стороны обеих сущностей?
Самым очевидным решением для меня было расположить @JoinTable в обеих сущностях, и это действительно сработало. Я не знаю, насколько это корректное решение, потому что есть и другие варианты (на том же Baeldung), но это действительно работает.
Таким образом решение конкретно моей проблемы следующее. Если я хочу, чтобы обновление сущности Course влияло на таблицу связей, я просто должен сделать вот так:
Student:
@Entity
@Table(name = "students")
public class Student {
@ManyToMany(mappedBy = "students")
List<Course> courses;
// другие поля
}
Course:
@Entity
@Table(name = "courses")
public class Course {
@ManyToMany
@JoinTable(
name = "students_courses",
joinColumns = @JoinColumn(name = "course_id"),
inverseJoinColumns = @JoinColumn(name = "student_id")
)
List<Student> students;
// другие поля
}
А если необходимо, чтобы на таблицу связей влияли обе сущности, то лучше рассмотреть другой вариант, например вот этот.