仅当 Mono 为空时如何执行操作,如果不为空则抛出错误

Posted

技术标签:

【中文标题】仅当 Mono 为空时如何执行操作,如果不为空则抛出错误【英文标题】:How to perform an action only if the Mono is empty and throw an error if not empty 【发布时间】:2020-01-27 08:13:01 【问题描述】:

我正在尝试将项目转换为使用 Spring WebFlux,但在使一些基本业务逻辑正常工作时遇到了问题。我有一个负责检索/保存记录的存储库层和一个负责应用程序业务规则的服务层。我想要做的(在服务层)是检查给定用户名的用户是否已经存在。如果是这样,我想回复一个错误。如果没有,我想允许插入发生。

我在存储库层调用了一个方法,该方法将通过用户名查找用户,如果未找到,它将返回一个空的 Mono。这按预期工作;但是,我尝试了 flatMap 和 (defaultIfEmpty 和 swithIfEmpty) 的各种组合,但无法编译/构建。

    public Mono<User> insertUser(User user) 
        return userRepository.findByUsername(user.username())
            .flatMap(__ -> Mono.error(new DuplicateResourceException("User already exists with username [" + user.username() + "]")))
            .switchIfEmpty(userRepository.insertUser(user));
    

我得到的错误是 Mono&lt;Object&gt; cannot be converted to Mono&lt;User&gt;,所以 swithIfEmpty 似乎没有反映适当的类型,并且转换似乎也不起作用。

【问题讨论】:

你用的是什么编译器? 您使用什么来将数据保存在存储库中? 我正在使用 Java 11 JDK 构建。持久化是在 Postgresql 中完成的。但是,我现在只是想让一个单元测试适用于这个逻辑,所以存储库层被模拟了。 【参考方案1】:

经过额外的测试,并考虑到其他开发人员的反馈,我找到了以下解决方案:

    public Mono<User> insertUser(User user) 
        return userRepository.findByUsername(user.username())
            .flatMap(__ -> Mono.error(new DuplicateResourceException("User already exists with username [" + user.username() + "]")))
            .switchIfEmpty(Mono.defer(() -> userRepository.insertUser(user)))
            .cast(User.class);
    

正如 Thomas 所说,编译器感到困惑。我的假设是因为flatMap 返回一个带有错误的 Mono,而 switchIfEmpty 返回一个带有用户的 Mono,因此它恢复为带有 Object 的 Mono(因此额外的 .cast 运算符让它编译) .

另一个添加是在switchMap 中添加Mono.defer。否则,switchIfEmpty 总是在触发。

我仍然对其他建议/替代方案持开放态度(因为这似乎是一种相当普遍的需求/模式)。

【讨论】:

嗨@cgaskill!为什么要使用 Mono.defer 方法?【参考方案2】:

您收到此编译器错误的原因如下。

flatmap 获取已完成的Mono 中的内容,并尝试将其转换为它可以推断的任何类型。 Mono.error 包含一个类型,该类型属于Object

一种方法可能是将您的逻辑移动到平面图中。

// This is just example code using strings instead of repos
public Mono<String> insertUser(String user) 
    return Mono.just(user)
            // Here we map/convert instead based on logic
            .flatMap(__ -> 
                if (__.isEmpty())
                    return Mono.error(new IllegalArgumentException("User already exists with username [" + user + "]"));
                return Mono.just(user);
            ).switchIfEmpty(Mono.just(user));

switchIfEmpty 不适合做出合乎逻辑的决定,恕我直言。文档说明

如果此单声道已完成,则回退到备用单声道 没有数据

如果我们没有得到任何东西,这更像是对其他东西的后备,因此我们可以保持数据流继续进行。

你也可以

Mono.empty().doOnNext(o -> 
        throw new IllegalArgumentException("User already exists with username [" + o + "]");
    ).switchIfEmpty(Mono.just("hello")).subscribe(System.out::println);

【讨论】:

flatMap 将返回通过调用userRepository.findUserByUsername 找到的用户,它没有像字符串那样的isEmpty 方法。我的期望是,如果对userRepository.findUserByUsername 的调用简单地完成而不发出用户,则flatMap 中的代码甚至不会被调用,switchIfEmpty(或defaultIfEmpty)中的代码将被调用,这将允许我插入用户(因为它们不存在)。 ofc 你的用户没有isEmpty 方法,这不是重点。您可以改为空检查。如果完成但返回空,则不会进入平面地图。但是如果它完成并且有一个值,编译器怎么知道你想要返回什么,你现在告诉它返回Mono&lt;Object&gt;。但是编译器怎么知道你的意图呢? flatMap 需要能够推断返回类型,并且在您的代码中,您告诉它推断 Mono 的返回类型。你必须像编译器一样思考,编译器是怎么知道的?好吧它没有。 但是,我的解决方案是在数据库中的字段上放置一个唯一约束,然后您不需要在应用程序中执行所有这些逻辑,您总是尝试保存,如果保存失败,将错误返回给客户端。 如果您在数据库中执行实际删除,这将起作用。我们的过程是永不删除(我们不喜欢丢失数据),因此我们执行逻辑删除(我们停用记录)。所以我们不能有一个唯一的约束,因为我们希望允许另一个用户使用相同的用户名(只要它是非活动的)。所以只有 1 条具有该用户名的活动记录。【参考方案3】:

我正在使用具有类型参数E 的抽象类,所以我不能使用.cast(E.class)。我们的解决方案是

private Mono<E> checkIfStatementExists(E statement) 
    return this.statementService.getByStatementRequestId(statement.getStatementRequestId())
            .flatMap(sr -> Mono.<E>error(new ValidationException("Statement already exists for this request!")))
            .switchIfEmpty(Mono.just(statement));

我想我需要在下周与我的同事讨论这件事。

编辑。我们和同事讨论过,更新的代码在上面。

【讨论】:

【参考方案4】:

遇到同样的问题,结果在下面做,

请注意:这不需要类型转换并且适用于两种情况,即

如果元素已存在于数据库中,则抛出错误。

插入元素,否则返回 Mono。

  public Mono<UserDTO> insertUser(User user) 
     return this.userRepository.findByUsername(user.getUsername())
             .flatMap(foundUser-> null != foundUser ? Mono
                     .error(new UserAlreadyExistsException(ExceptionsConstants.USER_ALREADY_EXISTS_EXCEPTION))
                     : Mono.just(user))
             .switchIfEmpty(this.userRepository.save(user))
             .map(userCreated -> copyUtils.createUserDTO(userCreated));
  

【讨论】:

以上是关于仅当 Mono 为空时如何执行操作,如果不为空则抛出错误的主要内容,如果未能解决你的问题,请参考以下文章

仅当搜索栏文本不为空时,如何运行过滤器功能?

仅当变量不为空时,如何应用 Hasura `where` 过滤器?

SwiftUI:仅当输入不为空时才启用保存按钮

仅当对象在一行上不为空时才设置属性[重复]

仅当字段不为空时才进行电子邮件验证

仅当查询不为空时,才从查询写入 BigQuery 中的表