Что нужно изменить в сущности для работы составного ключа?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Что нужно изменить в сущности для работы составного ключа?
Составной ключ (composite key) — это первичный ключ, состоящий из нескольких столбцов таблицы. В JPA/Hibernate для работы с составными ключами требуется создать отдельный класс и внести специальные изменения в сущность.
Когда нужен составной ключ?
Составной ключ необходим, когда уникальность записи гарантируется комбинацией нескольких полей:
Таблица: student_courses
Уникальность: (student_id, course_id) - один студент не может быть дважды записан на один курс
Шаг 1: Создать класс для составного ключа (@Embeddable)
Класс должен быть помечен аннотацией @Embeddable и реализовывать Serializable:
import javax.persistence.Embeddable;
import java.io.Serializable;
import java.util.Objects;
@Embeddable
public class StudentCourseKey implements Serializable {
private static final long serialVersionUID = 1L;
private Long studentId;
private Long courseId;
// Конструктор по умолчанию ОБЯЗАТЕЛЕН
public StudentCourseKey() {
}
// Конструктор с параметрами
public StudentCourseKey(Long studentId, Long courseId) {
this.studentId = studentId;
this.courseId = courseId;
}
// Getters
public Long getStudentId() {
return studentId;
}
public Long getCourseId() {
return courseId;
}
// ОБЯЗАТЕЛЬНО переопределить equals и hashCode
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
StudentCourseKey that = (StudentCourseKey) o;
return Objects.equals(studentId, that.studentId) &&
Objects.equals(courseId, that.courseId);
}
@Override
public int hashCode() {
return Objects.hash(studentId, courseId);
}
}
Важные правила:
- Класс ключа ДОЛЖЕН быть
Serializable - ДОЛЖНА быть переменная
serialVersionUID - ОБЯЗАТЕЛЕН конструктор без параметров
- ОБЯЗАТЕЛЬНО переопределить
equals()иhashCode()
Шаг 2: Использовать составной ключ в сущности
В сущности нужно использовать @EmbeddedId вместо @Id:
import javax.persistence.*;
@Entity
@Table(name = "student_courses")
public class StudentCourse {
// Составной ключ вместо простого @Id
@EmbeddedId
private StudentCourseKey id;
@ManyToOne
@MapsId("studentId") // Связываем studentId из ключа
@JoinColumn(name = "student_id")
private Student student;
@ManyToOne
@MapsId("courseId") // Связываем courseId из ключа
@JoinColumn(name = "course_id")
private Course course;
// Дополнительные поля
private Double grade;
private String status;
// Конструкторы
public StudentCourse() {
}
public StudentCourse(Student student, Course course) {
this.id = new StudentCourseKey(student.getId(), course.getId());
this.student = student;
this.course = course;
this.status = "ACTIVE";
}
// Getters/Setters
public StudentCourseKey getId() {
return id;
}
public void setId(StudentCourseKey id) {
this.id = id;
}
public Student getStudent() {
return student;
}
public void setStudent(Student student) {
this.student = student;
}
public Course getCourse() {
return course;
}
public void setCourse(Course course) {
this.course = course;
}
public Double getGrade() {
return grade;
}
public void setGrade(Double grade) {
this.grade = grade;
}
}
Соответствующая таблица в БД
CREATE TABLE student_courses (
student_id BIGINT NOT NULL,
course_id BIGINT NOT NULL,
grade DOUBLE PRECISION,
status VARCHAR(50),
PRIMARY KEY (student_id, course_id),
FOREIGN KEY (student_id) REFERENCES students(id),
FOREIGN KEY (course_id) REFERENCES courses(id)
);
Как работать с составным ключом
Создание записи:
@Service
public class StudentCourseService {
@Autowired
private StudentCourseRepository repository;
@Autowired
private StudentRepository studentRepository;
@Autowired
private CourseRepository courseRepository;
public void enrollStudent(Long studentId, Long courseId) {
Student student = studentRepository.findById(studentId)
.orElseThrow(() -> new EntityNotFoundException("Student not found"));
Course course = courseRepository.findById(courseId)
.orElseThrow(() -> new EntityNotFoundException("Course not found"));
// Создаём новую запись
StudentCourse enrollment = new StudentCourse(student, course);
// Сохраняем
repository.save(enrollment);
}
// Поиск по составному ключу
public Optional<StudentCourse> findEnrollment(Long studentId, Long courseId) {
StudentCourseKey key = new StudentCourseKey(studentId, courseId);
return repository.findById(key);
}
// Удаление по составному ключу
public void unenrollStudent(Long studentId, Long courseId) {
StudentCourseKey key = new StudentCourseKey(studentId, courseId);
repository.deleteById(key);
}
}
Repository:
import org.springframework.data.jpa.repository.JpaRepository;
public interface StudentCourseRepository extends JpaRepository<StudentCourse, StudentCourseKey> {
// findById(key) работает с составным ключом автоматически
}
Альтернатива: @IdClass вместо @Embeddable
Есть старый способ с @IdClass:
// Класс ключа (обычный класс)
public class StudentCourseId implements Serializable {
public Long studentId;
public Long courseId;
public StudentCourseId() {}
public StudentCourseId(Long studentId, Long courseId) {
this.studentId = studentId;
this.courseId = courseId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
StudentCourseId that = (StudentCourseId) o;
return Objects.equals(studentId, that.studentId) &&
Objects.equals(courseId, that.courseId);
}
@Override
public int hashCode() {
return Objects.hash(studentId, courseId);
}
}
// Сущность с @IdClass
@Entity
@Table(name = "student_courses")
@IdClass(StudentCourseId.class)
public class StudentCourse {
@Id
private Long studentId;
@Id
private Long courseId;
@ManyToOne
@JoinColumn(name = "student_id", insertable = false, updatable = false)
private Student student;
@ManyToOne
@JoinColumn(name = "course_id", insertable = false, updatable = false)
private Course course;
private Double grade;
}
@Embeddable vs @IdClass:
| Аспект | @Embeddable (@EmbeddedId) | @IdClass |
|---|---|---|
| Читаемость | Лучше | Хуже |
| Переиспользование | Можно внедрить в другие сущности | Только для ID |
| Сложность | Средняя | Та же |
| Рекомендуется | ДА, в современном JPA | НЕТ, это legacy |
Важные особенности
1. equals() и hashCode() КРИТИЧНЫ:
// Если не переопределить, Hibernate не найдёт запись в cache
StudentCourseKey key1 = new StudentCourseKey(1L, 2L);
StudentCourseKey key2 = new StudentCourseKey(1L, 2L);
// Без equals/hashCode: key1.equals(key2) == false
// С equals/hashCode: key1.equals(key2) == true
2. Конструктор без параметров ОБЯЗАТЕЛЕН:
// Hibernate создаёт объект без параметров
StudentCourseKey key = StudentCourseKey.class.newInstance(); // Требует пустой конструктор
3. Serializable требуется:
// Ключ должен быть сериализуемым для кэширования, сессий и т.д.
implements Serializable
private static final long serialVersionUID = 1L;
4. MapsId для связей:
@ManyToOne
@MapsId("studentId") // Это свойство в StudentCourseKey
@JoinColumn(name = "student_id")
private Student student;
// MapsId говорит: значение student_id получай от объекта student
// Не нужно вручную устанавливать studentId в ключ
Полный рабочий пример
// Миграция БД
CREATE TABLE students (id BIGINT PRIMARY KEY);
CREATE TABLE courses (id BIGINT PRIMARY KEY);
CREATE TABLE student_courses (
student_id BIGINT NOT NULL,
course_id BIGINT NOT NULL,
enrollment_date TIMESTAMP,
PRIMARY KEY (student_id, course_id),
FOREIGN KEY (student_id) REFERENCES students(id),
FOREIGN KEY (course_id) REFERENCES courses(id)
);
// Использование
Student student = studentRepository.findById(1L).get();
Course course = courseRepository.findById(2L).get();
StudentCourse enrollment = new StudentCourse(student, course);
enrollment.setEnrollmentDate(LocalDateTime.now());
studentCourseRepository.save(enrollment);
// Поиск
Optional<StudentCourse> found = studentCourseRepository.findById(
new StudentCourseKey(1L, 2L)
);
// Удаление
studentCourseRepository.deleteById(new StudentCourseKey(1L, 2L));
Вывод: для составного ключа нужно создать класс с @Embeddable, реализовать equals/hashCode/Serializable, использовать @EmbeddedId в сущности и @MapsId для связей с другими сущностями.