Рубрики
Без рубрики

Лучший способ справиться с исключением LazyInitializationException

Автор оригинала: Vlad Mihalcea.

Исключение LazyInitializationException , несомненно, является одним из наиболее распространенных исключений, которые вы можете получить при использовании Hibernate. В этой статье мы обобщим лучшие и худшие способы обработки ленивых ассоциаций.

С помощью JPA вы можете не только извлекать сущности из базы данных, но и извлекать ассоциации сущностей. По этой причине JPA определяет две стратегии FetchType :

  • ЖАЖДУЩИЙ
  • ЛЕНИВЫЙ

НЕТЕРПЕЛИВАЯ выборка означает, что ассоциации всегда извлекаются вместе с их родительской сущностью. На самом деле НЕТЕРПЕЛИВАЯ выборка очень плоха с точки зрения производительности , потому что очень сложно разработать глобальную политику выборки, которая применима к каждому бизнес-варианту использования, который может быть у вас в корпоративном приложении.

Как только у вас появится НЕТЕРПЕЛИВАЯ ассоциация, вы никак не сможете сделать ее ЛЕНИВОЙ . Таким образом, ассоциация всегда будет извлекаться, даже если она не обязательно нужна пользователю для конкретного случая использования. Что еще хуже, если вы забудете указать, что для получения ассоциации необходимо присоединиться к запросу JPQL, Hibernate выдаст дополнительный выбор для каждой неинициализированной ассоциации, что приведет к N+1 проблемам с запросом .

К сожалению, JPA 1.0 решил, что @ManyToOne и @OneToOne по умолчанию должны иметь значение FetchType.НЕТЕРПЕЛИВЫЙ , поэтому теперь вам нужно явно пометить эти две ассоциации как FetchType.ЛЕНИВЫЙ :

@ManyToOne(fetch = FetchType.LAZY)
private Post post;

По этой причине лучше использовать ЛЕНИВЫЕ ассоциации. ЛЕНИВАЯ ассоциация отображается через прокси-сервер, что позволяет уровню доступа к данным загружать ассоциацию по требованию. К сожалению, ЛЕНИВЫЕ ассоциации могут привести к Исключению LazyInitializationException .

В нашем следующем примере мы будем использовать следующие сущности:

При выполнении следующей логики:

List comments = null;

EntityManager entityManager = null;
EntityTransaction transaction = null;
try {
    entityManager = entityManagerFactory()
        .createEntityManager();
    transaction = entityManager.getTransaction();
    transaction.begin();

    comments = entityManager.createQuery(
        "select pc " +
        "from PostComment pc " +
        "where pc.review = :review", PostComment.class)
    .setParameter("review", review)
    .getResultList();

    transaction.commit();
} catch (Throwable e) {
    if (transaction != null && 
        transaction.isActive())
        transaction.rollback();
    throw e;
} finally {
    if (entityManager != null) {
        entityManager.close();
    }
}

try {
    for(PostComment comment : comments) {
        LOGGER.info(
            "The post title is '{}'", 
            comment.getPost().getTitle()
        );
    }
} catch (LazyInitializationException expected) {
    assertEquals(
        "could not initialize proxy - no Session", 
        expected.getMessage()
    );
}

Hibernate собирается создать исключение LazyInitializationException , потому что Комментарий к сообщению сущность не извлекла Сообщение ассоциацию, пока EntityManager все еще был открыт, а Сообщение связь была отмечена Типом выборки.ЛЕНИВЫЙ :

@ManyToOne(fetch = FetchType.LAZY)
private Post post;

К сожалению, существуют также плохие способы обработки исключения LazyInitializationException , например:

  • Открытая сессия в представлении
  • Открытая сессия в представлении

Эти два антишаблона очень неэффективны с точки зрения базы данных, поэтому вы никогда не должны использовать их в своем корпоративном приложении.

Сущности необходимы только в том случае, если текущей выполняемой транзакции уровня приложения необходимо изменить извлекаемые сущности. Благодаря механизму автоматической проверки на наличие ошибок Hibernate позволяет очень легко переводить переходы состояний сущностей в операторы SQL.

Учитывая, что нам нужно изменить Комментарий к публикации сущности, а также Опубликовать сущности, нам просто нужно использовать директиву JOIN FETCH , как в следующем запросе:

comments = entityManager.createQuery(
    "select pc " +
    "from PostComment pc " +
    "join fetch pc.post " +
    "where pc.review = :review", PostComment.class)
.setParameter("review", review)
.getResultList();

Директива JOIN FETCH предписывает Hibernate выполнить ВНУТРЕННЕЕ СОЕДИНЕНИЕ, чтобы объекты Post извлекались вместе с записями PostComment :

SELECT pc.id AS id1_1_0_ ,
       p.id AS id1_0_1_ ,
       pc.post_id AS post_id3_1_0_ ,
       pc.review AS review2_1_0_ ,
       p.title AS title2_0_1_
FROM   post_comment pc
INNER JOIN post p ON pc.post_id = p.id
WHERE  pc.review = 'Excellent!'

Вот и все! Все так просто!

Итак, мы еще не закончили. Что делать, если вам вообще не нужны сущности? Если вам не нужно изменять считываемые данные, зачем вам вообще понадобилось извлекать сущность? Проекция DTO позволяет извлекать меньше столбцов, и вы не будете рисковать Исключением LazyInitializationException .

Например, у нас может быть следующий класс DTO:

public class PostCommentDTO {

    private final Long id;

    private final String review;

    private final String title;

    public PostCommentDTO(
        Long id, String review, String title) {
        this.id = id;
        this.review = review;
        this.title = title;
    }

    public Long getId() {
        return id;
    }

    public String getReview() {
        return review;
    }

    public String getTitle() {
        return title;
    }
}

Если бизнес-логике нужна только проекция, DTO гораздо более подходят, чем сущности. Предыдущий запрос можно переписать следующим образом:

List comments = doInJPA(entityManager -> {
    return entityManager.createQuery(
        "select new " +
        "   com.vladmihalcea.book.hpjp.hibernate.fetching.PostCommentDTO(" +
        "       pc.id, pc.review, p.title" +
        "   ) " +
        "from PostComment pc " +
        "join pc.post p " +
        "where pc.review = :review", PostCommentDTO.class)
    .setParameter("review", review)
    .getResultList();
});

for(PostCommentDTO comment : comments) {
    LOGGER.info("The post title is '{}'", comment.getTitle());
}

И Hibernate может выполнить SQL – запрос, для которого нужно выбрать только три столбца вместо пяти :

SELECT pc.id AS col_0_0_ ,
       pc.review AS col_1_0_ ,
       p.title AS col_2_0_
FROM   post_comment pc
INNER JOIN post p ON pc.post_id = p.id
WHERE  pc.review = 'Excellent!'

Мало того , что мы избавились от исключения LazyInitializationException , но SQL-запрос стал еще более эффективным. Круто, правда?

Исключение LazyInitializationException – это запах кода, потому что он может скрыть тот факт, что сущности используются вместо ДЛЯ проекций . Иногда выборка сущностей является правильным выбором, и в этом случае директива JOIN FETCH является самым простым и лучшим способом инициализации прокси-серверов LAZY Hibernate.