← Назад к вопросам

Как реализуется связь Many-to-Many в БД?

1.0 Junior🔥 181 комментариев
#ORM и Hibernate#Базы данных и SQL

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Связь Many-to-Many в базе данных

Many-to-Many (Много-ко-Многим) — это одна из самых распространённых связей в реляционных БД. Например, студент может посещать несколько курсов, а каждый курс может иметь много студентов.

Основная идея

Many-to-Many связь реализуется через промежуточную таблицу (junction table или bridge table), которая содержит внешние ключи обеих сторон.

Пример: Студенты и Курсы

Таблица students:

CREATE TABLE students (
    id UUID PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) UNIQUE,
    enrollment_date TIMESTAMP
);

Таблица courses:

CREATE TABLE courses (
    id UUID PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    description TEXT,
    credits INT,
    created_at TIMESTAMP
);

Таблица student_course (промежуточная):

CREATE TABLE student_course (
    student_id UUID NOT NULL,
    course_id UUID NOT NULL,
    enrollment_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    grade VARCHAR(2),  -- Оценка студента
    PRIMARY KEY (student_id, course_id),
    FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE,
    FOREIGN KEY (course_id) REFERENCES courses(id) ON DELETE CASCADE
);

Как это работает

1. Добавление связи Когда студент записывается на курс:

INSERT INTO student_course (student_id, course_id, enrollment_date)
VALUES ('uuid-student-1', 'uuid-course-1', NOW());

2. Запрос: Найти все курсы студента

SELECT c.*
FROM courses c
JOIN student_course sc ON c.id = sc.course_id
WHERE sc.student_id = 'uuid-student-1';

3. Запрос: Найти всех студентов на курсе

SELECT s.*
FROM students s
JOIN student_course sc ON s.id = sc.student_id
WHERE sc.course_id = 'uuid-course-1';

4. Удаление связи Когда студент отписывается от курса:

DELETE FROM student_course
WHERE student_id = 'uuid-student-1' AND course_id = 'uuid-course-1';

Реализация на Hibernate/JPA

Сторона владельца (Student):

@Entity
@Table(name = "students")
public class Student {
    @Id
    private UUID id;
    
    private String name;
    private String email;
    
    @ManyToMany
    @JoinTable(
        name = "student_course",
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id")
    )
    private Set<Course> courses = new HashSet<>();
    
    // Конструктор и методы
    public void enrollCourse(Course course) {
        courses.add(course);
        course.getStudents().add(this);
    }
    
    public void unenrollCourse(Course course) {
        courses.remove(course);
        course.getStudents().remove(this);
    }
}

Сторона обратной ссылки (Course):

@Entity
@Table(name = "courses")
public class Course {
    @Id
    private UUID id;
    
    private String title;
    private String description;
    private Integer credits;
    
    @ManyToMany(mappedBy = "courses")
    private Set<Student> students = new HashSet<>();
}

Промежуточная таблица с дополнительными атрибутами

Когда нужно хранить дополнительные данные (например, оценку студента), создаём отдельную entity:

Класс для связи с атрибутами:

@Entity
@Table(name = "student_course")
public class StudentCourse {
    @EmbeddedId
    private StudentCourseId id;
    
    @ManyToOne
    @MapsId("studentId")
    @JoinColumn(name = "student_id")
    private Student student;
    
    @ManyToOne
    @MapsId("courseId")
    @JoinColumn(name = "course_id")
    private Course course;
    
    private LocalDateTime enrollmentDate;
    private String grade;  // Оценка
    private Double attendancePercentage;  // Посещаемость
}

@Embeddable
public class StudentCourseId implements Serializable {
    @Column(name = "student_id")
    private UUID studentId;
    
    @Column(name = "course_id")
    private UUID courseId;
    
    // equals() и hashCode()
}

Обновленные Student и Course:

@Entity
public class Student {
    @Id
    private UUID id;
    
    private String name;
    
    @OneToMany(mappedBy = "student", cascade = CascadeType.ALL)
    private Set<StudentCourse> enrollments = new HashSet<>();
}

@Entity
public class Course {
    @Id
    private UUID id;
    
    private String title;
    
    @OneToMany(mappedBy = "course", cascade = CascadeType.ALL)
    private Set<StudentCourse> enrollments = new HashSet<>();
}

Типы связей в Many-to-Many

1. Двусторонняя связь (Bidirectional) Обе стороны знают друг о друге:

Student student = studentRepository.findById(id).get();
student.getCourses()  // Получить все курсы студента

Course course = courseRepository.findById(id).get();
course.getStudents()  // Получить всех студентов курса

2. Односторонняя связь (Unidirectional) Только одна сторона знает о другой:

@Entity
public class Student {
    @ManyToMany
    @JoinTable(...)
    private Set<Course> courses;
}

@Entity
public class Course {
    // Не знает о Student
}

Запросы через JPQL

Найти студентов конкретного курса:

@Repository
public interface CourseRepository extends JpaRepository<Course, UUID> {
    @Query("SELECT c.students FROM Course c WHERE c.id = :courseId")
    Set<Student> findStudentsByCourse(@Param("courseId") UUID courseId);
}

Найти курсы студента:

@Repository
public interface StudentRepository extends JpaRepository<Student, UUID> {
    @Query("SELECT s.courses FROM Student s WHERE s.id = :studentId")
    Set<Course> findCoursesByStudent(@Param("studentId") UUID studentId);
}

Сложный запрос с фильтрацией:

@Query("""
    SELECT DISTINCT c
    FROM Course c
    JOIN c.students s
    WHERE s.id = :studentId AND c.credits >= :minCredits
""")
List<Course> findCoursesByStudentAndMinCredits(
    @Param("studentId") UUID studentId,
    @Param("minCredits") Integer minCredits
);

Операции с Many-to-Many связями

Добавление студента на курс:

Student student = studentRepository.findById(studentId).get();
Course course = courseRepository.findById(courseId).get();

student.getCourses().add(course);
course.getStudents().add(student);

studentRepository.save(student);

Удаление студента с курса:

student.getCourses().remove(course);
course.getStudents().remove(student);
studentRepository.save(student);

Удаление всех связей студента:

student.getCourses().clear();
studentRepository.save(student);

Производительность

N+1 проблема в Many-to-Many:

// Плохо - N+1 запросов
List<Student> students = studentRepository.findAll();
for (Student s : students) {
    // Каждый вызов вызывает отдельный запрос к courses
    Set<Course> courses = s.getCourses();
}

// Хорошо - JOIN FETCH
@Query("SELECT DISTINCT s FROM Student s LEFT JOIN FETCH s.courses")
List<Student> findAllWithCourses();

// Или через @EntityGraph
@EntityGraph(attributePaths = "courses")
@Query("SELECT s FROM Student s")
List<Student> findAllWithCourses();

Примеры других Many-to-Many связей

Книги и Авторы:

CREATE TABLE books (
    id UUID PRIMARY KEY,
    title VARCHAR(255)
);

CREATE TABLE authors (
    id UUID PRIMARY KEY,
    name VARCHAR(255)
);

CREATE TABLE book_author (
    book_id UUID,
    author_id UUID,
    PRIMARY KEY (book_id, author_id),
    FOREIGN KEY (book_id) REFERENCES books(id),
    FOREIGN KEY (author_id) REFERENCES authors(id)
);

Тегов и Статей:

CREATE TABLE articles (
    id UUID PRIMARY KEY,
    title VARCHAR(255)
);

CREATE TABLE tags (
    id UUID PRIMARY KEY,
    name VARCHAR(50) UNIQUE
);

CREATE TABLE article_tag (
    article_id UUID,
    tag_id UUID,
    PRIMARY KEY (article_id, tag_id),
    FOREIGN KEY (article_id) REFERENCES articles(id) ON DELETE CASCADE,
    FOREIGN KEY (tag_id) REFERENCES tags(id)
);

Резюме

Many-to-Many связь:

  • Реализуется через промежуточную таблицу (junction table)
  • Промежуточная таблица содержит внешние ключи обеих сторон
  • Может быть одно- или двусторонней
  • Может содержать дополнительные атрибуты (оценка, дата и т.д.)
  • Требует особого внимания к производительности (N+1 проблема)
  • В Java/Hibernate используется @ManyToMany или @OneToMany с промежуточной entity