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

Что нужно изменить в сущности для работы составного ключа?

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

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

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

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

Что нужно изменить в сущности для работы составного ключа?

Составной ключ (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 для связей с другими сущностями.

Что нужно изменить в сущности для работы составного ключа? | PrepBro