Рубрики
Без рубрики

Отношения “Многие ко многим” в JPA

Узнайте, как моделировать отношения “многие ко многим” в Java с помощью JPA

Автор оригинала: Attila Fejér.

1. Обзор

В этом уроке мы увидим несколько способов справиться с отношениями “многие ко многим” с помощью JPA.

Мы будем использовать модель студентов, курсов и различных отношений между ними.

Для простоты в примерах кода мы покажем только атрибуты и конфигурацию JPA, связанные с отношениями “многие ко многим”.

Дальнейшее чтение:

Сопоставление имен классов сущностей с именами таблиц SQL с помощью JPA

Обзор типов каскадов JPA/Hibernate

2. Основные Многие ко многим

2.1. Моделирование отношений “Многие ко многим”

Связь – это связь между двумя типами сущностей. В случае отношений “многие ко многим” обе стороны могут относиться к нескольким экземплярам другой стороны.

Обратите внимание, что типы сущностей могут находиться в отношениях друг с другом. Подумайте о примере моделирования генеалогических деревьев: каждый узел-это человек, поэтому, если мы говорим об отношениях между родителями и детьми, оба участника будут людьми.

Однако не имеет такого значения, говорим ли мы о связи между одним или несколькими типами сущностей. Поскольку легче думать об отношениях между двумя различными типами сущностей, мы будем использовать это для иллюстрации наших примеров.

Давайте возьмем пример студентов, отмечающих курсы, которые им нравятся.

Студенту может понравиться много курсов, и многим студентам может понравиться один и тот же курс:

Как мы знаем, в СУБД мы можем создавать отношения с внешними ключами. Поскольку обе стороны должны иметь возможность ссылаться друг на друга, нам нужно создать отдельную таблицу для хранения внешних ключей :

Такая таблица называется таблицей join. В совместной таблице комбинация внешних ключей будет ее составным первичным ключом.

2.2. Внедрение в JPA

Моделирование отношений “многие ко многим” с POJOs легко. Мы должны включить Коллекцию в оба класса , которая содержит элементы других.

После этого нам нужно пометить класс @Entity и первичный ключ @Id , чтобы сделать их правильными сущностями JPA.

Кроме того, мы должны настроить тип отношений. Итак, мы помечаем коллекции @ManyToMany аннотациями:

@Entity
class Student {

    @Id
    Long id;

    @ManyToMany
    Set likedCourses;

    // additional properties
    // standard constructors, getters, and setters
}

@Entity
class Course {

    @Id
    Long id;

    @ManyToMany
    Set 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 likedCourses;

Обратите внимание, что использование @JoinTable или даже @JoinColumn не требуется. JPA сгенерирует для нас имена таблиц и столбцов. Однако стратегия, используемая JPA, не всегда будет соответствовать соглашениям об именах, которые мы используем. Итак, нам нужна возможность настроить имена таблиц и столбцов.

На целевой стороне нам нужно только указать имя поля, которое отображает связь.

Поэтому мы устанавливаем атрибут mappedBy аннотации @ManyToMany в классе Course :

@ManyToMany(mappedBy = "likedCourses")
Set likes;

Имейте в виду , что, поскольку отношение “многие ко многим” не имеет стороны владельца в базе данных , мы могли бы настроить таблицу соединений в классе Course и ссылаться на нее из класса Student .

3. Многие ко многим С использованием составного ключа

3.1. Моделирование атрибутов отношений

Допустим, мы хотим, чтобы студенты оценивали курсы. Студент может оценить любое количество курсов, и любое количество студентов может оценить один и тот же курс. Следовательно, это также отношения “многие ко многим”.

Что делает этот пример немного более сложным, так это то, что в рейтинговых отношениях есть нечто большее, чем сам факт их существования. Нам нужно сохранить рейтинговую оценку, которую студент дал на курсе.

Где мы можем хранить эту информацию? Мы не можем поместить его в сущность Student , так как студент может давать разные оценки разным курсам. Аналогично, хранение его в сущности Course также не было бы хорошим решением.

Это ситуация, когда само отношение имеет атрибут.

В этом примере прикрепление атрибута к отношению выглядит следующим образом на диаграмме 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 для пометки первичного ключа , который является экземпляром Ключа оценки курса класса.
  • Мы отметили студент и курс поля с @MapsId .

@MapsId означает, что мы привязываем эти поля к части ключа, и они являются внешними ключами отношения “многие к одному”. Нам это нужно, потому что, как мы уже упоминали, у нас не может быть сущностей в составном ключе.

После этого мы можем настроить обратные ссылки в сущностях Student и Course , как и раньше:

class Student {

    // ...

    @OneToMany(mappedBy = "student")
    Set ratings;

    // ...
}

class Course {

    // ...

    @OneToMany(mappedBy = "course")
    Set ratings;

    // ...
}

Обратите внимание, что существует альтернативный способ использования составных ключей: аннотация @IdClass.

3.4. Дополнительные Характеристики

Мы настроили отношения с Студент и Курс классы как @ManyToOne . Мы могли бы сделать это, потому что с новой сущностью мы структурно разложили отношение “многие ко многим” на два отношения “многие к одному”.

Почему мы смогли это сделать? Если мы внимательно изучим таблицы в предыдущем случае, то увидим, что они содержали два отношения “многие к одному”. Другими словами, в СУБД нет никаких отношений “многие ко многим”. Мы называем структуры, которые мы создаем с помощью таблиц соединений, отношениями “многие ко многим”, потому что это то, что мы моделируем.

Кроме того, это более ясно, если мы говорим об отношениях “многие ко многим”, потому что это наше намерение. Между тем, объединенная таблица-это всего лишь деталь реализации; на самом деле нас это не волнует.

Кроме того, у этого решения есть дополнительная функция, о которой мы еще не упоминали. Простое решение “многие ко многим” создает связь между двумя сущностями. Поэтому мы не можем расширить отношения на большее количество сущностей. Но у нас нет этого ограничения в этом решении: мы можем моделировать отношения между любым количеством типов сущностей.

Например, когда несколько учителей могут преподавать курс, учащиеся могут оценить, как конкретный учитель преподает конкретный курс. Таким образом, рейтинг будет представлять собой отношения между тремя сущностями: студентом, курсом и преподавателем.

4. Многие ко многим С Новой сущностью

4.1. Моделирование атрибутов отношений

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

В идеальном мире мы могли бы решить эту проблему с помощью предыдущего решения, где у нас была сущность с составным ключом. Однако мир далек от идеала, и студенты не всегда заканчивают курс с первой попытки.

В этом случае существует несколько соединений между одними и теми же парами студент-курс или несколькими строками с одинаковыми парами student_id-course_id . Мы не можем смоделировать его с помощью любого из предыдущих решений, потому что все первичные ключи должны быть уникальными. Итак, нам нужно использовать отдельный первичный ключ.

Поэтому мы можем ввести сущность , которая будет содержать атрибуты регистрации:

В этом случае объект регистрации представляет отношения между двумя другими объектами.

Поскольку это сущность, у нее будет свой собственный первичный ключ.

В предыдущем решении помните, что у нас был составной первичный ключ, который мы создали из двух внешних ключей.

Теперь два внешних ключа не будут частью первичного ключа:

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 registrations;

    // ...
}

class Course {

    // ...

    @OneToMany(mappedBy = "courses")
    Set registrations;

    // ...
}

Опять же, мы настроили отношения ранее, поэтому нам нужно только сообщить JPA, где он может найти эту конфигурацию.

Мы также могли бы использовать это решение для решения предыдущей проблемы рейтинговых курсов студентов. Тем не менее, кажется странным создавать выделенный первичный ключ, если в этом нет необходимости.

Более того, с точки зрения СУБД это не имеет особого смысла, поскольку объединение двух внешних ключей сделало идеальный составной ключ. Кроме того, этот составной ключ имел четкое значение: какие сущности мы соединяем в отношениях.

В противном случае выбор между этими двумя реализациями часто является просто личным предпочтением.

5. Заключение

В этой статье мы рассмотрели, что такое отношение “многие ко многим” и как мы можем смоделировать его в СУБД с использованием JPA.

Мы видели три способа смоделировать это в JPA. Все три имеют различные преимущества и недостатки, когда дело доходит до этих аспектов:

  • ясность кода
  • Четкость ДБ
  • возможность присвоения атрибутов отношениям
  • сколько сущностей мы можем связать с отношением
  • поддержка нескольких соединений между одними и теми же объектами

Как обычно, примеры доступны на GitHub .