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

Спящий режим – OneToOne, OneToMany, ManyToOne и ManyToMany

С помощью аннотаций JPA, когда мы используем Hibernate, мы можем управлять отношениями между двумя… Помеченный как java, hibernate, jpa.

С помощью аннотаций 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 List students;

    /* 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 List students;

    /* 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 Set students;

    /* 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 Set courses;

    /* 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”