Какие типы связей между сущностями (Entity) существуют в JPA/Hibernate?

«Какие типы связей между сущностями (Entity) существуют в JPA/Hibernate?» — вопрос из категории Hibernate, который задают на 22% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

JPA (Hibernate как его реализация) определяет четыре основных типа связей между сущностями, которые отображаются на реляционные связи в БД.

1. Один-к-одному (@OneToOne) Одна сущность связана ровно с одним экземпляром другой сущности.

@Entity
public class User {
    @Id
    private Long id;

    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinColumn(name = "profile_id") // Владелец связи, содержит внешний ключ
    private UserProfile profile;
}

@Entity
public class UserProfile {
    @Id
    private Long id;

    @OneToOne(mappedBy = "profile") // Обратная, невладеющая сторона
    private User user;
}
  • Использование: Профиль пользователя, паспортные данные.
  • Важно: Обычно одна из сторон является владеющей (содержит @JoinColumn), а другая — обратной (использует mappedBy).

2. Один-ко-многим (@OneToMany) / Многие-к-одному (@ManyToOne) Это две стороны одной и той же связи. @ManyToOne всегда является владеющей стороной.

@Entity
public class Department {
    @Id
    private Long id;

    @OneToMany(mappedBy = "department") // Обратная сторона
    private List<Employee> employees = new ArrayList<>();
}

@Entity
public class Employee {
    @Id
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY) // Владеющая сторона, содержит внешний ключ
    @JoinColumn(name = "department_id")
    private Department department;
}
  • Использование: Отдел и его сотрудники, заказ и позиции в заказе.
  • Почему @ManyToOne — владеющая: В таблице Employee создаётся столбец department_id. Это наиболее эффективное отображение.

3. Многие-ко-многим (@ManyToMany) Каждая сущность может быть связана с коллекцией другой сущности. Реализуется через связующую таблицу (join table).

@Entity
public class Student {
    @Id
    private Long id;

    @ManyToMany
    @JoinTable(
        name = "student_course", // Имя связующей таблицы
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id")
    )
    private Set<Course> courses = new HashSet<>(); // Используйте Set для избежания дубликатов
}

@Entity
public class Course {
    @Id
    private Long id;

    @ManyToMany(mappedBy = "courses") // Обратная сторона
    private Set<Student> students = new HashSet<>();
}
  • Использование: Студенты и курсы, теги и статьи.
  • Важно: Одна из сторон должна быть владеющей (определяет @JoinTable), другая — обратной (mappedBy).

Ключевые атрибуты для управления связями:

  • fetch (FetchType.LAZY или FetchType.EAGER) — стратегия загрузки. Всегда предпочитайте LAZY для производительности. Данные загружаются только при обращении.
  • cascade (CascadeType.ALL, PERSIST, MERGE, etc.) — определяет, какие операции с родительской сущностью распространяются на дочерние.
  • orphanRemoval (true/false) — если true, при удалении сущности из коллекции (например, employees.remove(emp)) связанная сущность будет удалена из БД.

Рекомендация: Для связей @OneToMany и @ManyToMany всегда инициализируйте коллекции (new ArrayList<>(), new HashSet<>()), чтобы избежать NullPointerException.