В какой момент прогружается поле в Hibernate
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Когда загружается поле в 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:
- Простые поля (String, int, Date) — ВСЕГДА EAGER, при загрузке entity
- @Lob поля — LAZY по умолчанию (большие данные)
- @ManyToOne — LAZY по умолчанию
- @OneToMany, @ManyToMany — LAZY по умолчанию (коллекции)
- @OneToOne — LAZY по умолчанию
Проблема: N+1 query problem при использовании LAZY loading
Решение:
- JOIN FETCH в JPQL
- fetch в Criteria Query
- Hibernate.initialize() для принудительной загрузки
- Переопределить fetch type на EAGER (если нужно всегда)
Лучшая практика: явно управлять загрузкой данных на уровне repository, не полагаться на OpenSessionInView.