null

Все, что вы боялись спросить про JPA @OneToMany

Аннотация @OneToMany в JPA используется для обозначения отношения "один ко многим" между сущностями. Разница между unidirectional (однонаправленным) и bidirectional (двунаправленным) отношением заключается в том, как эти отношения реализованы в коде и как они влияют на поведение сущностей в контексте базы данных и ORM.

1. Однонаправленное отношение (Unidirectional @OneToMany)

В этом случае связь существует только с одной стороны — у родительской сущности есть список дочерних, но дочерние сущности не знают о родительской.

Пример:
 

@Entity
public class Parent {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany
    @JoinColumn(name = "parent_id")  // Связь через внешний ключ в дочерней таблице
    private List<Child> children = new ArrayList<>();

    // Геттеры и сеттеры
}

@Entity
public class Child {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    // Геттеры и сеттеры
}

В данном случае сущность Parent знает о своих дочерних сущностях через коллекцию children, но у дочерней сущности Child нет ссылки на родительскую.

Плюсы однонаправленного отношения:

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

Минусы:

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

2. Двунаправленное отношение (Bidirectional @OneToMany)

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

Пример:
 

@Entity
public class Parent {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Child> children = new ArrayList<>();

    // Геттеры и сеттеры
}

@Entity
public class Child {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;

    // Геттеры и сеттеры
}

В этом примере у сущности Parent есть коллекция children, а у сущности Child есть ссылка на родителя через поле parent. То есть, можно навигировать как от родителя к детям, так и от ребенка к родителю.

Плюсы двунаправленного отношения:

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

Минусы:

  • Более сложная структура, так как необходимо синхронизировать обе стороны отношения.
  • Увеличение сложности кода и потенциальная необходимость использования методов для синхронизации (например, добавления дочерних элементов к родителю и установки родителя для каждого дочернего элемента).

Вывод:

  • Однонаправленное отношение проще в реализации и использует меньше ресурсов, но оно ограничено в навигации.
  • Двунаправленное отношение дает больше возможностей для манипуляции данными, но требует более сложной настройки и поддержки.

Давайте немного углубимся в детали и разберемся с атрибутами и аннотациями, которые используются вместе с @OneToMany/@ManyToOne.


Зачем нужен атрибут mappedBy

Атрибут mappedBy указывает на поле в другой сущности, которое управляет отношением. В случае отношения @OneToMany, оно указывает на поле с @ManyToOne, которое владеет внешним ключом в базе данных. Именно сторона с @ManyToOne является владельцем связи, потому что именно на этой стороне находится внешний ключ, и она контролирует, как данные сохраняются в базе данных.
Посмотрим на происходящее в примере выше с точки зрения mappedBy:

  • В классе Parent аннотация @OneToMany использует атрибут mappedBy = "parent", который указывает на поле parent в классе Child.
  • В классе Child аннотация @ManyToOne является владельцем связи, так как именно здесь находится внешний ключ (parent_id).

Если в двунаправленном отношении @OneToMany не указать атрибут mappedBy, то JPA будет считать, что обе стороны управляют связью. Это приведет к созданию двух отдельных схем для связи: 

  1. Создание внешнего ключа в таблице дочерних сущностей:
    • Связь @ManyToOne на стороне дочерней сущности приведет к созданию колонки (например, parent_id) в таблице дочерних сущностей, которая будет хранить внешний ключ на родителя.
  2. Создание промежуточной таблицы:
    • Так как @OneToMany на стороне родительской сущности не использует mappedBy, JPA будет считать, что родительская сущность также управляет связью, и создаст промежуточную таблицу для этой связи. Эта таблица будет содержать внешние ключи на обе стороны (например, parent_id и child_id).

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

 

Когда использовать аннотацию @JoinColumn

Когда отношение @OneToMany является однонаправленным, JPA не может напрямую хранить внешний ключ в таблице дочерних сущностей, так как дочерняя сторона не владеет этой связью и не имеет явной ссылки на родителя. В таких случаях JPA автоматически создает промежуточную таблицу для управления связью между родительской и дочерней сущностями. Аннотация @JoinColumn позволяет JPA правильно сопоставлять дочерние сущности с родительской, указывая на столбец, который будет выступать в роли внешнего ключа в отношении между сущностями. 
Кроме того, @JoinColumn позволяет явно задать имя этого столбца, что может быть полезно для соответствия с существующей схемой базы данных или для улучшения читаемости (по умолчанию JPA сгенерирует имя, основываясь на названии связанной сущности и её идентификатора).

Например, если класс Child имеет поле parent, представляющее связь @ManyToOne, аннотация @JoinColumn будет указывать на столбец, который хранит внешний ключ (в нашем случае, parent_id).

В двунаправленных отношениях типа @OneToMany mappedBy указывает, что внешний ключ управляется другой стороной, и в этом случае использование @JoinColumn на обратной стороне не нужно.

 

Связующая таблица @JoinTable 

Если же вы не хотите хранить внешний ключ в таблице владельца связи, а вместо этого предпочитаете использовать промежуточную (связывающую) таблицу, то можно заменить аннотацию @JoinColumn на @JoinTable в отношении @OneToMany, Это может быть полезно, когда требуется избежать добавления внешнего ключа в таблицу владельца или для случаев, когда структура базы данных требует использования отдельной таблицы, которая будет содержать два внешних ключа — один для владельца связи (родительской сущности) и один для дочерней сущности.

Пример однонаправленного отношения @OneToMany с явно заданной связующей таблицей.

@Entity
public class Parent {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany
    @JoinTable(
        name = "parent_child_link", // Имя промежуточной таблицы
        joinColumns = @JoinColumn(name = "parent_id"), // Внешний ключ на таблицу Parent
        inverseJoinColumns = @JoinColumn(name = "child_id") // Внешний ключ на таблицу Child
    )
    private List<Child> children = new ArrayList<>();

    // Геттеры и сеттеры
}

@Entity
public class Child {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    // Геттеры и сеттеры
}

Что здесь происходит:

  1. @JoinTable: указывает, что для отношения @OneToMany между Parent и Child будет использоваться промежуточная таблица.
  2. name = "parent_child_link": это имя промежуточной таблицы, которая будет хранить связи между сущностями Parent и Child.
  3. joinColumns = @JoinColumn(name = "parent_id"): внешний ключ на сущность Parent, который будет храниться в промежуточной таблице.
  4. inverseJoinColumns = @JoinColumn(name = "child_id"): внешний ключ на сущность Child, который также будет храниться в этой промежуточной таблице.

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

1)  Используем атрибут mappedBy:

@Entity
public class Parent {
    @Id
    private Long id;

    @OneToMany(mappedBy = "parent")
    private List<Child> children;
}

@Entity
public class Child {
    @Id
    private Long id;

    @ManyToOne
    @JoinTable(
        name = "parent_child_link",
        joinColumns = @JoinColumn(name = "child_id"),
        inverseJoinColumns = @JoinColumn(name = "parent_id")
    )
    private Parent parent;
}

В этом примере:

  • @JoinTable: указывает, что для отношения @OneToMany между Parent и Child будет использоваться промежуточная таблица "parent_child_link".
  • mappedBy = "parent": указывает, что отношением управляет поле parent в классе Child.

Важно напомнить, что если не указать атрибут mappedBy, JPA будет считать, что родительская сущность также управляет связью, и создаст промежуточную таблицу для этой связи. В результате, валидатор схемы Hibernate найдет несоответствие между объектной моделью и схемой в БД:

Caused by: javax.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is org.hibernate.tool.schema.spi.SchemaManagementException: Schema-validation: missing table [parent_children]

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

2) Задаем связующую таблицу и в родительской, и в дочерней сущностях:

@Entity
public class Parent {
    @Id
    private Long id;

    @OneToMany
    @JoinTable(
        name = "parent_child",
        joinColumns = @JoinColumn(name = "parent_id"),
        inverseJoinColumns = @JoinColumn(name = "child_id")
    )
    private List<Child> children;
}

@Entity
public class Child {
    @Id
    private Long id;

    @ManyToOne
    @JoinTable(
        name = "parent_child",
        joinColumns = @JoinColumn(name = "child_id"),
        inverseJoinColumns = @JoinColumn(name = "parent_id")
    )
    private Parent parent;
}

Таким образом, две однонаправленные связи корректно отображаются на схему данных в БД, так как валидатор проверяет наличие связующей таблицы по ее названию, переопределенному с помощью аннотации @JoinTable.

@JoinTable vs @JoinColumn

Когда стоит использовать @JoinTable вместо @JoinColumn:

  1. Когда структура базы данных или бизнес-логика требуют использования отдельной таблицы для связи между сущностями.
  2. Если не хотите засорять таблицу дочерних сущностей (например, Child) внешними ключами и предпочитаете хранить связи в отдельной таблице.
  3. Для реализации сложных связей, где использование внешнего ключа недостаточно или нецелесообразно.

Когда лучше использовать @JoinColumn:

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