Этап 5 - Relationships: One-to-One, One-to-Many и Many-to-Many
Приложения редко хранят изолированные записи. У пользователя есть профиль, пользователь создает много заказов, категория содержит много товаров, а студенты могут проходить много курсов. В базе relationship - это структура ключей, а в Java она становится object reference или collection. В реляционной базе такие связи представлены primary keys, foreign keys и иногда join tables. В JPA они представлены ссылками между entities и relationship annotations.
One-to-One
One-to-one означает, что одна строка таблицы A связана с одной строкой таблицы B. Типичный пример - User и Profile: один пользователь имеет один профиль, и один профиль принадлежит одному пользователю. В базе одна таблица обычно содержит foreign key с unique constraint.
@OneToOne
@JoinColumn(name = "profile_id")
private Profile profile;
@OneToOne описывает тип связи. @JoinColumn описывает foreign key column, по которой соединяются таблицы. One-to-one не стоит использовать слишком часто. Если две группы полей всегда загружаются и изменяются вместе, возможно, они должны быть в одной таблице. Отдельная entity полезна, когда lifecycle или security rules отличаются.
One-to-Many и Many-to-One
One-to-many означает, что один parent связан со многими children. У пользователя может быть много заказов. В базе foreign key обычно находится на стороне many: таблица orders содержит user_id. В JPA child side обычно моделируется через @ManyToOne, а parent collection - через @OneToMany(mappedBy = "user").
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@OneToMany(mappedBy = "user")
private List<Order> orders = new ArrayList<>();
mappedBy означает, что другой side владеет связью. Owning side - это сторона, где находится foreign key column. Частая ошибка - поставить @JoinColumn на обе стороны или забыть обновить обе object references в коде. Если добавить заказ в user.getOrders(), нужно также вызвать order.setUser(user), чтобы object graph и foreign key в базе совпадали.
Many-to-Many и join tables
Many-to-many означает, что много строк с одной стороны могут быть связаны со многими строками с другой стороны. Студенты и курсы - классический пример. Студент может проходить много курсов, а курс может иметь много студентов. Реляционная база не хранит это напрямую в одной колонке, поэтому используется join table, например student_courses с student_id и course_id.
@ManyToMany
@JoinTable(
name = "student_courses",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private Set<Course> courses = new HashSet<>();
Many-to-many удобна для простых связей, но в бизнес-системах у связи часто есть собственные данные: дата записи, статус, прогресс или оценка. Когда у relationship есть свои поля, join table лучше моделировать отдельной entity, например Enrollment.
| Вопрос моделирования | Правильный ответ в реляционной базе | Последствие для JPA mapping |
|---|---|---|
| Может ли один profile принадлежать двум users? | Нет, значит foreign key должен быть unique. | Используй @OneToOne; owning side держи там, где находится foreign key. |
| Где хранится foreign key для user-order? | В orders.user_id, потому что много orders указывают на одного user. | Order владеет связью через @ManyToOne и @JoinColumn. |
Почему User.orders нужен mappedBy? | Таблица users не хранит список order ids. | @OneToMany(mappedBy = "user") говорит, что collection отражает Order.user. |
| Почему student-course нужна отдельная table? | Обе стороны имеют много rows, одного foreign key column недостаточно. | Используй @JoinTable или entity Enrollment, если у связи есть свои fields. |
| Когда опасен cascade delete? | Когда child row является shared или имеет самостоятельный lifecycle. | Не ставь CascadeType.REMOVE на shared relations вроде many-to-many. |
Cascades и orphan removal
Cascade управляет тем, распространяется ли операция с одной entity на связанные entities. PERSIST сохраняет новые связанные entities. MERGE объединяет related detached entities. REMOVE удаляет связанные entities. ALL включает все cascade operations. Cascades сильные и опасные: CascadeType.REMOVE на many-to-many может случайно удалить shared records.
orphanRemoval = true означает, что child, удаленный из parent collection, должен быть удален из базы. Это полезно для настоящего владения, например order items внутри одного order. Это не подходит, если child может существовать самостоятельно.
Практика
Смоделируй User и Profile как one-to-one, User и Order как one-to-many/many-to-one, а Student и Course как many-to-many. Затем переделай student-course в entity Enrollment и добавь enrolledAt. Это показывает, почему many-to-many часто является только первым шагом.
Чек-лист понимания
- Могу определить, где должен находиться foreign key.
- Понимаю
mappedByи owning side. - Знаю, почему many-to-many использует join table.
- Могу аккуратно выбирать cascades.
- Понимаю, когда подходит
orphanRemoval.