← Назад к вопросам
Какой тип загрузки используется в 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 потому что:
- Загружает всё в одном SQL запросе (с JOIN)
- Предотвращает N+1 problem — вместо 1001 запроса будет 1
- Явно говорит ORM что нужно загружать связи
- Это золотая середина между LAZY и EAGER
Если забыл JOIN FETCH и запустил query в цикле по коллекции — будет N+1 problem. Помни об этом!