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

Руководство по JPA с отображением отношений в режиме гибернации

В этом уроке мы погрузимся в сопоставление отношений с JPA и спящий режим на Java с примерами аннотаций “Многие ко многим”, “Один ко многим”, “Многие к одному” и “Один к одному”.

Автор оригинала: François Dupire.

Вступление

В этой статье мы погрузимся в Сопоставление отношений с JPA и спящий режим в Java .

API сохраняемости Java (JPA) является стандартом сохраняемости экосистемы Java. Это позволяет нам сопоставлять нашу модель домена непосредственно со структурой базы данных, а затем дает нам гибкость в управлении объектами в нашем коде – вместо того, чтобы возиться с громоздкими компонентами JDBC, такими как Соединение , Набор результатов и т. Д.

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

  • Руководство по JPA с гибернацией – Базовое сопоставление
  • Руководство по JPA с отображением отношений в режиме гибернации (вы здесь)
  • Руководство по JPA с Hibernate: Сопоставление наследования ( скоро! )
  • Руководство по JPA с запросами в режиме гибернации (скоро будет!)

Наш Пример

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

Вот как выглядит эта модель:

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

Кроме того, мы сможем извлекать их и манипулировать ими как объектами без хлопот с JDBC.

Отношения

Прежде всего, давайте определим отношения . Если мы посмотрим на нашу диаграмму классов, то увидим несколько взаимосвязей:

Преподаватели и курсы – студенты и курсы – курсы и учебные материалы.

Существуют также связи между учащимися и адресами, но они не считаются отношениями. Это связано с тем, что Адрес Не является сущностью (т. Е. он не сопоставлен с собственной таблицей). Итак, что касается JPA, это не отношения.

Существует несколько типов отношений:

  • Один ко многим
  • Многие к одному
  • Один к одному
  • Многие ко многим

Давайте разберемся с этими отношениями по одному.

Один ко многим/Многие к одному

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

Что такое Отношения ” Один ко многим|//?

Как следует из его названия, это связь, которая связывает одну сущность с многими другими сущностями.

В нашем примере это будет Преподаватель и их Курсы . Учитель может читать несколько курсов, но курс читает только один учитель (это Много к одному перспектива-много курсов одному учителю).

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

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

@Entity
public class Teacher {
    private String firstName;
    private String lastName;
}

@Entity
public class Course {
    private String title;
}

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

@OneToMany
private List courses;

Мы использовали Список в качестве типа поля здесь, но мы могли бы выбрать Набор или Карту (хотя для этого требуется немного больше конфигурации ).

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

JPA делает это за нас, учитывая наш вклад в то, как она должна справляться с отношениями. Это делается с помощью @JoinColumn аннотации:

@OneToMany
@JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID")
private List courses;

Использование этой аннотации сообщит JPA, что в таблице КУРС должен быть столбец внешнего ключа TEACHER_ID , который ссылается на столбец ИДЕНТИФИКАТОР таблицы УЧИТЕЛЯ .

Давайте добавим некоторые данные в эти таблицы:

insert into TEACHER(ID, LASTNAME, FIRSTNAME) values(1, 'Doe', 'Jane');

insert into COURSE(ID, TEACHER_ID, TITLE) values(1, 1, 'Java 101');
insert into COURSE(ID, TEACHER_ID, TITLE) values(2, 1, 'SQL 101');
insert into COURSE(ID, TEACHER_ID, TITLE) values(3, 1, 'JPA 101');

А теперь давайте проверим, работают ли отношения так, как ожидалось:

Teacher foundTeacher = entityManager.find(Teacher.class, 1L);

assertThat(foundTeacher.id()).isEqualTo(1L);
assertThat(foundTeacher.lastName()).isEqualTo("Doe");
assertThat(foundTeacher.firstName()).isEqualTo("Jane");
assertThat(foundTeacher.courses())
        .extracting(Course::title)
        .containsExactly("Java 101", "SQL 101", "JPA 101");

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

Если вы не знакомы с тестированием на Java, вам может быть интересно прочитать Модульное тестирование на Java с JUnit 5 !

Сторона владения и Двунаправленность

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

Курс называется ссылающейся стороной в этих отношениях.

Мы могли бы создать Курс сторону владения отношениями, сопоставив поле Учитель с @ManyToOne в Курсе классе вместо этого:

@ManyToOne
@JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID")
private Teacher teacher;

Теперь нет необходимости иметь список курсов в классе Учитель . Отношения сложились бы наоборот:

Course foundCourse = entityManager.find(Course.class, 1L);

assertThat(foundCourse.id()).isEqualTo(1L);
assertThat(foundCourse.title()).isEqualTo("Java 101");
assertThat(foundCourse.teacher().lastName()).isEqualTo("Doe");
assertThat(foundCourse.teacher().firstName()).isEqualTo("Jane");

На этот раз мы использовали аннотацию @ManyToOne так же, как мы использовали @OneToMany .

Примечание: Рекомендуется помещать сторону владения отношениями в класс/таблицу, в которой будет храниться внешний ключ.

Итак, в нашем случае эта вторая версия кода лучше. Но что, если мы все еще хотим, чтобы наш Учитель класс предлагал доступ к своему списку курсов//?

Мы можем сделать это, определив двунаправленные отношения:

@Entity
public class Teacher {
    // ...

    @OneToMany(mappedBy = "teacher")
    private List courses;
}

@Entity
public class Course {
    // ...
    
    @ManyToOne
    @JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID")
    private Teacher teacher;
}

Мы сохраняем наше @ManyToOne отображение на Курсе сущности. Однако мы также сопоставляем список Курсов с сущностью Учитель|/.

Что важно отметить здесь, так это использование флага mappedBy в аннотации @OneToMany на стороне ссылки .

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

С его помощью мы сообщаем JPA, что поле уже отображено другим объектом. Он отображается полем учитель объекта Курс .

Нетерпеливая и Ленивая загрузка

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

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

Это может отрицательно сказаться на производительности приложения. Технически это может быть решено с помощью Шаблона проектирования объекта передачи данных и извлечения Преподавателя информации без курсов.

Однако это может быть массовым перебором, если все, что мы получаем от шаблона, – это исключение курсов.

К счастью, JPA продумала все заранее и сделала Один ко многим отношения загруженными лениво по умолчанию.

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

В нашем примере это означало бы, что до тех пор, пока мы не вызовем метод Teacher#courses , курсы не будут извлечены из базы данных.

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

Мы можем изменить эти характеристики, установив аргумент fetch для обеих аннотаций:

Git Essentials

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

@OneToMany(mappedBy = "teacher", fetch = FetchType.EAGER)
private List courses;

@ManyToOne(fetch = FetchType.LAZY)
private Teacher teacher;

Это изменило бы то, как это работало изначально. Курсы будут загружаться с нетерпением, как только мы загрузим объект Учитель . В отличие от этого, учитель не будет загружен, когда мы выберем курсы , если в данный момент он не нужен.

Опциональность

Теперь давайте поговорим об опциональности.

Отношения могут быть необязательными или обязательными .

Учитывая сторону Один ко многим -это всегда необязательно, и мы ничего не можем с этим поделать. Сторона Многие к одному , с другой стороны, предлагает нам возможность сделать это обязательным .

По умолчанию связь необязательна, что означает, что мы можем сохранить Курс , не назначая ему преподавателя:

Course course = new Course("C# 101");
entityManager.persist(course);

А теперь давайте сделаем эти отношения обязательными. Для этого мы будем использовать необязательный аргумент аннотации @ManyToOne и установим его в false (по умолчанию это true ):

@ManyToOne(optional = false)
@JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID")
private Teacher teacher;

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

Course course = new Course("C# 101");
assertThrows(Exception.class, () -> entityManager.persist(course));

Но если мы дадим ему учителя, он снова будет работать нормально:

Teacher teacher = new Teacher();
teacher.setLastName("Doe");
teacher.setFirstName("Will");

Course course = new Course("C# 101");
course.setTeacher(teacher);

entityManager.persist(course);

Ну, по крайней мере, так казалось бы. Если бы мы запустили код, было бы выдано исключение:

javax.persistence.PersistenceException: org.hibernate.PersistentObjectException: detached entity passed to persist: com.fdpro.clients.stackabuse.jpa.domain.Course

Почему это так? Мы установили допустимый объект Учитель в объекте Курс , который мы пытаемся сохранить. Однако мы не сохраняли объект Учитель до попытки сохранить объект Курс .

Таким образом, объект Учитель не является управляемой сущностью . Давайте исправим это и попробуем еще раз:

Teacher teacher = new Teacher();
teacher.setLastName("Doe");
teacher.setFirstName("Will");
entityManager.persist(teacher);

Course course = new Course("C# 101");
course.setTeacher(teacher);

entityManager.persist(course);
entityManager.flush();

Выполнение этого кода сохранит обе сущности и сохранит связь между ними.

Каскадные Операции

Однако мы могли бы сделать и другое – мы могли бы каскадировать и, таким образом , распространять постоянство объекта Учитель , когда мы сохраняем объект Курс .

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

Для этого мы изменим флаг каскад аннотации:

@ManyToOne(optional = false, cascade = CascadeType.PERSIST)
@JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID")
private Teacher teacher;

Таким образом, Hibernate знает, что необходимый объект также должен сохраняться в этих отношениях.

Существует несколько типов каскадных операций: СОХРАНЕНИЕ , ОБЪЕДИНЕНИЕ , УДАЛЕНИЕ , ОБНОВЛЕНИЕ , ОТСОЕДИНЕНИЕ и ВСЕ (который объединяет все предыдущие).

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

Один к одному

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

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

Это, например, взаимосвязь между Курсом и его Материалом курса . Давайте сначала нанесем на карту Материалы курса , которые мы еще не сделали:

@Entity
public class CourseMaterial {
    @Id
    private Long id;
    private String url;
}

Аннотация для сопоставления одной сущности с одной другой сущностью, как ни странно, @OneToOne .

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

В нашем примере это был бы Материал курса , поскольку имеет смысл, что он ссылается на Курс (хотя мы могли бы пойти другим путем):

@OneToOne(optional = false)
@JoinColumn(name = "COURSE_ID", referencedColumnName = "ID")
private Course course;

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

Говоря о направлении, давайте сделаем отношения двунаправленными, чтобы мы могли получить доступ к материалу курса, если он у него есть. В классе Курс давайте добавим:

@OneToOne(mappedBy = "course")
private CourseMaterial material;

Здесь мы сообщаем Hibernate, что материал в пределах Курса уже отображен полем курса объекта CourseMaterial .

Кроме того, здесь нет необязательного атрибута, так как по умолчанию он true , и мы могли бы представить курс без материала (от очень ленивого преподавателя).

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

Многие ко многим

Теперь, последнее, но не менее важное: Отношения ” Многие ко многим /отношения. Мы сохранили их до конца, потому что они требуют немного больше работы, чем предыдущие.

Фактически, в базе данных отношение Многие ко многим включает в себя среднюю таблицу, ссылающуюся на обе другие таблицы.

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

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

Чтобы отобразить отношения Многие ко многим , мы будем использовать аннотацию @ManyToMany . Однако на этот раз мы также будем использовать аннотацию @JoinTable для настройки таблицы, представляющей взаимосвязь:

@ManyToMany
@JoinTable(
  name = "STUDENTS_COURSES",
  joinColumns = @JoinColumn(name = "COURSE_ID", referencedColumnName = "ID"),
  inverseJoinColumns = @JoinColumn(name = "STUDENT_ID", referencedColumnName = "ID")
)
private List students;

А теперь расскажите, что здесь происходит. Аннотация принимает несколько параметров. Прежде всего, мы должны дать таблице имя. Мы выбрали его STUDENTS_COURSES .

После этого нам нужно будет указать Hibernate, к каким столбцам присоединиться, чтобы заполнить STUDENTS_COURSES . Первый параметр joinColumns определяет, как настроить столбец соединения (внешний ключ) стороны – владельца связи в таблице. В этом случае сторона-владелец-это Курс .

С другой стороны, параметр inverseJoinColumns делает то же самое, но для стороны ссылки ( Студент ).

Давайте создадим набор данных со студентами и курсами:

Student johnDoe = new Student();
johnDoe.setFirstName("John");
johnDoe.setLastName("Doe");
johnDoe.setBirthDateAsLocalDate(LocalDate.of(2000, FEBRUARY, 18));
johnDoe.setGender(MALE);
johnDoe.setWantsNewsletter(true);
johnDoe.setAddress(new Address("Baker Street", "221B", "London"));
entityManager.persist(johnDoe);

Student willDoe = new Student();
willDoe.setFirstName("Will");
willDoe.setLastName("Doe");
willDoe.setBirthDateAsLocalDate(LocalDate.of(2001, APRIL, 4));
willDoe.setGender(MALE);
willDoe.setWantsNewsletter(false);
willDoe.setAddress(new Address("Washington Avenue", "23", "Oxford"));
entityManager.persist(willDoe);

Teacher teacher = new Teacher();
teacher.setFirstName("Jane");
teacher.setLastName("Doe");
entityManager.persist(teacher);

Course javaCourse = new Course("Java 101");
javaCourse.setTeacher(teacher);
entityManager.persist(javaCourse);

Course sqlCourse = new Course("SQL 101");
sqlCourse.setTeacher(teacher);
entityManager.persist(sqlCourse);

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

public class Course {

    private List students = new ArrayList<>();

    public void addStudent(Student student) {
        this.students.add(student);
    }
}

Теперь мы можем завершить наш набор данных:

Course javaCourse = new Course("Java 101");
javaCourse.setTeacher(teacher);
javaCourse.addStudent(johnDoe);
javaCourse.addStudent(willDoe);
entityManager.persist(javaCourse);

Course sqlCourse = new Course("SQL 101");
sqlCourse.setTeacher(teacher);
sqlCourse.addStudent(johnDoe);
entityManager.persist(sqlCourse);

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

Course courseWithMultipleStudents = entityManager.find(Course.class, 1L);

assertThat(courseWithMultipleStudents).isNotNull();
assertThat(courseWithMultipleStudents.students())
  .hasSize(2)
  .extracting(Student::firstName)
  .containsExactly("John", "Will");

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

Мы также можем каскадировать операции, а также определять, должны ли сущности загружаться лениво или нетерпеливо ( Отношения “Многие ко многим” по умолчанию являются ленивыми).

Вывод

На этом заканчивается эта статья о взаимоотношениях сопоставленных объектов с JPA. Мы рассмотрели Многие к одному , Один ко многим , Многие ко многим и Отношения Один к одному|/. Кроме того, мы рассмотрели каскадные операции, двунаправленность, возможность выбора и типы выборки с быстрой/медленной загрузкой.

Код для этой серии можно найти на GitHub .