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

