Автор оригинала: 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 Listcourses;
Мы использовали Список
в качестве типа поля здесь, но мы могли бы выбрать Набор
или Карту
(хотя для этого требуется немного больше конфигурации ).
Как JPA отражает эту взаимосвязь в базе данных? Как правило, для такого типа отношений мы должны использовать внешний ключ в таблице.
JPA делает это за нас, учитывая наш вклад в то, как она должна справляться с отношениями. Это делается с помощью @JoinColumn
аннотации:
@OneToMany @JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID") private Listcourses;
Использование этой аннотации сообщит 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 Listcourses; } @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 Listcourses; @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 Liststudents;
А теперь расскажите, что здесь происходит. Аннотация принимает несколько параметров. Прежде всего, мы должны дать таблице имя. Мы выбрали его 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 Liststudents = 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 .