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

Какой тип загрузки используется в JOIN FETCH?

2.2 Middle🔥 161 комментариев
#Основы Java

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

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

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

JOIN FETCH и Типы Загрузки в Hibernate/JPA

Основной Ответ

JOIN FETCH использует Eager Loading (нетерпеливую загрузку) — все связанные данные загружаются в одном SQL запросе, а не в отдельных.

Три Типа Загрузки в Hibernate

// Тип 1: LAZY Loading (по умолчанию для @OneToMany, @ManyToMany)
@Entity
public class User {
    @Id
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)  // Default!
    private List<Order> orders;  // Загружается только когда обращаемся
}

// Использование:
User user = entityManager.find(User.class, 1L);
// SELECT * FROM users WHERE id = 1  ← 1 запрос

List<Order> orders = user.getOrders();  // Аааа! Еще N+1 запросов!
// SELECT * FROM orders WHERE user_id = 1  ← запрос ленивой загрузки
for (Order order : orders) {
    System.out.println(order.getOrderNumber());
}

// Проблема: N+1 query problem!
// Если user.getOrders() вернул 100 заказов, будет еще 100 запросов!


// Тип 2: EAGER Loading (по умолчанию для @ManyToOne, @OneToOne)
@Entity
public class Order {
    @Id
    private Long id;
    
    @ManyToOne(fetch = FetchType.EAGER)  // Загружает сразу!
    private User user;  // Загружается одновременно с Order
}

// Использование:
Order order = entityManager.find(Order.class, 1L);
// SELECT o.*, u.* FROM orders o JOIN users u ON o.user_id = u.id WHERE o.id = 1
// ↑ Один запрос с JOIN!

User user = order.getUser();  // Уже в памяти, нет нового запроса


// Тип 3: JOIN FETCH (явная нетерпеливая загрузка)
// Это решение N+1 problem для @OneToMany и @ManyToMany

N+1 Query Problem Визуально

Без JOIN FETCH (LAZY loading):
Получить пользователя с его заказами
    |
    └─ SELECT * FROM users WHERE id = 1             ← 1 запрос
       |
       └─ user.getOrders() {
            ├─ SELECT * FROM orders WHERE user_id = 1  ← N запросов (на каждый user!)
            ├─ SELECT * FROM orders WHERE user_id = 1
            ├─ SELECT * FROM orders WHERE user_id = 1
            └─ ... (N больше запросов для N пользователей)
       }

С JOIN FETCH (EAGER loading):
Получить пользователя с его заказами
    |
    └─ SELECT u.*, o.* FROM users u 
       JOIN orders o ON u.id = o.user_id 
       WHERE u.id = 1                    ← 1 запрос (всё вместе!)

Пример N+1 Problem

// ПЛОХО: N+1 query problem
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    List<User> findAll();  // Без JOIN FETCH!
}

// Использование:
List<User> users = userRepository.findAll();
// SELECT * FROM users                            ← 1 запрос

for (User user : users) {
    for (Order order : user.getOrders()) {  // PROBLEM HERE!
        // SELECT * FROM orders WHERE user_id = 1  ← 1-й пользователь
        // SELECT * FROM orders WHERE user_id = 2  ← 2-й пользователь
        // SELECT * FROM orders WHERE user_id = 3  ← 3-й пользователь
        // ... (если 1000 пользователей, то 1001 запрос!)
        System.out.println(order.getOrderNumber());
    }
}

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


// ХОРОШО: JOIN FETCH решает N+1
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    @Query("SELECT u FROM User u JOIN FETCH u.orders")
    List<User> findAllWithOrders();  // Join fetch в JPQL!
}

// Использование:
List<User> users = userRepository.findAllWithOrders();
// SELECT u.*, o.* FROM users u 
// JOIN orders o ON u.id = o.order_id  ← 1 запрос с JOIN!

for (User user : users) {
    for (Order order : user.getOrders()) {  // Всё уже в памяти!
        System.out.println(order.getOrderNumber());
    }
}

// Результат: 1 запрос! (или 2 если есть сортировка/паджинейшн)

Синтаксис JOIN FETCH

В JPQL

// JPQL с JOIN FETCH
@Query("SELECT u FROM User u JOIN FETCH u.orders WHERE u.id = ?1")
User findByIdWithOrders(Long userId);

// Несколько JOIN FETCH
@Query("""n    SELECT u FROM User u 
    JOIN FETCH u.orders o
    JOIN FETCH o.items
    WHERE u.id = ?1
    """)
User findByIdWithOrdersAndItems(Long userId);

// С фильтрацией
@Query("""n    SELECT DISTINCT u FROM User u 
    JOIN FETCH u.orders o
    WHERE u.id = ?1 AND o.status = 'COMPLETED'
    """)
User findByIdWithCompletedOrders(Long userId);

В Criteria API

@Repository
public class UserRepositoryImpl {
    @PersistenceContext
    private EntityManager em;
    
    public User findByIdWithOrders(Long userId) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<User> cq = cb.createQuery(User.class);
        Root<User> root = cq.from(User.class);
        
        root.fetch("orders", JoinType.LEFT);  // LEFT FETCH
        cq.where(cb.equal(root.get("id"), userId));
        
        return em.createQuery(cq).getSingleResult();
    }
}

// Или в Spring Data JPA EntityGraph
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    @EntityGraph(attributePaths = {"orders", "orders.items"})
    User findById(Long id);
}

LEFT JOIN FETCH vs INNER JOIN FETCH

// INNER JOIN FETCH — вернет только пользователей С заказами
@Query("SELECT u FROM User u JOIN FETCH u.orders")
List<User> findAllWithOrders();
// Если у пользователя нет заказов, он не будет в результате!

// LEFT JOIN FETCH — вернет ВСЕ пользователей, даже без заказов
@Query("SELECT u FROM User u LEFT JOIN FETCH u.orders")
List<User> findAllWithOrdersOrEmpty();
// Пользователь с пустым списком заказов тоже будет в результате

// В реальности:
// LEFT FETCH в 95% случаев — это то, что нужно!

Осторожно с DISTINCT

// Проблема: DISTINCT на уровне БД может не работать
@Query("""n    SELECT DISTINCT u FROM User u 
    LEFT JOIN FETCH u.orders
    """)
List<User> findAll();  // DISTINCT на уровне БД не сработает с JOIN!

// Решение: DISTINCT на уровне приложения
@Query("""n    SELECT DISTINCT u FROM User u 
    LEFT JOIN FETCH u.orders
    """)
Set<User> findAll();  // Set автоматически удалит дубликаты

// Или вручную
List<User> users = em.createQuery(
    "SELECT DISTINCT u FROM User u LEFT JOIN FETCH u.orders",
    User.class
).setHint("hibernate.query.passDistinctThrough", false)
 .getResultList();

Производительность: Сравнение

Сценарий: 1000 пользователей, 5 заказов на пользователя

1. LAZY Loading (без JOIN FETCH):
   - 1 запрос для findAll()
   - 1000 запросов при обращении к getOrders()
   - Итого: 1001 запрос ✗
   - Время: 500ms

2. EAGER Loading (с @ManyToOne):
   - 1 запрос с JOIN для каждого orders (если прямой доступ)
   - Может вернуться 5000 строк (1000 * 5)
   - Итого: 1 запрос (но большой) ✓
   - Время: 50ms

3. JOIN FETCH:
   - 1 запрос с LEFT JOIN на уровне JPQL
   - Вернется 5000 строк (1000 * 5)
   - Hibernate "склеит" обратно в 1000 объектов User
   - Итого: 1 запрос ✓
   - Время: 50ms

Когда Использовать

Используй JOIN FETCH:

  • Нужно получить сущность + её коллекции в одном запросе
  • Хочешь избежать N+1 problem
  • Доступ к связям будет точно нужен (если не знаешь, используй)
  • Collection имеет @OneToMany или @ManyToMany

Не используй JOIN FETCH:

  • Не будешь обращаться к связям (зачем загружать?)
  • Нужна паджинейшн (сложно с JOIN FETCH)
  • Коллекция очень большая (тысячи записей)

Боль с JOIN FETCH и Паджинейшн

// Проблема: JOIN FETCH не работает хорошо с паджинейшном
@Query("SELECT u FROM User u LEFT JOIN FETCH u.orders")
Page<User> findAll(Pageable pageable);  // ВНИМАНИЕ: может вернуть неправильный результат!

// Решение 1: оставить LAZY и воспользоваться @Transactional
@Transactional
public List<User> getUsers() {
    List<User> users = userRepository.findAll(PageRequest.of(0, 10)).getContent();
    // Инициализируем коллекции в рамках транзакции
    users.forEach(u -> u.getOrders().size());
    return users;
}

// Решение 2: использовать batch loading
@Query("SELECT u FROM User u WHERE u.id IN (?1)")
@Fetch(FetchMode.SUBSELECT)
List<User> findByIds(List<Long> ids);

// Решение 3: две отдельные query
@Query("SELECT u FROM User u")
Page<User> findAll(Pageable pageable);

@Query("SELECT u FROM User u LEFT JOIN FETCH u.orders WHERE u.id IN (?1)")
List<User> findWithOrders(List<Long> userIds);

Вывод

JOIN FETCH использует EAGER Loading потому что:

  1. Загружает всё в одном SQL запросе (с JOIN)
  2. Предотвращает N+1 problem — вместо 1001 запроса будет 1
  3. Явно говорит ORM что нужно загружать связи
  4. Это золотая середина между LAZY и EAGER

Если забыл JOIN FETCH и запустил query в цикле по коллекции — будет N+1 problem. Помни об этом!