С помощью аннотаций JPA, когда мы используем Hibernate, мы можем управлять отношениями между двумя таблицами, как если бы они были объектами. Это упрощает сопоставление атрибутов базы данных с объектной моделью приложения. В зависимости от бизнес-логики и того, как мы моделируем, мы можем создавать однонаправленные или двунаправленные отношения.
@OneToOne (двунаправленный)
На следующем рисунке показана наша модель базы данных. student_id – это внешний ключ (отныне FK), который указывает на student.
Прежде всего, мы должны спросить себя, кто является владельцем этих отношений. Это подскажет нам, где должен находиться FK. Студент связан с обучением, и это обучение связано с уникальным студентом.
Хорошей практикой является использование каскада в родительском объекте, чтобы мы могли распространять изменения и применять их к дочерним элементам. В нашем примере не имеет смысла обучение существовать, если student не существует, следовательно, student будет иметь родительскую роль.
Если мы посмотрим на предыдущее изображение, я решил, что у меня есть FK. Мы можем думать, что знаем столько же, сколько владелец отношений, владелец этого FK (сторона-владелец) и студент, не владеющий отношениями (сторона, не являющаяся владельцем). Но как я могу создать двунаправленную связь, если студент хотел бы иметь свойства для обучения? Мы могли бы подумать о том, чтобы иметь еще одну студенческую сторону FKin, но это создало бы ненужную двойственность в нашей модели базы данных. Чтобы правильно сопоставить обе сущности, мы можем использовать аннотации @JoinColumn и сопоставить с помощью.
@Entity @Table(name = "student") public class Student { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @OneToOne(mappedBy = "student", cascade = CascadeType.ALL, orphanRemoval = true, fetch=FetchType.LAZY) private Tuition tuition; /* Getters and setters */ }
@Entity @Table(name = "tuition") public class Tuition { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private Double fee; //what column in Tuition table has the FK @JoinColumn(name = "student_id") @OneToOne(fetch = FetchType.LAZY) private Student student; /* Getters and setters */ }
@JoinColumn показывает имя столбца, на который мы хотели бы указать в таблице обучения. С помощью mappedBy мы можем создать двунаправленную связь, даже если у нас есть только один FK, мы можем связать обе таблицы. В конце концов, главная цель этих аннотаций – убедиться, где находится ключ, который отображает взаимосвязи.
orphanRemoval=true означает, что дочерняя сущность должна быть автоматически удалена ORM, если на нее больше не ссылается родительская сущность, например. у нас есть коллекция предметов, и мы удаляем один из них. У этого элемента сейчас нет ссылки, и он будет удален. Будьте осторожны, не путайте его с CascadeType, которые являются операциями на уровне базы данных.
FetchType=LAZY, извлекает сущность только тогда, когда она нам действительно нужна. Важно знать, что сеанс должен быть открыт, чтобы вызвать средство получения и извлечь объект, поскольку в режиме гибернации используется шаблон прокси (проксирование объектов). В противном случае (когда сеанс закрыт) сущность перейдет из постоянного состояния в отсоединение, и будет выдано исключение LazyInitializationException.
Давайте создадим очень простой тест, который проверяет запускаемую инструкцию sql.
@Test @Transactional @Rollback(false) public void check_sql_statement_when_persisting_in_one_to_one_bidirectional() { Student student = new Student(); student.setName("Ana"); Tuition tuition = new Tuition(); tuition.setFee(200); tuition.setStudent(student); student.setTuition(tuition); entityManager.persist(student); }
Использование каскада в родительской сущности гарантирует, что обучение будет сохраняться всякий раз, когда сохраняется студент.
Альтернативой предыдущему примеру является использование @MapsId. Как я уже говорил ранее, плата за обучение не имеет смысла, если студент не существует, с каждым студентом может быть связано только одно обучение. С помощью @MapsId мы говорим в режиме гибернации, что student_id – это PK обучения (первичный ключ), а также студенческий FK. Обе сущности теперь имеют одинаковое значение, и больше нет необходимости использовать @GeneratedValue для генерации новых идентификаторов в обучении.
@Entity @Table(name = "tuition") public class Tuition { @Id private Long id; private Double fee; @MapsId @OneToOne() private Student student; /* Getters and setters */ }
@OneToMany (двунаправленный)
На следующем рисунке показана наша модель базы данных. university_id – это FK, который указывает на университет.
В университете может быть много студентов, поэтому в университетском классе у нас будет @OneToMany. Студент связан только с одним университетом, поэтому мы используем @ManyToOne в студенческом классе. Сторона владельца этих отношений обычно находится в @ManyToOne и mappedBy в родительском объекте.
@Entity @Table(name = "university") public class University { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @OneToMany(mappedBy = "university", cascade = CascadeType.ALL, orphanRemoval = true) private Liststudents; /* Getters and setters */ }
@Entity @Table(name = "student") public class Student { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @ManyToOne @JoinColumn(name = "university_id") private University university; /* Getters and setters */ }
@OneToMany (однонаправленный)
В однонаправленной связи @OneToMany аннотация @JoinColumn указывает на таблицу “многих” (student в нашем примере). Из-за этого на следующем изображении мы видим @JoinColumn в университетском классе. Класс Student будет иметь только поля id и name.
@Entity @Table(name = "university") public class University { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @JoinColumn(name = "university_id") private Liststudents; /* Getters and setters */ }
Давайте создадим тест и проверим инструкции sql.
@Test @Transactional @Rollback(false) public void check_sql_statement_when_persisting_in_one_to_many_unidirectional() { University university = new University(); university.setName("Universidad de Las Palmas de Gran Canaria"); Student student1 = new Student(); student1.setName("Ana"); Student student2 = new Student(); student2.setName("Jonathan"); university.setStudents(List.of(student1, student2)); entityManager.persist(university); }
Почему в запросах есть обновления? Поскольку мы не сопоставляли с помощью FKin student (как в предыдущем примере), Hibernate должен запускать дополнительные запросы для решения этой проблемы. Гораздо лучше (рекомендуется) использовать @ManyToOne, если мы хотим однонаправленную связь или просто создаем двунаправленную связь. Мы будем избегать выполнения ненужных запросов.
@ManyToMany (двунаправленный)
Наша модель базы данных выглядит следующим образом
С помощью @ManyToMany мы должны создать третью таблицу, чтобы мы могли сопоставить обе сущности. Эта третья таблица будет иметь два FK, указывающих на их родительские таблицы. Следовательно, student_id указывает на таблицу student, а course_id указывает на таблицу course.
@Entity @Table(name="course") public class Course { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private Double fee; @ManyToMany(mappedBy = "courses") private Setstudents; /* Getters and setters */ }
@Entity @Table(name="student") public class Student { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE }) @JoinTable( name = "student_course", joinColumns = {@JoinColumn(name = "student_id")}, inverseJoinColumns = {@JoinColumn(name = "course_id")} ) private Setcourses; /* Getters and setters */ }
В этом примере я решил, что сторона-владелец – student, и именно здесь мы используем аннотацию @JoinTable. Мы должны указать там имя таблицы, которая связывает обе таблицы (student_course). Joincolumns указывает на таблицу стороны-владельца (студент), а InverseJoinColumns указывает на обратную таблицу стороны-владельца (курс). Я решил использовать Каскадное слияние и сохранение, но не каскад. Удалить, потому что, если я удалю курс, я не хочу удалять студентов с этого курса.
Как вы можете видеть в примере, я использую Set, а не List в своей ассоциации. Это связано с тем, что Hibernate удаляет все строки в student_course, связанные с этой сущностью, и повторно вставляет те, которые мы не хотели удалять. Это, конечно, неэффективно и не нужно. На следующем рисунке показаны инструкции sql, использующие List. Я пытаюсь удалить только один курс у студента, у которого есть 4 курса.
Помните об использовании Set, это позволит избежать такого рода нежелательного поведения.
Оригинал: “https://dev.to/jhonifaber/hibernate-onetoone-onetomany-manytoone-and-manytomany-8ba”