You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot响应式管道中repo.save()与repo.findById()执行异常排查

问题描述

基于Spring Boot 3.4应用结合响应式MS SQL Server驱动r2dbc-mssql进行测试,测试代码如下:

repo.save(User.builder().name("Joe Smoe").build())
        .doOnNext(saved -> log.info("Saved user: {}", saved))
        .flatMap(user -> repo.findById(user.getId()))
        .subscribe(user -> log.info("User: {}", user),
                error -> log.error("Error: {}", error.getMessage()));

repo.findAll()
        .subscribe(user -> log.info("User: {}", user));

输出结果:

Saved user: User(id=null, name=Joe Smoe)
Error: Id must not be null
User: User(id=1, name=Joe Smoe)

可见repo.save()返回的User对象并未包含生成的ID,但数据确实已存入数据库,因为repo.findAll()能查询到该对象。

请问在响应式保存流程中调用repo.save()后立即执行repo.findById()是否存在并发问题?我的使用方式是否有误?


相关代码

Repository定义

public interface UserRepository extends R2dbcRepository<User, Long> {}

User实体类

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "users")
public class User {
    @Id
    private Long id;  // 数据库中为int IDENTITY NOT NULL primary key
    private String name;
}

application.yml配置

spring:
  r2dbc:
    username: dbuser
    password: dbpassword
    url: r2dbc:mssql://localhost:1433/mydb

环境信息

  • JDK 23
  • Spring Boot 3.4.5
  • r2dbc-mssql: 1.0.2.RELEASE

问题解答

1. 核心原因:并非并发问题,是驱动版本的ID回填特性缺失

你遇到的问题和并发无关,本质是r2dbc-mssql 1.0.2.RELEASE版本不支持自动回填SQL Server自增IDENTITY列的ID到返回实体。SQL Server插入自增ID数据后,需要额外执行SELECT SCOPE_IDENTITY()获取生成的ID,但该版本的驱动默认不会处理这一步,导致save()返回的对象ID仍为null。

2. 使用方式的错误点

你在flatMap中直接调用repo.findById(user.getId()),但此时save()返回的对象ID还是null,自然触发Id must not be null的错误。

3. 解决方案

方案一:升级r2dbc-mssql版本

r2dbc-mssql 1.1.0及以上版本已修复该问题,会自动回填自增ID到返回实体。修改依赖版本即可:

<!-- Maven示例 -->
<dependency>
    <groupId>io.r2dbc</groupId>
    <artifactId>r2dbc-mssql</artifactId>
    <version>1.1.4.RELEASE</version>
</dependency>

方案二:自定义Save方法手动获取ID

若暂时无法升级,可在Repository中自定义方法,通过SQL手动插入并返回生成的ID:

public interface UserRepository extends R2dbcRepository<User, Long> {

    @Query("INSERT INTO users(name) VALUES (:name); SELECT SCOPE_IDENTITY() as id")
    Mono<Long> saveAndReturnId(@Param("name") String name);
}

调用逻辑修改为:

repo.saveAndReturnId("Joe Smoe")
        .doOnNext(id -> log.info("Saved user id: {}", id))
        .flatMap(repo::findById)
        .subscribe(user -> log.info("User: {}", user),
                error -> log.error("Error: {}", error.getMessage()));

方案三:补充ID生成策略注解(配合新版本驱动)

在实体类ID字段添加@GeneratedValue注解,明确指定自增策略:

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

注意:该方案需配合r2dbc-mssql 1.1.0+版本生效。

4. 关于响应式流程的并行说明

你的代码中save链和findAll是并行订阅执行的,findAll能查到数据是因为它在save完成后执行,但这不是导致save返回ID为null的原因。


内容的提问来源于stack exchange,提问作者Berti

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 02:12:16