Руководство по JPA с Hibernate - Отображение отношений

Введение В этой статье мы углубимся в отображение отношений с помощью JPA и Hibernate в Java. Java Persistence API (JPA) - это стандарт устойчивости экосистемы Java. Это позволяет нам сопоставить нашу модель предметной области непосредственно со структурой базы данных, а затем дает нам гибкость в манипулировании объектами в нашем коде - вместо того, чтобы возиться с громоздкими компонентами JDBC, такими как Connection, ResultSet и т. Д. Мы составим исчерпывающее руководство по использованию JPA с Hibernate в качестве поставщика. В этом

Вступление

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

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

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

Наш пример

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

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

модель предметнойобласти{.ezlazyload}

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

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

Отношения

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

Учителя и курсы - студенты и курсы - курсы и учебные материалы.

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

Есть несколько типов отношений:

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

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

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

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

Что такое отношения « один ко многим» ?

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

В нашем примере это Teacher и его Courses . Учитель может дать несколько курсов, но курс дается только один учитель (это многие-к-одной точки зрения - многие курсы к одному учителю).

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

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

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

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

 @OneToMany 
 private List<Course> courses; 

Мы использовали здесь List в качестве типа поля, но мы могли бы выбрать Set или Map (хотя для этого требуется немного больше настроек ).

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

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

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

Использование этой аннотации сообщит JPA, что COURSE должна иметь столбец внешнего ключа TEACHER_ID который ссылается на столбец ID таблицы TEACHER

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

 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"); 

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

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

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

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

Course называется ссылочной стороной в этих отношениях.

Мы могли бы сделать Course стороной-владельцем отношений, сопоставив вместо этого поле Teacher @ManyToOne в Course

 @ManyToOne 
 @JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID") 
 private Teacher 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 .

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

Итак, в нашем случае вторая версия кода лучше. Но что, если мы по-прежнему хотим, чтобы наш Teacher предлагал доступ к своему списку Course

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

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

Мы сохраняем отображение @ManyToOne в сущности Course Однако мы также сопоставляем список Course с сущностью Teacher

Что важно отметить здесь является использование mappedBy флага в @OneToMany аннотацию на стороне ссылающейся.

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

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

Жажда против ленивой загрузки

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

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

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

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

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

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

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

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

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

 @OneToMany(mappedBy = "teacher", fetch = FetchType.EAGER) 
 private List<Course> courses; 
 
 @ManyToOne(fetch = FetchType.LAZY) 
 private Teacher teacher; 

Это полностью изменило бы то, как это работало изначально. Курсы будут загружаться быстро, как только мы загрузим объект Teacher Напротив, teacher не загружался бы, когда мы выбираем courses если он не нужен в то время.

Необязательность

Теперь поговорим о необязательности.

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

Что касается стороны « один ко многим» - это всегда необязательно, и мы ничего не можем с этим поделать. С другой стороны, сторона Many-to-One предлагает нам возможность сделать это обязательным .

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

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

Теперь давайте сделаем это отношение обязательным. Для этого мы воспользуемся optional аргументом @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 Course мы пытаемся сохранить. Однако мы не сохранили объект Teacher до попытки сохранить объект Course

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

 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(); 

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

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

Однако мы могли бы сделать другое - мы могли бы каскадировать и, таким образом, распространять постоянство Teacher когда мы сохраняем объект Course

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

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

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

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

Есть несколько типов каскадные операции: PERSIST , MERGE , REMOVE , REFRESH , DETACH и ALL (который сочетает в себе все предыдущие).

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

Один к одному

Теперь, когда мы создали основы сопоставления отношений в JPA через отношения « один-ко-многим / многие-к-одному» и их настройки, мы можем перейти к отношениям « один-к-одному» .

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Итак, в нашем примере отношения « многие ко многим» будут отношениями между Student и Course поскольку студент может посещать несколько курсов, а за курсом могут следовать несколько студентов.

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

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

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

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

С другой стороны, inverseJoinColumns делает то же самое, но для ссылающейся стороны ( Student ).

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

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

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

 public class Course { 
 
 private List<Student> 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 , Teacher и Student а также их отношения. Например, давайте извлечем студента из сохраненного курса и проверим, все ли в порядке:

 Course courseWithMultipleStudents = entityManager.find(Course.class, 1L); 
 
 assertThat(courseWithMultipleStudents).isNotNull(); 
 assertThat(courseWithMultipleStudents.students()) 
 .hasSize(2) 
 .extracting(Student::firstName) 
 .containsExactly("John", "Will"); 

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

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

Заключение

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

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

comments powered by Disqus