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

В какой момент прогружается поле в Hibernate

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

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

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

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

Когда загружается поле в Hibernate: Lazy Loading

Получение данных из БД — это один из самых важных аспектов Hibernate. Когда именно загружаются поля, зависит от их аннотаций и стратегии загрузки.

Стратегии загрузки в Hibernate

Hibernate поддерживает несколько способов загрузки данных:

1. EAGER Loading (немедленная загрузка)

Поля загружаются сразу при загрузке родительского объекта.

@Entity
public class User {
    @Id
    private Long id;
    private String name;  // Простое поле, всегда EAGER
    
    @ManyToOne(fetch = FetchType.EAGER)  // Явно EAGER
    private Department department;
}

// Что происходит при вызове:
User user = entityManager.find(User.class, 1L);

// SQL запросы:
// SELECT u.* FROM users u WHERE u.id = 1     — загружает User
// SELECT d.* FROM departments d WHERE d.id = ? — загружает Department
// (может быть LEFT JOIN в одном запросе)

System.out.println(user.getDepartment().getName());  // Данные уже в памяти

Когда используется: когда всегда нужны связанные данные.

2. LAZY Loading (ленивая загрузка) — по умолчанию

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

@Entity
public class User {
    @Id
    private Long id;
    private String name;
    
    @OneToMany(fetch = FetchType.LAZY)  // По умолчанию для collections
    private List<Order> orders;  // Не загружается автоматически
}

// Что происходит при вызове:
User user = entityManager.find(User.class, 1L);
// SELECT u.* FROM users u WHERE u.id = 1 — только User
// orders еще не загружены!

// При обращении к orders:
List<Order> orders = user.getOrders();  // LazyInitializationException!
for (Order order : orders) {
    System.out.println(order.getAmount());
}

// SELECT o.* FROM orders o WHERE o.user_id = 1 — загружается сейчас

Проблема: LazyInitializationException

// Сессия закрывается после метода
@Transactional  // Открывает сессию
public User getUserWithOrders(Long userId) {
    return entityManager.find(User.class, userId);
}  // Сессия закрывается

// Вызов в другом слое
User user = getUserWithOrders(1L);  // Сессия закрыта
try {
    System.out.println(user.getOrders().size());  // LazyInitializationException!
} catch (LazyInitializationException e) {
    // Сессия закрыта, Hibernate не может загрузить orders
}

3. FetchType.LAZY с проблемой N+1

Сценарий:

User user = entityManager.find(User.class, userId);
// SELECT u.* FROM users u WHERE u.id = ?

// При обращении:
for (Order order : user.getOrders()) {
    // SELECT o.* FROM orders o WHERE o.user_id = ?
    // Это выполняется ВНУТРИ цикла!
    // Если 100 ордеров, будет 101 запрос (1 + 100)!
}

4. Решение 1: EAGER Loading

@Entity
public class User {
    @Id
    private Long id;
    
    @OneToMany(fetch = FetchType.EAGER)
    private List<Order> orders;  // Загружается сразу
}

User user = entityManager.find(User.class, userId);
// SELECT u.* FROM users u LEFT JOIN orders o ON u.id = o.user_id
for (Order order : user.getOrders()) {
    // Данные уже в памяти, нет доп. запросов
}

Минус: даже если не нужны orders, они будут загружены.

5. Решение 2: Явная загрузка в транзакции

@Repository
public class UserRepository {
    
    // Инициализируем lazy коллекции в границах транзакции
    @Transactional
    public User getUserWithOrders(Long userId) {
        User user = entityManager.find(User.class, userId);
        
        // Принудительная загрузка (инициализация)
        Hibernate.initialize(user.getOrders());
        
        return user;
    }
}

// Использование
User user = userRepository.getUserWithOrders(1L);
// Сессия закрыта, но orders уже загружены
for (Order order : user.getOrders()) {
    System.out.println(order.getAmount());  // OK
}

6. Решение 3: JPQL с JOIN FETCH

@Repository
public class UserRepository {
    
    @Query("SELECT u FROM User u LEFT JOIN FETCH u.orders WHERE u.id = :id")
    Optional<User> findWithOrders(@Param("id") Long id);
}

// Использование
User user = userRepository.findWithOrders(1L).orElse(null);
// SELECT u.*, o.* FROM users u LEFT JOIN orders o ON u.id = o.user_id
for (Order order : user.getOrders()) {
    System.out.println(order.getAmount());  // OK
}

7. Решение 4: Criteria Query с fetch

@Repository
public class UserRepository {
    
    public User findWithOrders(Long userId) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<User> query = cb.createQuery(User.class);
        Root<User> root = query.from(User.class);
        
        // FETCH: загружает related данные в одном запросе
        root.fetch("orders", JoinType.LEFT);
        
        query.select(root).where(cb.equal(root.get("id"), userId));
        
        return entityManager.createQuery(query).getSingleResult();
    }
}

8. Простые поля (не отношения)

Простые поля (String, int, Date) всегда EAGER:

@Entity
public class User {
    @Id
    private Long id;
    
    private String name;  // ВСЕГДА EAGER
    private String email; // ВСЕГДА EAGER
    private int age;      // ВСЕГДА EAGER
    
    @Lob
    private String biography;  // По умолчанию LAZY! (большие данные)
    
    @ManyToOne(fetch = FetchType.LAZY)
    private Department department;  // LAZY по умолчанию
}

User user = entityManager.find(User.class, 1L);
// SELECT u.* FROM users u WHERE u.id = 1

System.out.println(user.getName());      // Сразу (EAGER)
System.out.println(user.getBiography()); // Отдельный запрос (LAZY)
System.out.println(user.getDepartment().getName());  // Отдельный запрос (LAZY)

9. @Lob для больших данных

@Entity
public class Document {
    @Id
    private Long id;
    
    private String title;  // EAGER
    
    @Lob  // Large Object — может быть большой
    private String content;  // LAZY по умолчанию
    
    @Lob
    @Basic(fetch = FetchType.EAGER)  // Явно EAGER, если нужно
    private byte[] file;  // Загрузится вместе с document
}

10. Таблица: Когда загружается что?

Тип поляСложностьПоведение по умолчаниюПочему
String, int, DateПростоеEAGERОбычно небольше
@LobПростоеLAZYМожет быть большой
@ManyToOneОтношениеLAZYЧасто не нужно
@OneToOneОтношениеLAZYМожет быть не нужно
@OneToManyКоллекцияLAZYМожет быть большая
@ManyToManyКоллекцияLAZYОбычно большая

11. OpenSessionInView (антипаттерн)

Плохое решение: оставляем сессию открытой

// В конфиге (Spring)
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true

// Или фильтр
public class OpenSessionInViewFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                   HttpServletResponse response, 
                                   FilterChain filterChain) {
        // Открываем сессию на весь HTTP request
        // Пермит lazy loading в controller'е
        // НО: сессия открыта слишком долго (плохо для N+1 problem)
    }
}

Почему это плохо:

  • Скрывает проблемы N+1
  • Сессия открыта дольше, чем нужно
  • Сложнее отследить когда загружаются данные

12. Правильный подход

// 1. Service слой: @Transactional, явно загружаем нужные данные
@Service
public class UserService {
    
    @Transactional(readOnly = true)
    public UserDTO getUserWithOrders(Long userId) {
        User user = userRepository.findWithOrders(userId);
        // Orders загружены в одном запросе благодаря JOIN FETCH
        
        return UserDTO.from(user);  // Преобразуем в DTO
    }
}

// 2. Repository: используем JOIN FETCH или явную загрузку
@Repository
public class UserRepository extends JpaRepository<User, Long> {
    
    @Query("SELECT DISTINCT u FROM User u "
         + "LEFT JOIN FETCH u.orders "
         + "WHERE u.id = :id")
    Optional<User> findWithOrders(@Param("id") Long id);
}

// 3. DTO для presentation: только нужные данные
public class UserDTO {
    private Long id;
    private String name;
    private List<OrderDTO> orders;
    
    public static UserDTO from(User user) {
        UserDTO dto = new UserDTO();
        dto.id = user.getId();
        dto.name = user.getName();
        dto.orders = user.getOrders().stream()
            .map(OrderDTO::from)
            .collect(Collectors.toList());
        return dto;
    }
}

Резюме

Когда загружаются поля в Hibernate:

  1. Простые поля (String, int, Date) — ВСЕГДА EAGER, при загрузке entity
  2. @Lob поля — LAZY по умолчанию (большие данные)
  3. @ManyToOne — LAZY по умолчанию
  4. @OneToMany, @ManyToMany — LAZY по умолчанию (коллекции)
  5. @OneToOne — LAZY по умолчанию

Проблема: N+1 query problem при использовании LAZY loading

Решение:

  • JOIN FETCH в JPQL
  • fetch в Criteria Query
  • Hibernate.initialize() для принудительной загрузки
  • Переопределить fetch type на EAGER (если нужно всегда)

Лучшая практика: явно управлять загрузкой данных на уровне repository, не полагаться на OpenSessionInView.