Что такое составной первичный ключ в БД и когда он применяется?

Ответ

Составной первичный ключ (Composite Primary Key) — это первичный ключ, состоящий из двух или более столбцов таблицы. Уникальность записи обеспечивается только комбинацией значений этих столбцов.

Когда применяется:

  1. В связующих (junction) таблицах для отношений "многие-ко-многим".
  2. Когда бизнес-логика определяет сущность уникальной парой/набором атрибутов (например, номер рейса + дата вылета).

Пример на Java с JPA/Hibernate:

// 1. Класс-идентификатор (должен быть Serializable и реализовывать equals/hashCode)
public class EnrollmentId implements Serializable {
    private Long studentId;
    private Long courseId;

    // Конструкторы, геттеры, сеттеры...

    @Override
    public boolean equals(Object o) { /* сравнение по studentId и courseId */ }
    @Override
    public int hashCode() { /* хэш-код на основе studentId и courseId */ }
}

// 2. Сущность, использующая составной ключ через аннотацию @IdClass
@Entity
@IdClass(EnrollmentId.class) // Указываем класс-идентификатор
public class Enrollment {
    @Id
    @Column(name = "student_id")
    private Long studentId;

    @Id
    @Column(name = "course_id")
    private Long courseId;

    private Integer grade;
    private LocalDate enrolledDate;

    // Связи с сущностями Student и Course (опционально)
    @ManyToOne
    @MapsId("studentId") // Связывает с частью ключа
    @JoinColumn(name = "student_id", insertable = false, updatable = false)
    private Student student;

    @ManyToOne
    @MapsId("courseId")
    @JoinColumn(name = "course_id", insertable = false, updatable = false)
    private Course course;
    // ...
}

Как это работает в SQL:

CREATE TABLE enrollment (
    student_id BIGINT NOT NULL,
    course_id BIGINT NOT NULL,
    grade INT,
    enrolled_date DATE,
    PRIMARY KEY (student_id, course_id), -- Составной ключ
    FOREIGN KEY (student_id) REFERENCES student(id),
    FOREIGN KEY (course_id) REFERENCES course(id)
);

Почему это важно: Гарантирует, что один студент может быть записан на конкретный курс только один раз. Альтернативный подход — использование суррогатного ключа (отдельный id) и уникального ограничения на пару (student_id, course_id).

Ответ 18+ 🔞

А, составной ключ, ёпта! Ну это ж классика, как Муму в мешке, блядь!

Смотри, представь себе таблицу, где один столбец за уникальность не отвечает — нихуя не тянет, один как перст. А вот когда они, сука, объединяются, два или больше, вот тогда они и выдают эту самую уникальную запись. Как Герасим с Муму — по отдельности просто чувак и собачка, а вместе — трагедия на весь мир, блядь.

Где это, сука, пригождается:

  1. Таблицы-связки для этих ваших «многие-ко-многим». Чтобы студент на один курс дважды не записался, падла такая.
  2. Когда сама логика бизнеса кричит: «Уникальность вот в этой паре, блядь!». Номер рейса и дата, например. Самолёт же не может в одно и то же время в двух местах быть, если он не хуй с горы, конечно.

Вот тебе пример на Java, смотри, не обосрись:

// 1. Класс-идентификатор. Должен быть Serializable, и equals/hashCode чтоб не хуй собачий был!
public class EnrollmentId implements Serializable {
    private Long studentId;
    private Long courseId;
    // Конструкторы, геттеры, сеттеры...
    @Override
    public boolean equals(Object o) { /* сравниваем и studentId, И courseId */ }
    @Override
    public int hashCode() { /* хэш тоже из обоих полей делаем */ }
}

// 2. А вот и сама сущность, хитрая жопа.
@Entity
@IdClass(EnrollmentId.class) // Говорим Хибернейту: «Смотри, чувак, ключ — вот этот класс!»
public class Enrollment {
    @Id
    @Column(name = "student_id")
    private Long studentId; // Первая часть ключа

    @Id
    @Column(name = "course_id")
    private Long courseId; // Вторая часть ключа. Вместе — сила!

    private Integer grade; // Оценка
    private LocalDate enrolledDate; // Дата записи

    // Связи можно прикрутить, но это опционально, блядь
    @ManyToOne
    @MapsId("studentId") // Эта хуйня связывает связь с частью ключа!
    @JoinColumn(name = "student_id", insertable = false, updatable = false)
    private Student student;

    @ManyToOne
    @MapsId("courseId")
    @JoinColumn(name = "course_id", insertable = false, updatable = false)
    private Course course;
    // ...
}

А на SQL это, блядь, выглядит вот так, простота:

CREATE TABLE enrollment (
    student_id BIGINT NOT NULL,
    course_id BIGINT NOT NULL,
    grade INT,
    enrolled_date DATE,
    PRIMARY KEY (student_id, course_id), -- ВОТ ОН, СОСТАВНОЙ КЛЮЧ, В РОТ МЕНЯ ЧИХ-ПЫХ!
    FOREIGN KEY (student_id) REFERENCES student(id),
    FOREIGN KEY (course_id) REFERENCES course(id)
);

Суть в чём, блядь? Он гарантирует, что комбинация student_id и course_id будет одна-единственная. Нельзя записать студента дважды на один курс. Можно было бы, конечно, сделать отдельный id (суррогатный ключ) и потом на пару столбцов навесить UNIQUE. Но тут — сразу в лоб, красиво и понятно, как ебалом об асфальт.