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

Как связаны сущности "Преподаватель" и "Студент" в реляционной базе данных?

2.0 Middle🔥 111 комментариев
#ORM и Hibernate#Базы данных и SQL

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

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

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

Ответ

Сущности "Преподаватель" и "Студент" связаны отношением «многие-ко-многим» (Many-to-Many). Один преподаватель может учить множество студентов, а один студент может учиться у множества преподавателей. Эта связь требует промежуточной (junction) таблицы.

1. Диаграмма отношений

┌─────────────────┐         ┌──────────────────┐         ┌──────────────┐
│  Преподаватель  │────────|│  Преподаёт       │────────|│  Студент     │
│  (Teacher)      │        └──────────────────┘        │  (Student)   │
│                 │                                      │              │
│ id (PK)         │        teacher_id (FK)              │ id (PK)      │
│ name            │        student_id (FK)              │ name         │
│ email           │                                      │ email        │
│ department      │                                      │ group        │
└─────────────────┘                                      └──────────────┘

2. SQL схема Many-to-Many

-- Таблица преподавателей
CREATE TABLE teachers (
    id UUID PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    department VARCHAR(100),
    hire_date DATE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Таблица студентов
CREATE TABLE students (
    id UUID PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    group_number VARCHAR(20),
    enrollment_date DATE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Промежуточная таблица (junction table)
CREATE TABLE teacher_student (
    teacher_id UUID NOT NULL,
    student_id UUID NOT NULL,
    subject VARCHAR(100),  -- Какой предмет преподаётся
    semester INT,           -- В каком семестре
    grade VARCHAR(2),       -- Оценка студента
    PRIMARY KEY (teacher_id, student_id, subject),
    FOREIGN KEY (teacher_id) REFERENCES teachers(id) ON DELETE CASCADE,
    FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE
);

-- Индексы для производительности
CREATE INDEX idx_teacher_student_teacher ON teacher_student(teacher_id);
CREATE INDEX idx_teacher_student_student ON teacher_student(student_id);

3. JPA/Hibernate сущности

Вариант 1: С промежуточной таблицей без entity

@Entity
@Table(name = "teachers")
public class Teacher {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private String id;
    
    @Column(name = "name", nullable = false)
    private String name;
    
    @Column(name = "email", unique = true, nullable = false)
    private String email;
    
    @Column(name = "department")
    private String department;
    
    // Many-to-Many через промежуточную таблицу
    @ManyToMany(cascade = CascadeType.ALL)
    @JoinTable(
        name = "teacher_student",
        joinColumns = @JoinColumn(name = "teacher_id"),
        inverseJoinColumns = @JoinColumn(name = "student_id")
    )
    private List<Student> students = new ArrayList<>();
    
    public void addStudent(Student student) {
        students.add(student);
        student.getTeachers().add(this);
    }
}

@Entity
@Table(name = "students")
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private String id;
    
    @Column(name = "name", nullable = false)
    private String name;
    
    @Column(name = "email", unique = true, nullable = false)
    private String email;
    
    @Column(name = "group_number")
    private String groupNumber;
    
    // Many-to-Many обратное
    @ManyToMany(mappedBy = "students")
    private List<Teacher> teachers = new ArrayList<>();
}

Вариант 2: С промежуточной entity (рекомендуется)

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

@Entity
@Table(name = "teachers")
public class Teacher {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private String id;
    
    @Column(name = "name", nullable = false)
    private String name;
    
    @Column(name = "email", unique = true, nullable = false)
    private String email;
    
    @OneToMany(mappedBy = "teacher", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<TeacherStudentRelation> studentRelations = new ArrayList<>();
}

@Entity
@Table(name = "students")
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private String id;
    
    @Column(name = "name", nullable = false)
    private String name;
    
    @Column(name = "email", unique = true, nullable = false)
    private String email;
    
    @OneToMany(mappedBy = "student", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<TeacherStudentRelation> teacherRelations = new ArrayList<>();
}

// Промежуточная entity
@Entity
@Table(name = "teacher_student")
public class TeacherStudentRelation {
    @EmbeddedId
    private TeacherStudentId id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("teacherId")
    @JoinColumn(name = "teacher_id")
    private Teacher teacher;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("studentId")
    @JoinColumn(name = "student_id")
    private Student student;
    
    @Column(name = "subject")
    private String subject;
    
    @Column(name = "semester")
    private Integer semester;
    
    @Column(name = "grade")
    private String grade;
    
    @Column(name = "created_at")
    private LocalDateTime createdAt = LocalDateTime.now(UTC);
}

@Embeddable
public class TeacherStudentId implements Serializable {
    @Column(name = "teacher_id")
    private String teacherId;
    
    @Column(name = "student_id")
    private String studentId;
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof TeacherStudentId)) return false;
        TeacherStudentId that = (TeacherStudentId) o;
        return Objects.equals(teacherId, that.teacherId) &&
               Objects.equals(studentId, that.studentId);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(teacherId, studentId);
    }
}

4. Операции с отношением Many-to-Many

@Service
public class TeacherStudentService {
    @Autowired
    private TeacherRepository teacherRepository;
    
    @Autowired
    private StudentRepository studentRepository;
    
    // Назначить студента преподавателю
    @Transactional
    public void assignStudentToTeacher(String teacherId, String studentId) {
        Teacher teacher = teacherRepository.findById(teacherId).orElseThrow();
        Student student = studentRepository.findById(studentId).orElseThrow();
        
        teacher.addStudent(student);
        teacherRepository.save(teacher);
    }
    
    // Получить всех студентов преподавателя
    @Transactional(readOnly = true)
    public List<Student> getStudentsByTeacher(String teacherId) {
        Teacher teacher = teacherRepository.findById(teacherId).orElseThrow();
        return teacher.getStudents();
    }
    
    // Получить всех преподавателей студента
    @Transactional(readOnly = true)
    public List<Teacher> getTeachersByStudent(String studentId) {
        Student student = studentRepository.findById(studentId).orElseThrow();
        return student.getTeachers();
    }
    
    // Удалить студента из класса преподавателя
    @Transactional
    public void removeStudentFromTeacher(String teacherId, String studentId) {
        Teacher teacher = teacherRepository.findById(teacherId).orElseThrow();
        Student student = studentRepository.findById(studentId).orElseThrow();
        
        teacher.getStudents().remove(student);
        student.getTeachers().remove(teacher);
        teacherRepository.save(teacher);
    }
}

5. Запросы к базе данных

-- Найти всех студентов преподавателя с id = 1
SELECT s.* FROM students s
JOIN teacher_student ts ON s.id = ts.student_id
WHERE ts.teacher_id = "uuid_teacher_1";

-- Найти всех преподавателей студента
SELECT t.* FROM teachers t
JOIN teacher_student ts ON t.id = ts.teacher_id
WHERE ts.student_id = "uuid_student_1";

-- Найти преподавателей, учащих определённый предмет
SELECT DISTINCT t.* FROM teachers t
JOIN teacher_student ts ON t.id = ts.teacher_id
WHERE ts.subject = "Mathematics";

-- Найти средний балл студента
SELECT AVG(ts.grade) FROM teacher_student ts
WHERE ts.student_id = "uuid_student_1";

-- Найти преподавателей с наибольшим количеством студентов
SELECT t.id, t.name, COUNT(ts.student_id) as student_count
FROM teachers t
LEFT JOIN teacher_student ts ON t.id = ts.teacher_id
GROUP BY t.id, t.name
ORDER BY student_count DESC;

6. REST API пример

@RestController
@RequestMapping("/api/teachers")
public class TeacherController {
    @Autowired
    private TeacherStudentService service;
    
    @GetMapping("/{id}/students")
    public List<StudentDTO> getTeacherStudents(@PathVariable String id) {
        return service.getStudentsByTeacher(id);
    }
    
    @PostMapping("/{teacherId}/students/{studentId}")
    public void assignStudent(@PathVariable String teacherId,
                             @PathVariable String studentId) {
        service.assignStudentToTeacher(teacherId, studentId);
    }
    
    @DeleteMapping("/{teacherId}/students/{studentId}")
    public void removeStudent(@PathVariable String teacherId,
                             @PathVariable String studentId) {
        service.removeStudentFromTeacher(teacherId, studentId);
    }
}

Ключевые моменты

  1. Many-to-Many требует промежуточной таблицы — в реляционных БД нет прямого типа для этого
  2. Двусторонность — обе стороны должны быть синхронизированы
  3. Промежуточная entity — нужна, если есть атрибуты отношения (subject, grade, semester)
  4. Cascade и orphanRemoval — аккуратнее, можно потерять данные
  5. Индексы на внешние ключи — для производительности JOIN запросов

Это стандартная архитектура для учебных систем, социальных сетей, и любых систем с many-to-many отношениями.