Исключение LazyInitializationException при извлечении @EntityGraph из кэша 2-го уровня гибернации

#spring-boot #hibernate #spring-data-jpa #ehcache #hibernate-cache

#весенняя загрузка #переход в спящий режим #spring-data-jpa #ehcache #спящий режим-кэш

Вопрос:

Я разрабатываю веб-приложение Spring Boot 2.3.4 с JPA Spring Data.

Я хочу использовать кэш запросов 2-го уровня гибернации для метода репозитория с @EntityGraph. Однако я получаю исключение LazyInitializationException при создании представления Thymeleaf в случае, если данные уже находятся в кэше 2-го уровня, если у меня не включен открытый сеанс Spring в представлении. При первом извлечении данных из базы данных или без кэша 2-го уровня все в порядке, даже с spring.jpa.open-in-view=false . Более того, если я включу spring.jpa.open-in-view, при извлечении данных из кэша без какого-либо выбора в базе данных исключения не будет.

Как я могу заставить Hibernate извлекать сразу все ассоциации, указанные в @EntityGraph, при использовании кэша 2-го уровня гибернации?

Вот мой метод репозитория:

 @org.springframework.data.jpa.repository.QueryHints({@javax.persistence.QueryHint(name = "org.hibernate.cacheable", value = "true")})
@EntityGraph(attributePaths = { "venue.city", "lineup.artist", "ticketLinks" }, type = EntityGraphType.FETCH)
Optional<Event> findEventPageViewGraphById(long id);
  

и часть сущности:

 @Entity
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class Event {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
protected Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "venue_id")
private Venue venue;

@OneToMany(mappedBy = "event", cascade = CascadeType.ALL, orphanRemoval = true)
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
@OrderBy("orderId")
private Set<TicketLink> ticketLinks = new LinkedHashSet<>();

@OneToMany(mappedBy = "event", cascade = CascadeType.ALL, orphanRemoval = true) 
@OrderBy("orderId")
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
private Set<ArtistEvent> lineup = new LinkedHashSet<>();
  

}

Ответ №1:

Это известная проблема. Hibernate не проверяет кэш 2-го уровня на наличие ассоциаций при создании «просто прокси». Вам необходимо получить доступ к объектам, чтобы инициализировать их, что затем вызовет попадание в кэш 2-го уровня.

Я бы рекомендовал вам вместо этого использовать подход DTO. Я думаю, что это идеальный вариант использования для представлений объектов с сохранением Блейза.

Я создал библиотеку, позволяющую легко сопоставлять модели JPA с моделями, определяемыми пользовательским интерфейсом или абстрактным классом, что-то вроде весенних прогнозов данных на стероидах. Идея заключается в том, что вы определяете свою целевую структуру (модель домена) так, как вам нравится, и сопоставляете атрибуты (геттеры) через выражения JPQL с моделью сущности.

Модель DTO для вашего варианта использования может выглядеть следующим образом с помощью представлений объектов Blaze-Persistence:

 @EntityView(Event.class)
public interface EventDto {
    @IdMapping
    Long getId();
    VenueDto getVenue();
    @MappingIndex("orderId")
    List<TicketLinkDto> getTicketLinks();
    @MappingIndex("orderId")
    List<ArtistEventDto> getLineup();

    @EntityView(Venue.class)
    interface VenueDto {
        @IdMapping
        Long getId();
        CityDto getCity();
    }
    @EntityView(City.class)
    interface CityDto {
        @IdMapping
        Long getId();
        String getName();
    }
    @EntityView(TicketLink.class)
    interface TicketLinkDto {
        @IdMapping
        Long getId();
        String getName();
    }
    @EntityView(ArtistEvent.class)
    interface ArtistEventDto {
        @IdMapping
        Long getId();
        ArtistDto getArtist();
    }
    @EntityView(Artist.class)
    interface ArtistDto {
        @IdMapping
        Long getId();
        String getName();
    }
}
  

Запрос — это вопрос применения представления сущности к запросу, простейшим из которых является просто запрос по идентификатору.

EventDto a = entityViewManager.find(entityManager, EventDto.class, id);

Интеграция данных Spring позволяет использовать его почти как проекции данных Spring: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features

 Optional<EventDto> findEventPageViewGraphById(long id);
  

Ответ №2:

Спасибо, Кристиан, за твой ответ. Я решил проблему, инициализировав объекты с помощью статического метода Hibernate.initialize(), как описано здесь https://vladmihalcea.com/initialize-lazy-proxies-collections-jpa-hibernate /

 @Transactional(readOnly = true)
public Optional<Event> loadEventPageViewGraph(long id) {
    Optional<Event> eventO = eventRepository.findEventPageViewGraphById(id);
    if(eventO.isPresent()) {
        Hibernate.initialize(eventO.get());
        Hibernate.initialize(eventO.get().getVenue().getCity());
        for (ArtistEvent artistEvent: eventO.get().getLineup()) {
            Hibernate.initialize(artistEvent.getArtist());
        }
        Hibernate.initialize(eventO.get().getTicketLinks());
        return eventO;
    } else {
        return Optional.empty();
    }
}
  

Хотя я согласен, что в целом лучше использовать DTO / projections . Однако при использовании DTO возникает проблема с извлечением проекций, которые включают связанные коллекции (свойства @OneToMany), как описано здесь https://vladmihalcea.com/one-to-many-dto-projection-hibernate /. В частности, в случае, когда мы не хотим выбирать все свойства объекта. Я обнаружил, что представления сущностей с сохранением блеска имеют хорошее решение для этого https://persistence.blazebit.com/documentation/1.6/entity-view/manual/en_US/#subset-basic-collection-mapping. Я проверю это.

Комментарии:

1. Другой вариант — выполнить рендеринг представления Thymeleaf в той же транзакции, в которой вы извлекаете данные из БД