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

Что такое ленивый тип загрузки?

2.2 Middle🔥 171 комментариев
#ORM и Hibernate

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

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

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

# Ленивая загрузка (Lazy Loading) в Hibernate/JPA

Определение

Lazy Loading - это тип загрузки связанных сущностей, при котором связанные объекты загружаются только в момент их фактического использования, а не вместе с основной сущностью.

Как это работает

@Entity
public class Author {
    @Id
    private Long id;
    private String name;
    
    // Связанные сущности загружаются только при обращении к books
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "author")
    private List<Book> books;
}

Пример выполнения SQL запросов

// Запрос 1: загрузить автора
Author author = session.get(Author.class, 1L);
System.out.println(author.getName()); // "Пушкин"
// SQL: SELECT * FROM authors WHERE id = 1

// В этот момент books - это прокси объект, данные не загружены!

// Запрос 2: обращение к books инициирует SQL
List<Book> books = author.getBooks();
// SQL: SELECT * FROM books WHERE author_id = 1
// Теперь данные загружены

Механизм работы прокси

Hibernate использует прокси объекты для реализации lazy loading:

Author author = session.get(Author.class, 1L);

// author.getBooks() возвращает прокси (PersistentBag)
// Это не реальный ArrayList, а оболочка
System.out.println(author.getBooks().getClass().getName());
// org.hibernate.collection.internal.PersistentBag

// При первом обращении к методам коллекции:
author.getBooks().size();        // Инициирует SQL запрос
author.getBooks().get(0);        // Инициирует SQL запрос
for (Book book : author.getBooks()) {} // Инициирует SQL запрос

Преимущества Lazy Loading

1. Экономия памяти

// Загружаем только нужные данные
Author author = session.get(Author.class, 1L);
System.out.println(author.getName());
// Не загружали 10000 книг этого автора - сэкономили память

2. Производительность при отсутствии необходимости

// Если нам не нужны книги - они не загружаются
List<Author> authors = session.createQuery("FROM Author").getResultList();
int count = authors.size(); // Не загружены связанные книги

3. Упрощение разработки

Разработчик может не думать о том, какие данные загружать.

Недостатки Lazy Loading

1. LazyInitializationException

Это самая частая проблема:

// В контроллере
public AuthorDTO getAuthor(Long id) {
    Author author = authorService.findById(id);
    return new AuthorDTO(author); // LazyInitializationException!
}

// В сервисе
@Transactional
public Author findById(Long id) {
    Author author = session.get(Author.class, id);
    return author;
    // Сессия закрывается в этой точке
}

// В конструкторе DTO
public AuthorDTO(Author author) {
    this.name = author.getName();
    this.books = author.getBooks(); // ❌ Сессия закрыта, прокси не может загрузить
}

2. N+1 проблема

// Запрос 1: загрузить всех авторов
List<Author> authors = session.createQuery("FROM Author").getResultList();

// Запросы 2-N: для каждого автора загружаем книги
for (Author author : authors) {
    int bookCount = author.getBooks().size();
    // Каждая итерация = новый SQL запрос!
}

// Итого: 1 + N SQL запросов (где N - количество авторов)

3. Неожиданные SQL запросы

// В сложной логике может быть неясно, когда инициируются запросы
public void processAuthors(List<Author> authors) {
    for (Author author : authors) {
        // Здесь могут быть неявные SQL запросы
        if (shouldProcess(author)) {
            // А здесь - еще больше запросов
            List<Book> books = author.getBooks();
        }
    }
}

Решения проблем

1. JOIN FETCH (явная загрузка)

// Переопределяем LAZY на уровне запроса
List<Author> authors = session.createQuery(
    "SELECT DISTINCT a FROM Author a LEFT JOIN FETCH a.books"
).getResultList();
// Теперь books загружены в одном запросе с JOIN

2. @Transactional на правильном уровне

@Transactional
public AuthorDTO getAuthor(Long id) {
    Author author = session.get(Author.class, id);
    AuthorDTO dto = new AuthorDTO(author);
    // Инициализируем books в пределах транзакции
    author.getBooks().size();
    return dto;
}

3. Entity Graph (JPA 2.1)

@NamedEntityGraph(
    name = "Author.withBooks",
    attributeNodes = {
        @NamedAttributeNode("books")
    }
)
@Entity
public class Author { ... }

// Использование
EntityGraph<?> graph = em.getEntityGraph("Author.withBooks");
Map<String, Object> properties = new HashMap<>();
properties.put("jakarta.persistence.fetchgraph", graph);

Author author = em.find(Author.class, 1L, properties);
// books загружены в одном запросе

4. Batch Loading

@OneToMany(fetch = FetchType.LAZY, mappedBy = "author")
@BatchSize(size = 10)
private List<Book> books;

// Вместо N запросов будет N/10 запросов

Когда использовать Lazy Loading

Используй LAZY:

  • Для @OneToMany коллекций (по умолчанию)
  • Для @ManyToMany коллекций (по умолчанию)
  • Когда связанные данные нужны редко
  • Когда данных может быть очень много

НЕ используй LAZY:

  • Для @ManyToOne и @OneToOne (по умолчанию EAGER - это норма)
  • Когда данные нужны в 95% случаев
  • Для простых ссылочных данных

Лучшие практики

  1. По умолчанию используй LAZY для коллекций
  2. Используй JOIN FETCH в DAO/Repository слое для оптимизации
  3. Инициализируй данные в пределах транзакции перед возвратом из сервиса
  4. Помни о N+1 проблеме и профилируй SQL запросы
  5. Используй Entity Graph для сложных сценариев загрузки

Lazy Loading - это инструмент, который нужно понимать и использовать осознанно, чтобы избежать классических ошибок.

Что такое ленивый тип загрузки? | PrepBro