Вступление
В этой статье мы погрузимся в сопоставление отношений с JPA и Hibernate в Java .
Java Persistence API (JPA) - это стандарт устойчивости экосистемы
Java. Это позволяет нам сопоставить нашу модель предметной области
непосредственно со структурой базы данных, а затем дает нам гибкость в
манипулировании объектами в нашем коде - вместо того, чтобы возиться с
громоздкими компонентами JDBC, такими как Connection
, ResultSet
и
т. Д.
Мы составим исчерпывающее руководство по использованию JPA с Hibernate в качестве поставщика. В этой статье мы рассмотрим сопоставления отношений.
- Руководство по JPA с Hibernate - Базовое сопоставление
- Руководство по JPA с Hibernate - Отображение отношений (вы здесь)
- Руководство по JPA с Hibernate: Сопоставление наследования ( скоро! )
- Руководство по 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 .