I am adding some tests on my Spring Boot 2.4 application that works well in production.
In one of my SpringBootTest
, I call the API (using mockMvc
) and compare the result with what I have in the DB.
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
class TicketIT {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private TicketTypeRepository ticketTypeRepository;
@Test
void shouldReturnListOfTicketTypes() throws Exception {
RequestBuilder request =
MockMvcRequestBuilders.get(RESOURCE_BASE_URL + "/types").contentType(APPLICATION_JSON);
String responseAsString =
mockMvc
.perform(request)
.andExpect(status().isOk())
.andReturn()
.getResponse()
.getContentAsString();
List<TicketTypesRepresentation> ticketTypes =
objectMapper.readValue(
responseAsString, new TypeReference<List<TicketTypesRepresentation>>() {
});
assertThat(ticketTypes).hasSameSizeAs(ticketTypeRepository.findAll());
}
}
I have the feeling I've written that type of tests hundreds of times, but on this one, I am facing a problem : my application is configured correctly, because I receive a list of items in the API response.
However, what I find strange is that I get an exception from the ticketTypeRepository.findAll()
call :
failed to lazily initialize a collection of role ... could not initialize proxy - no Session
I understand the issue, and I can fix it either by making the relation eager (with @Fetch(FetchMode.JOIN)
on the entity), or my making the test @Transactional
but I am not sure I like any of the options..
I don't remember facing that issue in the past in other Spring Boot tests so I am a bit puzzled.
Am I missing something to make sure that all the calls made to ticketTypeRepository are made within a transaction ? TicketTypeRepository
is a wrapper around a CrudRepository
, is it the reason why it doesn't work directly ?
Here's the entity and repository code :
public class JpaTicketTypeRepository implements TicketTypeRepository {
public List<TicketType> findAll() {
var allTicketTypesEntity= jpaTicketTypesEntityRepository.findAll();
return StreamSupport.stream(allTicketTypesEntity.spliterator(), false)
.map(TicketTypeEntity::toTicketTypeList)
.collect(Collectors.toList())
.stream().flatMap(List::stream)
.collect(Collectors.toList());
}
}
and the entity (simplified) :
@Table(name = "TICKET_TYPES")
@Entity
@Slf4j
public class TicketTypeEntity {
@Id
private Long id;
@OneToMany
@JoinTable(name = "TICKET_TYPES_GROUPS",
joinColumns =
{@JoinColumn(name = "TICKET_TYPE_ID", referencedColumnName = "ID")},
inverseJoinColumns =
{@JoinColumn(name = "TICKET_GROUP_ID", referencedColumnName = "ID")})
@Nonnull
private List<TicketGroupsEntity> ticketGroupsEntity;
@Nonnull
public List<TicketType> toTicketTypeList() {
log.info("calling toTicketTypeList for id "+id);
log.info(" with size : "+ticketGroupsEntity.size());
return ticketGroupsEntity.stream().map(group -> TicketType.builder()
.id(id)
.build()).collect(Collectors.toList());
}
}
The exception happens the first time size()
is called on the collection :
failed to lazily initialize a collection of role: my.service.database.entities.TicketTypeEntity.ticketGroupsEntity, could not initialize proxy - no Session org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: my.service.database.entities.TicketTypeEntity.ticketGroupsEntity, could not initialize proxy - no Session at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:606) at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:218) at org.hibernate.collection.internal.AbstractPersistentCollection.readSize(AbstractPersistentCollection.java:162) at org.hibernate.collection.internal.PersistentBag.size(PersistentBag.java:371) at my.service.database.entities.TicketTypeEntity.toTicketTypeList(TicketTypeEntity.java:78) at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195) at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1655) at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484) at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474) at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:913) at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:578) at my.service.database.JpaTicketTypeRepository.findAll(JpaTicketTypeRepository.java:29)
I believe you mis-interpret the stack trace. The problem is not in calling the method size()
on the result of findAll()
, but in the method findAll
itself.
In findAll
you call TicketTypeEntity.toTicketTypeList
, which converts DB entity to DTO. This method touches ticketGroupsEntity
, which is a lazy collection.
The code fails in unit test, but runs when accessed via springs controller. This is due to Open Session In View, which is enabled by default.
See:
You could solve it multiple ways:
But to my eyes your entity mapping looks suspicious, you seem to have all data needed to construct TicketType in TicketGroupsEntity. Maybe you could query that entity instead?