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

Какие знаешь типы загрузок сущностей?

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

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

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

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

# Типы загрузок сущностей в ORM/Hibernate

Это один из важнейших концептов при работе с ORM фреймворками (Hibernate, JPA, Spring Data). Неправильная загрузка сущностей приводит к N+1 проблемам и падению производительности.

Два основных типа загрузки

1. Eager Loading (Нетерпеливая загрузка)

Сущность и все её связанные данные загружаются СРАЗУ, когда вы запрашиваете основной объект.

@Entity
@Table(name = "users")
public class User {
    @Id
    private Long id;
    
    @Column(name = "email")
    private String email;
    
    // ✅ EAGER Loading - загружается сразу
    @OneToMany(fetch = FetchType.EAGER)
    @JoinColumn(name = "user_id")
    private List<Order> orders;  // Все заказы загружаются со User
}

// В коде:
User user = userRepository.findById(1L);
System.out.println(user.getEmail());          // SELECT 1
System.out.println(user.getOrders().size());  // БЕЗ второго запроса!
// SQL: SELECT u.*, o.* FROM users u LEFT JOIN orders o ON ...

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

  • Связанные данные ВСЕГДА нужны
  • Связанных объектов мало (несколько)
  • Это критичные данные

Проблемы:

  • Может быть дорого загружать лишние данные
  • Может привести к Cartesian Product при нескольких @OneToMany

2. Lazy Loading (Ленивая загрузка)

Связанные данные загружаются ТОЛЬКО когда к ним обратиться.

@Entity
@Table(name = "users")
public class User {
    @Id
    private Long id;
    
    @Column(name = "email")
    private String email;
    
    // ✅ LAZY Loading - загружается при обращении
    @OneToMany(fetch = FetchType.LAZY)  // Это DEFAULT
    @JoinColumn(name = "user_id")
    private List<Order> orders;
}

// В коде:
User user = userRepository.findById(1L);
System.out.println(user.getEmail());          // SELECT 1 - только User
System.out.println(user.getOrders().size());  // SELECT 2 - запрос для Orders!
// Два отдельных SQL запроса

Когда использовать LAZY:

  • Связанные данные нужны НЕ ВСЕГДА
  • Много связанных объектов
  • Хотим минимизировать загрузку данных

Проблемы:

  • N+1 проблема (1 запрос для User + N запросов для каждого Order)
  • LazyInitializationException если сессия закрыта

N+1 проблема (самая частая)

Это классическая проблема с LAZY loading:

// ❌ ПЛОХО - N+1 запросы
List<User> users = userRepository.findAll();  // SELECT 1 - загружаем 100 User

for (User user : users) {
    System.out.println(user.getOrders());     // SELECT 2-101 - для каждого User!
}
// ИТОГО: 1 + 100 = 101 SQL запрос! 💥 КАТАСТРОФА
// ✅ ХОРОШО - 2 запроса
@Query("SELECT u FROM User u JOIN FETCH u.orders")
List<User> users = userRepository.findAllWithOrders();  // SELECT 1 - с заказами

for (User user : users) {
    System.out.println(user.getOrders());     // Уже в памяти!
}
// ИТОГО: 2 SQL запроса

Способы загрузки сущностей

1. FetchType в аннотациях

// Explicit Lazy (по умолчанию для OneToMany)
@OneToMany(fetch = FetchType.LAZY)
private List<Order> orders;

// Explicit Eager
@ManyToOne(fetch = FetchType.EAGER)
private Category category;

Проблема: Это ГЛОБАЛЬНОЕ решение - везде будет загружаться так.

2. JOIN FETCH в JPQL

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    // ✅ Явно указываем что загружаем
    @Query("SELECT u FROM User u JOIN FETCH u.orders WHERE u.id = ?1")
    User findByIdWithOrders(Long id);
    
    // Несколько связей
    @Query("SELECT u FROM User u "
         + "JOIN FETCH u.orders o "
         + "JOIN FETCH o.items")
    List<User> findAllWithOrdersAndItems();
}

Плюсы:

  • Явно видно что загружается
  • Можно выбрать загрузку для конкретного запроса

3. Entity Graph (Spring Data)

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    // Определяем граф сущностей
    @EntityGraph(attributePaths = {"orders", "orders.items"})
    @Query("SELECT u FROM User u")
    List<User> findAllWithGraph();
}

4. QueryDSL (альтернатива)

QUser user = QUser.user;

List<User> users = queryFactory
    .selectFrom(user)
    .leftJoin(user.orders).fetchJoin()  // Явно fetchJoin
    .fetch();

Полный пример: правильная загрузка

// СУЩНОСТИ
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false)
    private String email;
    
    // LAZY по умолчанию для OneToMany
    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    private List<Order> orders = new ArrayList<>();
    
    // EAGER для ManyToOne (обычно нужно)
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "company_id")
    private Company company;
}

@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false)
    private BigDecimal total;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;
    
    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
    private List<OrderItem> items = new ArrayList<>();
}

// REPOSITORY
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    // Вариант 1: для одного User с заказами
    @Query("SELECT u FROM User u "
         + "LEFT JOIN FETCH u.orders "
         + "WHERE u.id = ?1")
    Optional<User> findByIdWithOrders(Long id);
    
    // Вариант 2: для всех User с заказами и товарами
    @Query("SELECT u FROM User u "
         + "LEFT JOIN FETCH u.orders o "
         + "LEFT JOIN FETCH o.items")
    List<User> findAllWithOrdersAndItems();
    
    // Вариант 3: с EntityGraph
    @EntityGraph(attributePaths = {"orders"})
    @Query("SELECT u FROM User u")
    List<User> findAllWithOrdersGraph();
}

// SERVICE
@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;
    
    // Сценарий 1: нужен User с компанией (Eager по умолчанию)
    public User getUser(Long id) {
        User user = userRepository.findById(id).orElse(null);
        System.out.println(user.getCompany().getName());  // OK - загружено
        return user;
    }
    
    // Сценарий 2: нужен User с заказами
    public User getUserWithOrders(Long id) {
        // Явно загружаем заказы
        return userRepository.findByIdWithOrders(id).orElse(null);
    }
    
    // Сценарий 3: N+1 проблема!
    public List<Order> getAllUserOrders() {
        List<User> users = userRepository.findAll();  // ❌ 1 запрос
        
        return users.stream()
            .flatMap(u -> u.getOrders().stream())  // ❌ N запросов
            .collect(Collectors.toList());
        // ИТОГО: 1 + N запросов
    }
    
    // Сценарий 3 (исправленный)
    public List<Order> getAllUserOrders() {
        List<User> users = userRepository.findAllWithOrdersAndItems();  // ✅ Уже с заказами
        
        return users.stream()
            .flatMap(u -> u.getOrders().stream())
            .collect(Collectors.toList());
        // ИТОГО: 1-2 запроса
    }
}

LazyInitializationException

Одна из самых частых ошибок при LAZY loading:

@Transactional
public User getUser(Long id) {
    return userRepository.findById(id).orElse(null);  // LAZY orders
}

// ❌ ОШИБКА в другом сервисе
public void doSomething() {
    User user = userService.getUser(1L);  // Сессия закрылась
    System.out.println(user.getOrders());  // LazyInitializationException!
}

// ✅ РЕШЕНИЕ 1: JOIN FETCH
@Query("SELECT u FROM User u JOIN FETCH u.orders")
User findByIdWithOrders(Long id);

// ✅ РЕШЕНИЕ 2: @Transactional на более высоком уровне
@Transactional
public void doSomething() {
    User user = userRepository.findById(1L);
    System.out.println(user.getOrders());  // Работает - сессия открыта
}

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

  1. По умолчанию используй LAZY для @OneToMany и @ManyToMany
  2. EAGER для @ManyToOne и @OneToOne - обычно нужны
  3. Всегда используй JOIN FETCH если планируешь обращаться к связанным данным
  4. Профилируй запросы - используй SQL логирование
  5. Избегай N+1 - это самая частая причина медленных приложений

SQL логирование для проверки

# application.properties
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE

Заключение

Два типа загрузок:

  1. EAGER - загружает сразу (может быть неэффективно)
  2. LAZY - загружает при обращении (риск N+1 и LazyInitializationException)

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

  • Используй LAZY по умолчанию
  • Явно загружай данные через JOIN FETCH когда они нужны
  • Профилируй и отслеживай SQL запросы
Какие знаешь типы загрузок сущностей? | PrepBro