Reputation: 11
I'm developing a Spring Boot 2.3.4 web application with Spring Data JPA.
I want to use the Hibernate 2-nd level query cache for a repository method with @EntityGraph. However, I get a LazyInitializationException when generating a Thymeleaf view in case data is already in the 2-nd level cache unless I have Spring’s Open Session In View turned on. When fetching data for the first time from the database or without the 2nd level cache everything is OK even with spring.jpa.open-in-view=false. Moreover, if I enable spring.jpa.open-in-view there is no exception when fetching data from the cache without any select to the database.
How can I make Hibernate fetch at once all the associations specified in the @EntityGraph when using Hibernate 2nd level cache?
Here is my repository method:
@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);
and part of the entity:
@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<>();
}
Upvotes: 1
Views: 1045
Reputation: 11
Thank you Christian for your answer. I solved the problem by initializing entities with the static method Hibernate.initialize() as described here 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();
}
}
Though, I agree that in general it is better to use DTO's/projections. However, with DTO's there is a problem with fetching projections that include associated collections (@OneToMany properties) as described here https://vladmihalcea.com/one-to-many-dto-projection-hibernate/. In particular in the case when we don't want to select all of the entity properties. I found that Blaze-Persistence Entity Views has a nice solution for that https://persistence.blazebit.com/documentation/1.6/entity-view/manual/en_US/#subset-basic-collection-mapping. I'll check it out.
Upvotes: 0
Reputation: 16400
That's a known issue. Hibernate does not check the 2nd level cache for associations when constructing "just proxies". You need to access the objects to initialize them, which will then trigger a 2nd level cache hit.
I would recommend you use a DTO approach instead. I think this is a perfect use case for Blaze-Persistence Entity Views.
I created the library to allow easy mapping between JPA models and custom interface or abstract class defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure(domain model) the way you like and map attributes(getters) via JPQL expressions to the entity model.
A DTO model for your use case could look like the following with Blaze-Persistence Entity-Views:
@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();
}
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
EventDto a = entityViewManager.find(entityManager, EventDto.class, id);
The Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features
Optional<EventDto> findEventPageViewGraphById(long id);
Upvotes: 0