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

Какой FetchType по умолчанию в Hibernate?

2.0 Middle🔥 171 комментариев
#ORM и Hibernate#Базы данных и SQL

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

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

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

FetchType по умолчанию в Hibernate

В Hibernate существует два типа загрузки связанных объектов: LAZY и EAGER. Выбор правильного FetchType критичен для производительности приложения.

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

FetchType по умолчанию зависит от типа связи:

  • @OneToMany и @ManyToManyLAZY (ленивая загрузка)
  • @OneToOne и @ManyToOneEAGER (активная загрузка)

Это один из частых источников проблем производительности!

Детальное объяснение

LAZY (ленивая загрузка)

Данные загружаются только тогда, когда к ним впервые обращаются.

@Entity
public class User {
    @Id
    private Long id;
    
    private String name;
    
    // Связь OneToMany - по умолчанию LAZY
    @OneToMany(mappedBy = "user")
    private List<Order> orders;  // Загружается ЛЕНИВО
}

@Entity
public class Order {
    @Id
    private Long id;
    
    @ManyToOne  // По умолчанию EAGER!
    private User user;  // Загружается СРАЗУ
}

// Пример
User user = userRepository.findById(1L);  // SELECT * FROM User WHERE id=1
List<Order> orders = user.getOrders();    // SELECT * FROM Order WHERE user_id=1 (запрос выполняется здесь!)

EAGER (активная загрузка)

Данные загружаются сразу вместе с родительским объектом.

@Entity
public class Order {
    @Id
    private Long id;
    
    // По умолчанию EAGER
    @ManyToOne(fetch = FetchType.EAGER)
    private User user;  // Загружается вместе с Order
}

// Пример
Order order = orderRepository.findById(1L);
// SELECT o.*, u.* FROM Order o LEFT JOIN User u ON o.user_id = u.id WHERE o.id=1
User user = order.getUser();  // user уже загружен, нет дополнительного запроса

По умолчанию в JPA/Hibernate

// 1. @OneToMany - LAZY по умолчанию
@Entity
public class Department {
    @OneToMany(mappedBy = "department")
    private List<Employee> employees;  // Загружается лениво
}

// 2. @ManyToMany - LAZY по умолчанию
@Entity
public class Student {
    @ManyToMany
    @JoinTable(name = "student_course")
    private List<Course> courses;  // Загружается лениво
}

// 3. @ManyToOne - EAGER по умолчанию (ВНИМАНИЕ!)
@Entity
public class Order {
    @ManyToOne  // EAGER - опасно!
    private Customer customer;  // Загружается активно
}

// 4. @OneToOne - EAGER по умолчанию
@Entity
public class Account {
    @OneToOne
    private User user;  // Загружается активно
}

Проблемы с EAGER по умолчанию

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

// ❌ Проблема
@Entity
public class Order {
    @Id
    private Long id;
    
    @ManyToOne(fetch = FetchType.EAGER)  // EAGER загружает автоматически
    private Customer customer;
}

// Используемый код
List<Order> orders = orderRepository.findAll();  // SELECT * FROM Order (1 запрос)
// Для каждого Order загружается Customer (N запросов!)
// Итого: 1 + N запросов!

Проблема 2: Циклические загрузки

// ❌ Циклическая загрузка
@Entity
public class Order {
    @ManyToOne(fetch = FetchType.EAGER)
    private Customer customer;
}

@Entity
public class Customer {
    @OneToMany(mappedBy = "customer", fetch = FetchType.EAGER)  // Плохо!
    private List<Order> orders;
}

// Попытка сериализовать → бесконечная рекурсия

Правильное использование FetchType

Рекомендация 1: Используй LAZY по умолчанию

// ✅ Хорошо
@Entity
public class Order {
    @Id
    private Long id;
    
    // Явно указываем LAZY для ManyToOne
    @ManyToOne(fetch = FetchType.LAZY)
    private Customer customer;
    
    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
    private List<OrderItem> items;
}

Рекомендация 2: Используй EAGER только когда нужно

// ✅ EAGER только когда данные точно понадобятся
@Entity
public class Account {
    @Id
    private Long id;
    
    // Если Account всегда используется с User
    @OneToOne(fetch = FetchType.EAGER)
    private User user;
}

Рекомендация 3: Используй JOIN FETCH запрос

// ✅ Контролируемая загрузка
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    @Query("SELECT o FROM Order o JOIN FETCH o.customer WHERE o.id = :id")
    Order findByIdWithCustomer(@Param("id") Long id);
}

// SELECT o.*, c.* FROM Order o JOIN Customer c ON o.customer_id = c.id

Рекомендация 4: Используй DTO для контролируемого выбора полей

// ✅ DTO запрос - загружаешь только нужные данные
@Query(
    "SELECT new com.example.OrderDTO(o.id, o.total, c.name) " +
    "FROM Order o " +
    "LEFT JOIN o.customer c " +
    "WHERE o.id = :id"
)
OrderDTO findOrderDTOById(@Param("id") Long id);

public class OrderDTO {
    private Long id;
    private BigDecimal total;
    private String customerName;
    
    // конструктор и геттеры
}

Практический пример: Правильная конфигурация

// ❌ Плохой дизайн
@Entity
public class BadOrder {
    @Id
    private Long id;
    
    @ManyToOne(fetch = FetchType.EAGER)  // Зачем?
    private Customer customer;
    
    @OneToMany(fetch = FetchType.EAGER)  // Очень плохо!
    private List<OrderItem> items;
    
    @ManyToOne(fetch = FetchType.EAGER)  // Слишком много EAGER
    private Warehouse warehouse;
}

// ✅ Хороший дизайн
@Entity
public class GoodOrder {
    @Id
    private Long id;
    
    private Long customerId;  // Или LAZY загрузка
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customerId")
    private Customer customer;
    
    // Список всегда LAZY
    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
    private List<OrderItem> items;
    
    @ManyToOne(fetch = FetchType.LAZY)
    private Warehouse warehouse;
}

Тестирование FetchType

// Проверяем, сколько SQL запросов выполняется
@Test
@DisplayName("Проверяем, что связь загружается лениво")
void testLazyLoading() {
    // Настраиваем отслеживание SQL
    SQLStatementCountValidator.reset();
    
    Order order = orderRepository.findById(1L);
    // 1 запрос: SELECT * FROM Order
    assertThat(SQLStatementCountValidator.assertSelectCount()).isOne();
    
    // Обращаемся к customer - новый запрос
    order.getCustomer().getName();
    // 2 запроса: SELECT * FROM Customer
    assertThat(SQLStatementCountValidator.assertSelectCount()).isEqualTo(2);
}

Hibernate статистика

// Включи статистику для анализа
persistence.xml:
<property name="hibernate.generate_statistics" value="true" />
<property name="hibernate.use_sql_comments" value="true" />

// Анализируй запросы
Statistics stats = sessionFactory.getStatistics();
stats.logSummary();
stats.getQueryExecutionCount();

Таблица FetchType по умолчанию

АннотацияFetchType по умолчаниюРекомендация
@OneToManyLAZYОставляй LAZY
@ManyToManyLAZYОставляй LAZY
@OneToOneEAGERИзмени на LAZY если возможно
@ManyToOneEAGER⚠️ Измени на LAZY

Заключение

По умолчанию @ManyToOne и @OneToOne используют EAGER загрузку, а @OneToMany и @ManyToMany используют LAZY. Это может привести к N+1 проблемам и проблемам производительности.

Золотое правило: начни с LAZY везде, потом явно устанавливай EAGER или используй JOIN FETCH только для специфичных запросов, где ты точно знаешь, что нужны связанные данные.

Какой FetchType по умолчанию в Hibernate? | PrepBro