Spring 存储库并不总是抛出 DataIntegrityViolationException

Spring repository not always throwing DataIntegrityViolationException

我正在用 Spring 制作 REST API,但我无法对其进行单元测试。 我写了一个端点来更新用户组,当我在我的前端创建一个具有重复名称的组时,它确实发送了 409 冲突 (unique=true)。但是,当我进行单元测试时却没有。我发现在单元测试结束时添加这一行 groupRepository.findAll().forEach(g -> System.out.println(g.getName())); 确实会抛出 409.

端点:

@Override
public ResponseEntity<Object> update(@ApiParam(value = "form object to add to the store", required = true) @Valid @RequestBody FormGroupDTO group, @ApiParam(value = "Id of the form that needs to be updated", required = true) @PathVariable("groupId") Long groupId, @ApiParam(value = "token to be passed as a header", required = true) @RequestHeader(value = "token", required = true) String token) {
    String name = JWTutils.getEmailInToken(token);

    if(name == null) {
        return new ResponseEntity<>(HttpStatus.FORBIDDEN);
    }

    User user = userRepository.findByEmail(name);

    if(user == null){
        return  new ResponseEntity<>(HttpStatus.FORBIDDEN);
    }

    FormGroup groupModel = groupRepository.findByIdAndAdmin(groupId, user);

    if(groupModel == null){
        return  new ResponseEntity<>(HttpStatus.FORBIDDEN);
    }

    if(group.getMembers().stream().filter(m -> m.getRole() == UserFormGroupRole.ADMIN).toArray().length == 0){
        return new ResponseEntity<>(new ValidationErrorDTO("noAdmin", "MEMBER.NOADMIN"), HttpStatus.BAD_REQUEST);
    }

    // Get users
    groupModel.getUserFormGroups().clear();
    for(MemberDTO member : group.getMembers()){
        User u = userRepository.findByEmail(member.getEmail());
        if(u == null){
            return new ResponseEntity<>(new ValidationErrorDTO("notexist", "ADDUSER.NOTEXIST"), HttpStatus.BAD_REQUEST);
        }
        if(u.getRole() == UserRole.USER){
            return new ResponseEntity<>(new ValidationErrorDTO("notexist", "ADDUSER.NOTPRIVILEGED"), HttpStatus.BAD_REQUEST);
        }
        UserFormGroup ufg = userFormGroupRepository.findByUserAndFormGroup(u, groupModel);
        if(ufg == null){
            groupModel.getUserFormGroups().add(new UserFormGroup(u, groupModel, member.getRole()));
        } else{
            ufg.setRole(member.getRole());
            groupModel.getUserFormGroups().add(ufg);
        }
    }

    groupModel.setName(group.getName());

    try{
        groupRepository.save(groupModel);
        return  new ResponseEntity<>(HttpStatus.NO_CONTENT);
    } catch (DataIntegrityViolationException e){
        System.out.println(e.getMessage());
        System.out.println(e.getClass());
        return  new ResponseEntity<>(HttpStatus.CONFLICT);
    }
}

我的单元测试:

@Test
public void updateGroupNameAlreadyInUse() throws Exception {
    groupRepository.save(new FormGroup("newFormGroup", user));
    this.mockMvc.perform(put("/groups/" + group.getId())
            .header("token", token)
            .content(json(new FormGroupDTO("newFormGroup", group.getUserFormGroups().stream().map(ufg -> new MemberDTO(ufg.getUser().getEmail(), ufg.getRole())).collect(Collectors.toList()))))
            .contentType(contentType))
            .andExpect(status().isConflict());
}

我的 CrudRepository 的保存功能并不总是抛出 DataIntegrityViolationException。我刚刚意识到,也许我的单元测试的第一行 groupRepository.save(new FormGroup("newFormGroup", user)); 可能不会在我的单元测试结束之前执行,并且 findAll 函数会触发它。

简短的故事:您需要在插入测试后手动执行 flush()。现在让我们详细讨论。我假设您正在使用 ID 生成策略,例如 Sequence 或 UUID 或类似的东西。

有很多事情需要考虑:

同花顺

  • FlushMode - 确定 ORM 何时触发 SQL 语句。默认情况下,它在任何 SELECT 语句之前和事务提交之前触发。您获取每条记录名称的解决方案发出一个 SELECT 语句 - 触发所有未决 SQL 语句的刷新。
  • save()persist() 保证返回一个持久对象。这样的对象必须有一个 ID。一些 ID 生成策略(如 Identity)需要一个 INSERT 语句来生成 ID。其他人(如 SequenceUUID)- 不要。因此 ORM 可以在不插入记录的情况下获取 ID(它希望尽可能延迟一些优化)。

因此,在进行与 ORM 相关的测试时,您必须在对该数据执行任何操作之前手动调用 flush()

交易与会话

当您将事物标记为 @Transactional 时,行为是:

  1. 查看交易是否已在此线程中打开。
    • 如果是 - 什么都不做。
    • 如果否 - 创建事务并将其绑定到当前线程(通过 ThreadLocal 变量)。
  2. 方法完成后 - 检查事务是否由我启动。
    • 如果没有 - 什么也不做。
    • 如果是 - 提交事务,关闭会话。

我假设你用 @Transactional 标记你的测试。这意味着要测试谁启动会话和事务。存储库只使用已经打开的。然后因为没有提交 - 没有刷新。然后你使用 MockMvc - 在同一个线程中工作。它通过 @TransactionalOSIV 也发现交易已经开始。所以事务被重用了。

然后进入您的核心逻辑 - 您正在执行一些 SELECT 语句,这会刷新当前会话中未决的 SQL 语句。所以你原来的 save() 刚刚被冲洗了。

现在,在您的逻辑末尾,您再次执行 save(),它将 INSERT 语句放入待处理的 SQL 查询中。测试完成后,它只是回滚事务,最终的 INSERT 不会发生。除非.. 你正在做你提到的 SELECT 陈述。

所以最后 - 不要忘记在与 ORM 相关的测试中执行 flush()clear()。这些方法存在于 Hibernate 的 Session 或 JPA 的 EntityManager 中。前者可以用:SessionFactory#getCurrentSession()完成,后者可以用:

注入
@PersistenceContext 
EntityManager entityManager;

PS:我没有看到任何标有@Transactional的生产代码。如果不这样做,您可能 运行 会遇到问题。

PPS: this is not a unit test.