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

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

1. Вступление

В этом руководстве мы увидим несколько способовdeal with many-to-many relationships using JPA.

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

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

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

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

Узнайте, как имена таблиц генерируются по умолчанию и как переопределить это поведение.

Read more

Обзор JPA / Hibernate Типы Каскадов

Быстрый и практический обзор типов каскада JPA / Hibernate.

Read more

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

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

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

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

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

Например, когда студенты отмечают понравившиеся им курсы: студенту могут понравиться курсыmany, а студентамmany может понравиться тот же курс:

image

Как мы знаем, в РСУБД мы можем создавать отношения с внешними ключами. Поскольку обе стороны должны иметь возможность ссылаться на другую,we need to create a separate table to hold the foreign keys:

image

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

2.2. Реализация в JPA

Modeling a many-to-many relationship with POJOs легко. Нам следуетinclude a Collection in both classes, который содержит элементы остальных.

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

Также мы должны настроить тип отношений. Отсюда аннотацииwe mark the collections with @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
}

Кроме того, мы должны настроить, как моделировать отношения в RDBMS.

На стороне владельца мы настраиваем взаимосвязь, и в этом примере мы выберем классStudent.

We can do this with the @JoinTable annotation in the Student class. Мы предоставляем имя объединяемой таблицы (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, не всегда соответствует используемым нами соглашениям об именах. Отсюда возможность настроить имена таблиц и столбцов.

On the target side, we only have to provide the name of the field, which maps the relationship. Поэтому мы устанавливаем атрибутmappedBy аннотации@ManyToMany в классеCourse:

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

Обратите внимание, что, начиная сa many-to-many relationship doesn’t have an owner side in the database, мы могли настроить таблицу соединений в классеCourse и ссылаться на нее из классаStudent.

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

3.1. Атрибуты моделирования отношений

Допустим, мы хотим, чтобы студенты оценивали курсы. Студент может оценить любое количество курсов, и любое количество студентов может оценить один и тот же курс. Следовательно, это также отношения "многие ко многим". Немного сложнее то, чтоthere is more to the rating relationship than the fact that it exists. We need to store the rating score the student gave on the course.

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

Это ситуация, когдаthe relationship itself has an attribute.

Используя этот пример, присоединение атрибута к отношению выглядит на диаграмме ER следующим образом:

image

Мы можем смоделировать это почти так же, как и с простыми отношениями «многие ко многим». Единственная разница в том, что мы прикрепляем новый атрибут к объединенной таблице:

image

3.2. Создание составного ключа в JPA

Реализация простого отношения «многие ко многим» была довольно простой. Единственная проблема заключается в том, что мы не можем добавить свойство в отношение таким образом, потому что мы связали сущности напрямую. Следовательно,we had no way to add a property to the relationship itself.

Поскольку мы сопоставляем атрибуты БД с полями классов в JPA,we need to create a new entity class for the relationship.

Конечно, каждому объекту JPA нужен первичный ключ. Because our primary key is a composite key, we have to create a new class, which will hold the different parts of the key:с

@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
}

Обратите внимание, что есть несколькоkey requirements, which a composite key class has to fulfill:

  • Мы должны отметить это@Embeddable

  • Он должен реализоватьjava.io.Serializable

  • Нам нужно предоставить реализацию методовhashcode() иequals()

  • Ни одно из полей не может быть сущностью

3.3. Использование составного ключа в JPA

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

@Entity
class CourseRating {

    @EmbeddedId
    CourseRatingKey id;

    @ManyToOne
    @MapsId("student_id")
    @JoinColumn(name = "student_id")
    Student student;

    @ManyToOne
    @MapsId("course_id")
    @JoinColumn(name = "course_id")
    Course course;

    int rating;

    // standard constructors, getters, and setters
}

Этот код очень похож на обычную реализацию сущности. Однако у нас есть несколько ключевых отличий:

  • мы использовали@EmbeddedId, to mark the primary key, который является экземпляром классаCourseRatingKey

  • мы отметили поляstudent иcourse знаком@MapsId

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

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

class Student {

    // ...

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

    // ...
}

class Course {

    // ...

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

    // ...
}

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

3.4. Дальнейшие характеристики

Мы настроили отношения с классамиStudent иCourse как@ManyToOne. Мы могли сделать это, потому что с помощью новой сущности мы структурно разложили отношение «многие ко многим» на два отношения «многие ко многим».

Почему мы смогли это сделать? Если мы внимательно изучим таблицы в предыдущем случае, то увидим, что они содержат два отношения «многие к одному». In other words, there isn’t any many-to-many relationship in an RDBMS. We call the structures we create with join tables many-to-many relationships because that’s what we model.с

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

Кроме того, у этого решения есть дополнительная функция, о которой мы еще не упоминали. Простое решение «многие ко многим» создает отношения между двумя объектами. Следовательно, мы не можем расширять отношения с другими объектами. Однако в этом решении у нас нет этого ограничения:we can model relationships between any number of entity types.

Например, когда несколько учителей могут преподавать курс, студенты могут оценить, как конкретный учитель преподает определенный курс. That way, a rating would be a relationship between three entities: a student, a course, and a teacher.с

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

4.1. Атрибуты моделирования отношений

Допустим, мы хотим, чтобы студенты регистрировались на курсы. Такжеwe need to store the point when a student registered for a specific course. Кроме того, мы также хотим запомнить, какую оценку она получила в курсе.

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

В этом случае естьmultiple connections between the same student-course pairs или несколько строк с одинаковыми парамиstudent_id-course_id. Мы не можем смоделировать его с помощью любого из предыдущих решений, потому что все первичные ключи должны быть уникальными. Поэтому нам нужно использовать отдельный первичный ключ.

Следовательно,we can introduce an entity, который будет содержать атрибуты регистрации:

image

В этом случаеthe Registration entity represents the relationship между двумя другими объектами.

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

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

image

4.2. Реализация в JPA

Посколькуcoure_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, где он может найти эту конфигурацию.

Обратите внимание, что мы могли бы использовать это решение для решения предыдущей проблемы: оценки студентов курсов. Тем не менее, кажется странным создавать выделенный первичный ключ, если только нам это не нужно. Более того, с точки зрения СУБД в этом нет особого смысла, поскольку объединение двух внешних ключей дает идеальный составной ключ. Кроме того, чтоcomposite key had a clear meaning: which entities we connect in the relationship.

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

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

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

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

  • ясность кода

  • Ясность БД

  • возможность назначать атрибуты отношениям

  • сколько сущностей мы можем связать с отношениями, и

  • поддержка нескольких соединений между одними и теми же объектами

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