在 Spring 启动测试中注入一个 JPA 存储库,没有会话问题

inject a JPA repository in Spring Boot test, without session issue

我正在对我的 Spring Boot 2.4 应用程序添加一些测试,该应用程序在生产中运行良好。

在我的一个 SpringBootTest 中,我调用 API(使用 mockMvc)并将结果与​​数据库中的结果进行比较。

@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());

    }
}

我觉得我已经编写了数百次这种类型的测试,但是在这一次,我遇到了一个问题:我的应用程序配置正确,因为我收到了 [=44 中的项目列表=]响应。

但是,我觉得奇怪的是我从 ticketTypeRepository.findAll() 调用中得到一个异常:

failed to lazily initialize a collection of role ... could not initialize proxy - no Session

我理解这个问题,我可以通过使关系变得急切(在实体上使用 @Fetch(FetchMode.JOIN))或进行测试来解决它 @Transactional 但我不确定我是否喜欢任何选项..

我不记得过去在其他 Spring 启动测试中遇到过这个问题,所以我有点困惑。

我是否遗漏了一些东西来确保对 ticketTypeRepository 的所有调用都是在事务中进行的? TicketTypeRepositoryCrudRepository 的包装器,这是它不能直接工作的原因吗?

这是实体和存储库代码:


    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());

    }
}

和实体(简体):

    @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());
      }
    }

第一次对集合调用 size() 时发生异常:

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.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)

我相信你 mis-interpret 堆栈跟踪。问题不在于对 findAll() 的结果调用方法 size(),而在于方法 findAll 本身。

findAll 中调用 TicketTypeEntity.toTicketTypeList,它将 DB 实体转换为 DTO。此方法涉及ticketGroupsEntity,这是一个惰性集合。

代码在单元测试中失败,但在通过 springs 控制器访问时运行。 这是由于在视图中打开会话,默认启用。

参见:

您可以通过多种方式解决:

  • @Transactional findAll(注意延迟加载问题)
  • 查询中的显式提取
  • 实体图

但在我看来,您的实体映射看起来很可疑,您似乎拥有在 TicketGroupsEntity 中构造 TicketType 所需的所有数据。也许您可以改为查询该实体?