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

Как работает FetchType.EAGER?

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

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

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

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

# FetchType.EAGER в JPA и Hibernate

Краткий ответ

FetchType.EAGER означает, что связанные данные загружаются сразу вместе с основным объектом, в одном SQL запросе или через дополнительные запросы. Это контраст LAZY, который загружает данные только при обращении.

FetchType.LAZY (default)

По умолчанию для @OneToMany и @ManyToMany используется LAZY:

@Entity
public class User {
    @Id
    private Long id;
    private String name;
    
    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    private List<Order> orders; // загружается при вызове user.getOrders()
}

// Использование:
User user = userRepository.findById(1L);
// SQL: SELECT * FROM users WHERE id = 1

List<Order> orders = user.getOrders(); // ← НОВЫЙ SQL запрос!
// SQL: SELECT * FROM orders WHERE user_id = 1

FetchType.EAGER

С EAGER Hibernate загружает связанные данные сразу:

@Entity
public class User {
    @Id
    private Long id;
    private String name;
    
    @OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
    private List<Order> orders; // загружается СРАЗУ
}

// Использование:
User user = userRepository.findById(1L);
// SQL: SELECT u.*, o.* FROM users u
//      LEFT JOIN orders o ON u.id = o.user_id
//      WHERE u.id = 1

List<Order> orders = user.getOrders(); // уже в памяти

Как работает EAGER (изнутри)

Вариант 1: Inner/Left Join

@ManyToOne(fetch = FetchType.EAGER)
private Department department;

// Hibernate генерирует:
// SELECT e.*, d.* FROM employees e
// LEFT JOIN departments d ON e.dept_id = d.id
// WHERE e.id = 1

// Результат: один запрос, оба объекта загружены

Вариант 2: Дополнительный запрос (N+1 problem)

@OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
private List<Order> orders;

// Может привести к:
User user = userRepository.findById(1L);
// Query 1: SELECT * FROM users WHERE id = 1
// Query 2: SELECT * FROM orders WHERE user_id = 1 (для EAGER загрузки)

ManyToOne vs OneToMany EAGER

@ManyToOne и @OneToOne (default EAGER в ManyToOne)

@Entity
public class Employee {
    @ManyToOne(fetch = FetchType.EAGER) // default это EAGER!
    @JoinColumn(name = "dept_id")
    private Department department;
}

// Безопасно — обычно один связанный объект

@OneToMany и @ManyToMany (default LAZY)

@Entity
public class Department {
    @OneToMany(mappedBy = "department", fetch = FetchType.LAZY) // default
    private List<Employee> employees;
}

// LAZY по умолчанию, чтобы избежать загрузки тысяч объектов

Проблемы с EAGER

1. N+1 Problem

@Entity
public class Author {
    @Id
    private Long id;
    private String name;
    
    @OneToMany(mappedBy = "author", fetch = FetchType.EAGER)
    private List<Book> books; // плохо с EAGER!
}

// Код:
List<Author> authors = authorRepository.findAll();
// Query 1: SELECT * FROM authors; // 100 авторов
// Query 2-101: SELECT * FROM books WHERE author_id = 1;
//              SELECT * FROM books WHERE author_id = 2;
//              ... и т.д на каждого автора!

// Результат: 101 запрос вместо 1!

2. Cartesian Product (декартово произведение)

@Entity
public class Order {
    @ManyToOne(fetch = FetchType.EAGER)
    private User user;
    
    @OneToMany(mappedBy = "order", fetch = FetchType.EAGER)
    private List<OrderItem> items;
}

// SELECT o.*, u.*, oi.*
// FROM orders o
// LEFT JOIN users u
// LEFT JOIN order_items oi

// Если у заказа 5 items, результат будет 5 повторов заказа в SELECT
// Если у юзера 10 заказов по 5 items = 50 строк!
// Данные дублируются → много памяти, медленно

3. Непредсказуемое поведение

// Иногда EAGER может не сработать как ожидается:
findAll(); // может загрузить все
findAllByName("John"); // может НЕ загрузить все (зависит от реализации)

Решения вместо EAGER

Решение 1: EntityGraph

@Entity
public class User {
    @Id
    private Long id;
    private String name;
    
    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    private List<Order> orders;
}

// Repository
public interface UserRepository extends JpaRepository<User, Long> {
    @EntityGraph(attributePaths = {"orders"})
    Optional<User> findById(Long id);
}

// Использование: порядок управляет загрузкой
User user = userRepository.findById(1L);
// SELECT u.*, o.* FROM users u LEFT JOIN orders o ...

Решение 2: FetchProfile (в Hibernate)

@Entity
@FetchProfile(name = "userWithOrders",
    fetchOverrides = {
        @FetchProfile.FetchOverride(
            association = "orders",
            mode = FetchMode.JOIN
        )
    }
)
public class User {
    // ...
}

// Использование:
session.enableFetchProfile("userWithOrders");
User user = session.get(User.class, 1L);

Решение 3: Явная загрузка (LAZY + query)

@Entity
public class User {
    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    private List<Order> orders;
}

// JPQL запрос с JOIN FETCH
String jpql = "SELECT u FROM User u " +
              "JOIN FETCH u.orders " +
              "WHERE u.id = :id";
User user = entityManager.createQuery(jpql, User.class)
    .setParameter("id", 1L)
    .getSingleResult();

Решение 4: DTO (рекомендуется)

public class UserWithOrdersDTO {
    private Long userId;
    private String userName;
    private List<OrderDTO> orders;
}

// Сразу загружаем нужные данные
String jpql = "SELECT new com.example.UserWithOrdersDTO(" +
              "u.id, u.name, o) " +
              "FROM User u " +
              "LEFT JOIN u.orders o " +
              "WHERE u.id = :id";

Рекомендации

Когда можно использовать EAGER

// ✓ @ManyToOne — всегда один объект
@Entity
public class Employee {
    @ManyToOne(fetch = FetchType.EAGER) // Ok
    private Department department;
}

// ✓ @OneToOne — максимум один объект
@Entity
public class User {
    @OneToOne(fetch = FetchType.EAGER) // Ok
    private Profile profile;
}

// ✓ Маленькие коллекции
@OneToMany(fetch = FetchType.EAGER) // если список всегда 1-2 элемента
private List<Tag> tags;

Когда НЕ использовать EAGER

// ❌ @OneToMany с большой коллекцией
@OneToMany(fetch = FetchType.EAGER)
private List<Order> orders; // может быть 1000+

// ❌ Множественные EAGER отношения
@OneToMany(fetch = FetchType.EAGER)
private List<Order> orders;

@OneToMany(fetch = FetchType.EAGER)
private List<Payment> payments;

Лучшая практика

// ВСЕГДА используй LAZY по умолчанию
@OneToMany(mappedBy = "user")
private List<Order> orders; // LAZY по умолчанию

// Явно указывай, что загружать, когда нужно
@EntityGraph(attributePaths = {"orders"})
User findUserWithOrders(Long id);

// Или используй JOIN FETCH в запросе

Вывод

  1. FetchType.EAGER загружает данные сразу, вместе с основным объектом
  2. LAZY (по умолчанию) загружает только при обращении
  3. Проблема: EAGER может привести к N+1 и картезианскому произведению
  4. Решение: Используй EntityGraph, JOIN FETCH или DTO
  5. Правило: LAZY по умолчанию, явно загружай нужное, когда нужно