← Назад к вопросам

Как работает @OneToMany?

2.0 Middle🔥 171 комментариев
#ORM и Hibernate

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Аннотация @OneToMany в Hibernate/JPA

@OneToMany описывает отношение один-ко-многим между двумя сущностями. Один объект одного типа связан с несколькими объектами другого типа.


Диаграмма отношения

Одна категория — много товаров

┌──────────┐
│ Category │ 1
│  (id=1)  │────────┬─────────┬─────────┐
└──────────┘        │         │         │
                    N         N         N
              ┌─────────┐ ┌────────┐ ┌────────┐
              │ Product │ │Product │ │Product │
              │  id=10  │ │ id=11  │ │ id=12  │
              └─────────┘ └────────┘ └────────┘

Базовый пример

Структура БД:

CREATE TABLE categories (
  id BIGINT PRIMARY KEY,
  name VARCHAR(100)
);

CREATE TABLE products (
  id BIGINT PRIMARY KEY,
  name VARCHAR(100),
  category_id BIGINT NOT NULL,
  FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
);

Java классы:

@Entity
@Table(name = "categories")
public class Category {
  
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  
  private String name;
  
  // Со стороны "one" (родитель)
  @OneToMany(mappedBy = "category")  // mappedBy указывает имя поля в Product
  private List<Product> products = new ArrayList<>();  // никогда не null!
  
  // getters/setters
  public void addProduct(Product product) {
    products.add(product);
    product.setCategory(this);
  }
}

@Entity
@Table(name = "products")
public class Product {
  
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  
  private String name;
  
  // Со стороны "many" (ребёнок)
  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "category_id", nullable = false)  // название колонки в БД
  private Category category;
  
  // getters/setters
}

Как работает @OneToMany: подробно

1. Направление отношения

@OneToMany всегда имеет две стороны:

// Сторона "one" (управляющая сторона логически)
public class Category {
  @OneToMany(mappedBy = "category")  // НЕ создаёт FK!
  private List<Product> products;     // только отображение
}

// Сторона "many" (технически управляющая — откуда FK)
public class Product {
  @ManyToOne
  @JoinColumn(name = "category_id")  // ВОТ ЗДЕСЬ FK в таблице!
  private Category category;
}

Важно: mappedBy означает "это поле управляется другой стороной". Hibernate не создаёт FK на стороне @OneToMany, а читает его со стороны @ManyToOne.

2. Загрузка данных (Lazy vs Eager)

// ❌ По умолчанию LAZY (тяжелая ошибка для N+1)
@OneToMany(mappedBy = "category")
private List<Product> products;  // загружается только при доступе

// ❌ Иногда нужно EAGER (но осторожно!)
@OneToMany(mappedBy = "category", fetch = FetchType.EAGER)
private List<Product> products;  // всегда загружается вместе с Category

Проблема N+1:

// Загрузим 100 категорий
List<Category> categories = session.createQuery(
  "SELECT c FROM Category c", Category.class
).getResultList();  // 1 SQL

// Каждый раз при доступе к products — отдельный запрос
for (Category cat : categories) {
  System.out.println(cat.getProducts().size());  // 100 дополнительных SQL!
}
// Итого: 1 + 100 = 101 SQL запрос!

Решение — использовать JOIN FETCH:

List<Category> categories = session.createQuery(
  "SELECT DISTINCT c FROM Category c " +
  "LEFT JOIN FETCH c.products",
  Category.class
).getResultList();  // только 1 SQL с JOIN!

3. Каскадные операции (Cascade)

Определяют, что происходит с зависимыми объектами:

// Каскад удаления
@OneToMany(mappedBy = "category", cascade = CascadeType.REMOVE)
private List<Product> products;

// При удалении Category — все её Products удалятся!
Category cat = session.get(Category.class, 1L);
session.delete(cat);  // удалит category И все products

Типы каскада:

cascade = {
  CascadeType.PERSIST,   // сохранить children при сохранении parent
  CascadeType.MERGE,     // слить при merge
  CascadeType.REMOVE,    // удалить children при удалении parent
  CascadeType.REFRESH,   // обновить при refresh
  CascadeType.DETACH,    // открепить при detach
  CascadeType.ALL        // все выше
}

⚠️ Внимание: используй CascadeType.REMOVE с осторожностью!


Две формы @OneToMany

Форма 1: С @ManyToOne на другой стороне (РЕКОМЕНДУЕТСЯ)

// Category.java
@OneToMany(mappedBy = "category")  // управляется Product
private List<Product> products;    // только для отображения

// Product.java
@ManyToOne
@JoinColumn(name = "category_id")  // РЕАЛЬНАЯ СВЯЗЬ в БД
private Category category;          // управляет связью

Плюсы:

  • Чётко определена сторона с FK
  • Меньше проблем
  • Легче управлять

Форма 2: Самостоятельный @OneToMany (без @ManyToOne)

@OneToMany
@JoinColumn(name = "category_id")  // создаёт свой FK
private List<Product> products;    // управляет связью сама

Минусы:

  • На стороне Product нет информации о Category
  • Сложнее в использовании
  • Не рекомендуется

Практические сценарии

Сценарий 1: Добавление товара в категорию

Category cat = session.get(Category.class, 1L);
Product product = new Product();
product.setName("Ноутбук");
product.setCategory(cat);  // устанавливаем мать
cat.getProducts().add(product);  // добавляем в список

session.save(product);
// Hibernate создаст Product с category_id = 1

Сценарий 2: Получение товаров без N+1

// HQL с JOIN FETCH
List<Category> categories = session.createQuery(
  "SELECT c FROM Category c " +
  "LEFT JOIN FETCH c.products p",
  Category.class
).getResultList();

// Или с criteria API
CriteriaQuery<Category> query = cb.createQuery(Category.class);
Root<Category> root = query.from(Category.class);
root.fetch("products", JoinType.LEFT);
query.select(root).distinct(true);
query.orderBy(cb.asc(root.get("id")));

return em.createQuery(query).getResultList();

Сценарий 3: Удаление с каскадом

@OneToMany(mappedBy = "category", cascade = CascadeType.REMOVE)
private List<Product> products;

// При удалении категории удалятся все товары
Category cat = session.get(Category.class, 1L);
session.delete(cat);
// SQL: DELETE FROM products WHERE category_id = 1;
//      DELETE FROM categories WHERE id = 1;

Частые ошибки

❌ Инициализировать как null:

private List<Product> products;  // null!

// LazyInitializationException при доступе вне сессии
Category cat = getCategoryWithoutSession();
cat.getProducts().size();  // ❌ BOOM!

✅ Инициализировать как пустой список:

private List<Product> products = new ArrayList<>();  // никогда null

❌ Забыть про N+1:

for (Category cat : categories)  // 100 категорий + 100 запросов к products
  System.out.println(cat.getProducts().size());

✅ Использовать JOIN FETCH:

// Один запрос с JOIN
"SELECT DISTINCT c FROM Category c LEFT JOIN FETCH c.products"

Итог

@OneToMany — это объявление отношения один-ко-многим:

  • Логически находится на стороне "один"
  • Технически FK находится на стороне "много" (@ManyToOne)
  • Всегда используй с mappedBy чтобы указать обратное поле
  • Осторожно с N+1, используй JOIN FETCH
  • Инициализируй как new ArrayList<>()