В чем разница между FetchType.LAZY и FetchType.EAGER в Hibernate/JPA?

Ответ

Разница заключается в стратегии загрузки связанных сущностей.

  • FetchType.EAGER (жадная): Связанная сущность загружается немедленно, в том же запросе SELECT (или с помощью дополнительного JOIN), что и владеющая сущность.
  • FetchType.LAZY (ленивая): Связанная сущность загружается только при первом явном обращении к этому полю в коде, отдельным запросом.

Настройка:

@Entity
public class Order {
    @Id
    private Long id;

    // EAGER загрузка (по умолчанию для @ManyToOne)
    @ManyToOne(fetch = FetchType.EAGER)
    private Customer customer;

    // LAZY загрузка (по умолчанию для @OneToMany)
    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
    private List<OrderItem> items;
}

Плюсы и минусы:

  • LAZY:
    • Плюсы: Повышает производительность начальной загрузки, избегая избыточных JOIN и загрузки ненужных данных.
    • Минусы: Может привести к проблеме N+1 SELECT, если лениво загружать коллекции в цикле. Требует открытой сессии/контекста persistence при обращении, иначе выбрасывается LazyInitializationException.
  • EAGER:
    • Плюсы: Данные доступны сразу, нет риска LazyInitializationException.
    • Минусы: Может серьезно снижать производительность из-за загрузки больших графов объектов одним запросом (например, загрузка всех заказов со всеми позициями).

Best Practice: По умолчанию используйте LAZY для всех ассоциаций. Для случаев, когда связанные данные нужны всегда, используйте JOIN FETCH в запросе (JPQL или Criteria API) для эффективной eager-загрузки только когда это необходимо, избегая N+1 проблемы.

Ответ 18+ 🔞

А, ну это же классика, блядь! Вечная тема, про которую каждый второй джуниор мозги себе выносит, пока не поймёт, в чём подвох. Слушай сюда, сейчас разжую, как есть.

Вот представь, ты заказываешь пиццу. FetchType — это как раз твоя стратегия по её получению.

  • EAGER (жадный) — это когда ты, сука, такой нетерпеливый, заказываешь пиццу и сразу же, в одном заказе, требуешь, чтобы тебе привезли ещё и колу, и салат, и соусы на всю овердохуищу, даже если ты их, возможно, и не будешь есть. Курьер приезжает с одной здоровенной, ебучей коробкой, в которой всё на свете. Вроде всё сразу есть, но тащить это пиздец как тяжело, и половина может оказаться ненужной.
  • LAZY (ленивый) — это нормальная, адекватная стратегия. Ты заказываешь только пиццу. Привозят. Ты её начинаешь жрать, и тут тебя осеняет: «А хули я, блядь, без колы? Горло сухое!». Только тогда ты звонишь и заказываешь колу отдельным заказом. Экономишь силы и деньги, если кола не понадобится.

Как это в коде выглядит, ёпта:

@Entity
public class Order {
    @Id
    private Long id;

    // EAGER загрузка (дефолт для @ManyToOne) - закажешь пиццу, клиента (Customer) прицепят сразу, хош-не-хош
    @ManyToOne(fetch = FetchType.EAGER)
    private Customer customer;

    // LAZY загрузка (дефолт для @OneToMany) - позиции заказа (OrderItem) подгрузятся только если полезешь в них пальцами
    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
    private List<OrderItem> items;
}

Теперь про плюсы и минусы, тут собака, сука, порылась:

  • LAZY (Ленивая, она же нормальная):

    • Плюсы: Начальная загрузка — огонь, быстро. Не тащишь в память тонну ебучего хлама, который, возможно, и не понадобится. Здравый смысл, блядь!
    • Минусы: А вот тут засада. Если ты в цикле начнёшь обходить сто заказов и для каждого лезть в его items, то Hibernate, тупая мартышлюшка, сделает на каждый заказ отдельный запрос. Это и есть та самая проблема N+1 SELECT, от которой у всех волосы дыбом. И ещё: обращаться к ленивым полям можно только пока сессия жива, а то получишь LazyInitializationException — мол, «извини, дружок, пиццерия закрылась, колу теперь не закажешь».
  • EAGER (Жадная, она же проблемная):

    • Плюсы: Данные всегда при тебе, как преданный пёс. Никаких исключений при обращении.
    • Минусы: А производительность, блядь? Ты один раз запросил список из 100 заказов, а Hibernate, такой ретивый, в одном запросе попытался притащить тебе и все заказы, и всех клиентов, и все позиции к каждому заказу, и их товары... Вжух! И приложение легло, накрылось медным тазом от такого объёма хуйни. Это как пытаться за один присест съесть весь склад пиццерии.

Так что же делать, ёбана? Best Practice, который все узнают кровью:

Ставь везде LAZY по умолчанию, как будто это твоя религия. Это безопасно и правильно.

А когда тебе действительно нужны связанные данные (например, ты на странице «Детали заказа» и точно знаешь, что будешь показывать и заказ, и все его позиции), тогда не будь мудаком, не полагайся на жадную загрузку.

Используй JOIN FETCH в своём JPQL-запросе:

SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id

Вот это, блядь, серебряная пуля. Ты явно говоришь Хибернейту: «Слушай, пацан, дай мне вот этот конкретный заказ, и заодно, одним запросом, прихвати все его позиции. Не городи N+1, будь умником». И он сделает один красивый, эффективный JOIN, и проблема снимается.

Короче, запомни: EAGER — это как предоплата за всё, что есть в меню, на случай если проголодаешься. LAZY с JOIN FETCH — это разумный заказ по списку, когда точно знаешь, что будешь жрать. Не усложняй, живи проще.