1. Обзор
В этом руководстве мы увидим несколько способов работы с отношениями «многие ко многим» с использованием JPA.
Мы будем использовать модель студентов, курсов и различных отношений между ними.
Для простоты в примерах кода мы покажем только те атрибуты и конфигурацию JPA, которые относятся к отношениям «многие ко многим».
2. Базовый метод «многие ко многим»
2.1. Моделирование отношения «многие ко многим»
Отношение — это связь между двумя типами сущностей. В случае отношения «многие ко многим» обе стороны могут относиться к нескольким экземплярам другой стороны.
Обратите внимание, что типы сущностей могут быть связаны друг с другом. Подумайте о примере моделирования генеалогических деревьев: каждый узел — это человек, поэтому, если мы говорим об отношениях родитель-потомок, оба участника будут людьми.
Однако не имеет большого значения, говорим ли мы об отношениях между одним или несколькими типами сущностей. Так как легче думать об отношениях между двумя разными типами сущностей, мы будем использовать это для иллюстрации наших случаев.
Давайте возьмем пример студентов, отмечающих курсы, которые им нравятся.
Студенту может нравиться много курсов, а многим студентам может нравиться один и тот же курс:
Как мы знаем, в РСУБД мы можем создавать отношения с внешними ключами. Поскольку обе стороны должны иметь возможность ссылаться друг на друга, нам нужно создать отдельную таблицу для хранения внешних ключей :
Такая таблица называется таблицей соединений. В таблице соединений комбинация внешних ключей будет ее составным первичным ключом.
2.2. Реализация в JPA
Моделирование отношений «многие ко многим» с помощью POJO очень просто. Мы должны включить в оба класса коллекцию
, которая содержит элементы других.
После этого нам нужно пометить класс @Entity
и первичный ключ @Id
, чтобы сделать их правильными объектами JPA.
Кроме того, мы должны настроить тип отношения. Итак, помечаем коллекции аннотациями @ManyToMany
:
@Entity
class Student {
@Id
Long id;
@ManyToMany
Set<Course> likedCourses;
// additional properties
// standard constructors, getters, and setters
}
@Entity
class Course {
@Id
Long id;
@ManyToMany
Set<Student> likes;
// additional properties
// standard constructors, getters, and setters
}
Кроме того, мы должны настроить, как моделировать отношения в СУБД.
На стороне владельца мы настраиваем отношения. Мы будем использовать класс Student .
Мы можем сделать это с помощью аннотации @JoinTable
в классе Student
. Мы предоставляем имя таблицы соединений ( course_like
), а также внешние ключи с аннотациями @JoinColumn
. Атрибут joinColumn
будет подключаться к стороне владельца отношения, а inverseJoinColumn
— к другой стороне:
@ManyToMany
@JoinTable(
name = "course_like",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id"))
Set<Course> likedCourses;
Обратите внимание, что использование @JoinTable
или даже @JoinColumn
не требуется. JPA сгенерирует для нас имена таблиц и столбцов. Однако стратегия, которую использует JPA, не всегда будет соответствовать соглашениям об именах, которые мы используем. Итак, нам нужна возможность настроить имена таблиц и столбцов.
На целевой стороне нам нужно только указать имя поля, которое отображает отношение.
Поэтому мы устанавливаем атрибут mappedBy
аннотации @ManyToMany в классе
Course
:
@ManyToMany(mappedBy = "likedCourses")
Set<Student> likes;
Имейте в виду, что, поскольку отношение «многие ко многим» не имеет стороны владельца в базе данных , мы можем настроить таблицу соединения в классе « Курс
» и ссылаться на нее из класса « Студент
».
3. Многие ко многим с использованием составного ключа
3.1. Моделирование атрибутов отношений
Допустим, мы хотим, чтобы студенты оценивали курсы. Студент может оценить любое количество курсов, и любое количество студентов может оценить один и тот же курс. Следовательно, это также отношение многие ко многим.
Что делает этот пример немного более сложным, так это то, что отношение рейтинга представляет собой нечто большее, чем тот факт, что оно существует. Нам нужно сохранить оценку, которую студент поставил на курсе.
Где мы можем хранить эту информацию? Мы не можем поместить его в сущность « Студент
», так как студент может давать разные оценки разным курсам. Точно так же хранение его в объекте « Курс
» также не будет хорошим решением.
Это ситуация, когда само отношение имеет атрибут.
В этом примере присоединение атрибута к отношению выглядит на ER-диаграмме следующим образом:
Мы можем смоделировать его почти так же, как простое отношение «многие ко многим». Единственное отличие состоит в том, что мы присоединяем новый атрибут к таблице соединения:
3.2. Создание составного ключа в JPA
Реализация простого отношения «многие ко многим» была довольно простой. Единственная проблема заключается в том, что мы не можем добавить свойство к отношению таким образом, потому что мы связываем сущности напрямую. Поэтому у нас не было возможности добавить свойство к самой связи.
Поскольку мы сопоставляем атрибуты БД с полями класса в JPA, нам нужно создать новый класс сущности для отношения.
Конечно, каждому объекту JPA нужен первичный ключ. Поскольку наш первичный ключ является составным ключом, нам нужно создать новый класс, который будет содержать разные части ключа :
@Embeddable
class CourseRatingKey implements Serializable {
@Column(name = "student_id")
Long studentId;
@Column(name = "course_id")
Long courseId;
// standard constructors, getters, and setters
// hashcode and equals implementation
}
Обратите внимание, что составной ключевой класс должен соответствовать некоторым ключевым требованиям :
- Мы должны пометить его
@Embeddable
. - Он должен реализовать
java.io.Serializable
. - Нам нужно предоставить реализацию методов
hashcode()
иequals()
. - Ни одно из полей не может быть самостоятельным объектом.
3.3. Использование составного ключа в JPA
Используя этот составной ключевой класс, мы можем создать класс сущности, который моделирует таблицу соединений:
@Entity
class CourseRating {
@EmbeddedId
CourseRatingKey id;
@ManyToOne
@MapsId("studentId")
@JoinColumn(name = "student_id")
Student student;
@ManyToOne
@MapsId("courseId")
@JoinColumn(name = "course_id")
Course course;
int rating;
// standard constructors, getters, and setters
}
Этот код очень похож на обычную реализацию сущности. Однако у нас есть несколько ключевых отличий:
- Мы использовали
@EmbeddedId
для обозначения первичного ключа , который является экземпляром классаCourseRatingKey
. - Мы отметили поля
студента
икурса с помощью
@MapsId
.
@MapsId
означает, что мы привязываем эти поля к части ключа, и они являются внешними ключами отношения «многие к одному». Нам это нужно, потому что, как мы уже упоминали, мы не можем иметь сущности в составном ключе.
После этого мы можем настроить обратные ссылки в сущностях Student
и Course
, как и раньше:
class Student {
// ...
@OneToMany(mappedBy = "student")
Set<CourseRating> ratings;
// ...
}
class Course {
// ...
@OneToMany(mappedBy = "course")
Set<CourseRating> ratings;
// ...
}
Обратите внимание, что существует альтернативный способ использования составных ключей: аннотация @IdClass
.
3.4. Дополнительные характеристики
Мы настроили отношения к классам Student
и Course
как @ManyToOne
. Мы могли бы сделать это, потому что с новой сущностью мы структурно разложили отношение «многие ко многим» на два отношения «многие к одному».
Почему мы смогли это сделать? Если мы внимательно изучим таблицы в предыдущем случае, мы увидим, что они содержали два отношения «многие к одному». Другими словами, в СУБД нет отношений «многие ко многим». Мы называем структуры, которые мы создаем с помощью таблиц соединений, отношениями «многие ко многим», потому что это то, что мы моделируем.
Кроме того, будет более понятно, если мы будем говорить об отношениях «многие ко многим», потому что это и есть наше намерение. Между тем таблица соединений — это всего лишь деталь реализации; мы действительно не заботимся об этом.
Кроме того, это решение имеет дополнительную функцию, о которой мы еще не упоминали. Простое решение «многие ко многим» создает связь между двумя сущностями. Следовательно, мы не можем расширить отношение на большее количество сущностей. Но в этом решении у нас нет этого ограничения: мы можем моделировать отношения между любым количеством типов сущностей.
Например, когда несколько учителей могут вести курс, учащиеся могут оценивать, как конкретный учитель ведет определенный курс. Таким образом, рейтинг будет отношением между тремя сущностями: студентом, курсом и преподавателем.
4. Многие ко многим с новой сущностью
4.1. Моделирование атрибутов отношений
Допустим, мы хотим разрешить студентам регистрироваться на курсы. Также нам нужно сохранить точку, когда студент зарегистрировался на конкретный курс. Кроме того, мы хотим сохранить оценку, которую она получила за курс.
В идеальном мире мы могли бы решить эту проблему с помощью предыдущего решения, где у нас была сущность с составным ключом. Однако мир далек от идеала, и студенты не всегда проходят курс с первого раза.
В этом случае существует несколько соединений между одними и теми же парами студент-курс или несколькими строками с одними и теми же парами student_id-course_id
. Мы не можем смоделировать его, используя любое из предыдущих решений, потому что все первичные ключи должны быть уникальными. Итак, нам нужно использовать отдельный первичный ключ.
Следовательно, мы можем ввести сущность , которая будет содержать атрибуты регистрации:
В этом случае сущность Registration представляет отношение между двумя другими сущностями.
Поскольку это сущность, у нее будет собственный первичный ключ.
Помните, что в предыдущем решении у нас был составной первичный ключ, который мы создали из двух внешних ключей.
Теперь два внешних ключа не будут частью первичного ключа:
4.2. Реализация в JPA
Поскольку course_registration
стала обычной таблицей, мы можем создать простую старую JPA-сущность, моделирующую ее:
@Entity
class CourseRegistration {
@Id
Long id;
@ManyToOne
@JoinColumn(name = "student_id")
Student student;
@ManyToOne
@JoinColumn(name = "course_id")
Course course;
LocalDateTime registeredAt;
int grade;
// additional properties
// standard constructors, getters, and setters
}
Нам также необходимо настроить отношения в классах Student
и Course :
class Student {
// ...
@OneToMany(mappedBy = "student")
Set<CourseRegistration> registrations;
// ...
}
class Course {
// ...
@OneToMany(mappedBy = "course")
Set<CourseRegistration> registrations;
// ...
}
Опять же, мы настроили отношения ранее, поэтому нам нужно только сообщить JPA, где он может найти эту конфигурацию.
Мы также могли бы использовать это решение для решения предыдущей проблемы оценки курсов студентами. Однако кажется странным создавать выделенный первичный ключ, если в этом нет необходимости.
Более того, с точки зрения РСУБД это не имеет особого смысла, поскольку объединение двух внешних ключей дает идеальный составной ключ. Кроме того, этот составной ключ имел четкое значение: какие сущности мы связываем в отношениях.
В противном случае выбор между этими двумя реализациями часто является просто личным предпочтением.
5. Вывод
В этой статье мы увидели, что такое отношения «многие ко многим» и как мы можем смоделировать их в СУБД с использованием JPA.
Мы видели три способа смоделировать это в JPA. Все три имеют разные преимущества и недостатки, когда речь идет об этих аспектах:
- ясность кода
- Четкость БД
- возможность назначать атрибуты отношениям
- сколько сущностей мы можем связать отношениями
- поддержка нескольких соединений между одними и теми же сущностями
Как обычно, примеры доступны на GitHub .