HibernateMiddleTechnical

Какие типы ассоциаций существуют в Hibernate: @OneToOne, @OneToMany, @ManyToOne, @ManyToMany?

ManyToOne — owner FK, OneToMany — inverse через mappedBy, OneToOne — уникальный FK, ManyToMany — join-таблица. По умолчанию ManyToOne/OneToOne EAGER — всегда переключайте на LAZY.

Типы ассоциаций в Hibernate

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

@ManyToOne

Наиболее распространённый тип. Сторона @ManyToOne является owner ассоциации — именно в её таблице хранится foreign key. Hibernate генерирует колонку автоматически или её можно переопределить через @JoinColumn.

@Entity
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id", nullable = false)
    private Customer customer;
}

По умолчанию fetch-стратегия EAGER — это частая ловушка. Всегда явно указывайте FetchType.LAZY.

@OneToMany

Обратная сторона @ManyToOne. Как правило, является inverse стороной через mappedBy, что означает: эта сторона не управляет foreign key, а лишь отражает связь. Без mappedBy Hibernate создаст отдельную join-таблицу.

@Entity
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Order> orders = new ArrayList<>();

    // Обязательно поддерживайте обе стороны вручную
    public void addOrder(Order order) {
        orders.add(order);
        order.setCustomer(this);
    }
}

@OneToOne

Связь один-к-одному. Owner стороны содержит foreign key с UNIQUE constraint. Inverse сторона использует mappedBy.

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinColumn(name = "profile_id", unique = true)
    private UserProfile profile;
}

@Entity
public class UserProfile {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToOne(mappedBy = "profile")
    private User user;
}

@ManyToMany

Реализуется через промежуточную join-таблицу. Hibernate управляет ею автоматически через @JoinTable. Однако в production почти всегда лучше заменить @ManyToMany явной сущностью-связкой с дополнительными атрибутами (дата, статус и т.д.).

@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToMany
    @JoinTable(
        name = "student_course",
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id")
    )
    private Set<Course> courses = new HashSet<>();
}

Подводные камни

  • FetchType.EAGER по умолчанию в @ManyToOne и @OneToOne приводит к N+1 проблеме при обходе коллекций. Всегда используйте LAZY и загружайте через JOIN FETCH при необходимости.
  • Отсутствие mappedBy в @OneToMany без явной @JoinTable заставляет Hibernate создать скрытую join-таблицу вместо использования FK в дочерней таблице.
  • Несинхронизированные обе стороны: при двунаправленной связи изменение только одной стороны в памяти ведёт к некорректному состоянию первого уровня кэша до flush.
  • @ManyToMany с List вместо Set вызывает полное удаление и повторную вставку всех записей join-таблицы при любом изменении коллекции — используйте Set.
  • CascadeType.ALL на @ManyToMany опасен: удаление одного студента может каскадно удалить общие курсы.
  • equals/hashCode: без корректной реализации на основе бизнес-ключа (не id) поведение Set для managed/detached сущностей непредсказуемо.
  • Cartesian product: одновременная fetch-загрузка нескольких @OneToMany коллекций через JOIN FETCH порождает декартово произведение строк — используйте @BatchSize или несколько запросов.

Common mistakes

  • Путать термин «association types» с соседним механизмом Hibernate.
  • Не называть границу lifecycle, transaction, thread или request для «association types».
  • Игнорировать production-эффекты «association types»: latency, SQL shape, memory, security или observability.

What the interviewer is testing

  • Попросить объяснить механизм «association types» на минимальном примере.
  • Проверить, видит ли кандидат failure mode и диагностику для «association types».
  • Уточнить, какие настройки или API меняют «association types» в реальном сервисе.

Sources

Related topics