← Назад к вопросам
Как связаны сущности "Преподаватель" и "Студент" в реляционной базе данных?
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);
}
}
Ключевые моменты
- Many-to-Many требует промежуточной таблицы — в реляционных БД нет прямого типа для этого
- Двусторонность — обе стороны должны быть синхронизированы
- Промежуточная entity — нужна, если есть атрибуты отношения (subject, grade, semester)
- Cascade и orphanRemoval — аккуратнее, можно потерять данные
- Индексы на внешние ключи — для производительности JOIN запросов
Это стандартная архитектура для учебных систем, социальных сетей, и любых систем с many-to-many отношениями.