R2DBC 代码编写指南
保持 R2DBC 查询、订阅期语义、上下文和事务一致。
浏览全部手册
本页目录
文档来源与 Markdown 原文
权威正文:ineed-core/docs/project-development/r2dbc-code-guide.md。网站按工作区快照同步,原文中的历史日期和验证范围保留。
开始编码前核对同版本源码;跨仓文件引用可在源码定位目录查找。
下载 Markdown 原文 ↓本文约定业务模块中 *-r2dbc 子模块的标准写法。R2DBC 当前不引入额外查询 DSL,继续使用 core 的集中 SQL 支撑,但实体类型、表名、默认别名、基础过滤和排序必须由 core 统一收口,避免业务 Manager 中散落第二套规则。
基本原则
- 业务
Repository是 R2DBC operation,必须组合 core repository fragment,并继承GetEntityClassOperation<E>。 - 业务
Repository保留default getEntityClass();业务Manager不再实现getEntityClass()。 - 业务
Manager只通过getOperation()返回 repository;core 默认通过getOperation().getEntityClass()获取实体类型。 - 查询列、过滤列、排序列必须通过
getQualifiedColumn("column_name")生成;不要在appendWhereOfQuery(...)或getKeySearchColumns()中直接写裸列名。 - 不需要 join 或 VO 扩展字段时,不覆盖
buildFrom()、buildSelect()、getDefaultTableAlias()。 - 需要 join 时,只覆盖必要方法:
getDefaultTableAlias()、buildSelect()、buildJoin();主表列继续通过getQualifiedColumn(...)引用。 - 子表、关系表、从表的
findBy.../countBy.../deleteBy...放在具体Repository,优先使用 Spring Data R2DBC 派生方法。 - 自定义
RepositoryImpl只在 fragment 或派生方法覆盖不了时新增,并且实体类型、表名、row mapper 解析必须延迟到订阅期。
Repository 写法
业务 Repository 负责暴露持久化能力和实体类型:
public interface ParameterValueRepository extends
//@formatter:off
ReactiveCrudRepository<ParameterValue, String>,
CreateRepository<ParameterValue>,
UpdateRepository<ParameterValue>,
FindByIdRepository<ParameterValue>,
LogicDeleteRepository<ParameterValue>,
DataStatusRepository<ParameterValue>,
GetEntityClassOperation<ParameterValue> {
//@formatter:on
@Override
default Class<ParameterValue> getEntityClass() {
return ParameterValue.class;
}
Mono<Long> countByParameterIdAndDeleted(String parameterId, Byte deleted);
Mono<Void> deleteByParameterId(String parameterId);
}
不要为普通派生方法生成 RepositoryImpl。只有需要手写复杂 SQL、框架派生方法无法表达,或者 core fragment 无法覆盖时,才新增具体实现类。
Manager 写法
业务 Manager 组合 core 默认能力,只提供 getOperation() / converter / 生命周期钩子 / 查询条件:
@Component
@RequiredArgsConstructor
@Slf4j
public class ParameterValueManager extends AbstractR2dbcManager implements
//@formatter:off
CreateManager<ParameterValue, ParameterValueAO, ParameterValueVO, ParameterValueRepository>,
UpdateManager<ParameterValue, ParameterValueAO, ParameterValueVO, ParameterValueRepository>,
FindByIdManager<ParameterValue, ParameterValueVO, ParameterValueRepository>,
SearchManager<ParameterValue, ParameterValueQuery, ParameterValueVO> {
//@formatter:on
private final ParameterValueConverter parameterValueConverter;
private final ParameterValueRepository parameterValueRepository;
@Override
public ParameterValueRepository getOperation() {
return parameterValueRepository;
}
@Override
public EntityVoConverter<ParameterValue, ParameterValueVO> getEntityVoConverter() {
return parameterValueConverter;
}
}
不要在业务 Manager 中再写:
@Override
public Class<ParameterValue> getEntityClass() {
return ParameterValue.class;
}
R2DBC search、find、data status 等 core 默认能力都从 getOperation().getEntityClass() 读取实体类型。实体类型在 Repository 和 Manager 同时声明,会形成两套来源,后续很容易出现不一致。
Search 写法
getKeySearchColumns() 返回 SQL 列表达式,必须使用 getQualifiedColumn(...):
@Override
public String[] getKeySearchColumns() {
//@formatter:off
return new String[] {
getQualifiedColumn("parameter_value")
};
//@formatter:on
}
不要直接返回裸列名:
// 禁止
return new String[] {
"parameter_value"
};
appendWhereOfQuery(...) 同样使用 getQualifiedColumn(...):
@Override
public void appendWhereOfQuery(final List<String> conditions, final Map<String, Object> params, final ParameterValueQuery query) {
if (query == null) {
return;
}
if (StringUtils.isNotBlank(query.getParameterId())) {
conditions.add(getQualifiedColumn("parameter_id") + " = :parameterId");
params.put("parameterId", query.getParameterId());
}
}
不要写:
// 禁止
conditions.add("parameter_id = :parameterId");
core 默认的租户、经营场所、归属、逻辑删除、数据状态、排序条件都会通过 getQualifiedColumn(...) 走同一套列限定逻辑。业务查询条件如果直接写裸列名,在主表设置别名或引入 join 后很容易出现歧义。
Join 写法
只有查询结果确实需要关联表字段,或者过滤条件必须跨表时,才覆盖 join 相关方法:
@Override
public String buildSelect() {
return getQualifiedColumn("*")
+ ", g.application_key AS application_key"
+ ", g.group_key AS group_key"
+ ", g.group_name AS group_name";
}
@Override
public String buildJoin() {
return "LEFT JOIN icfg_parameter_group g ON g.id = " + getQualifiedColumn("group_id");
}
@Override
public String getDefaultTableAlias() {
return "p";
}
不需要 join 时不要为了统一格式强行覆盖:
// 禁止
@Override
public String buildFrom() {
return "table_name p";
}
默认 buildFrom() 已经会根据 getDefaultTableAlias() 生成 table alias。如果没有默认别名,默认查询就是实体表名。
RepositoryImpl 写法
自定义 R2DBC repository 实现必须保持响应式订阅期语义:
return getTableNameMono().flatMap(tableName -> {
final String sql = "SELECT * FROM " + tableName + " WHERE id = :id";
return databaseClient.sql(sql)
.bind("id", id)
.map(getEntityRowMapper())
.one();
});
不要在 Publisher 装配期提前求值:
// 禁止
final Class<E> entityClass = getEntityClass();
return Mono.just(getTableName());
这类写法会把“不支持该操作”从订阅期提前成同步异常,破坏 reactive 调用契约。
验证命令
改造或新增 R2DBC 模块后,至少执行对应模块编译:
mvn -pl :{module}-r2dbc -am -DskipTests compile
批量改造后扫描业务 R2DBC Manager:
find . -path '*/target/*' -prune -o \
-path '*-r2dbc/src/main/java/*/manager/*.java' -type f -name '*.java' -print0 \
| xargs -0 rg "public Class<[^>]+> getEntityClass\\(|conditions\\.add\\(\"[a-zA-Z_][a-zA-Z0-9_]* (?:=|LIKE|IN|BETWEEN|IS)|\"[a-zA-Z_][a-zA-Z0-9_]*\""
预期结果:
- 业务 R2DBC
Manager没有手写getEntityClass()。 appendWhereOfQuery(...)中普通主表列条件通过getQualifiedColumn(...)构建。getKeySearchColumns()返回getQualifiedColumn(...)结果。- 没有不必要的
buildFrom()/buildSelect()/getDefaultTableAlias()覆盖。 - 业务 R2DBC
Repository保留GetEntityClassOperation<E>和getEntityClass()。
常见错误
| 错误 | 正确处理 |
|---|---|
Manager 需要实体类型 |
不实现 getEntityClass();通过 getOperation().getEntityClass() 走 core 默认实现 |
getKeySearchColumns() 返回裸列名 |
改为 getQualifiedColumn("column_name") |
appendWhereOfQuery(...) 直接写 "column = :param" |
改为 getQualifiedColumn("column") + " = :param" |
为普通查询覆盖 buildFrom() |
删除覆盖,使用 core 默认 buildFrom() |
| 为普通查询设置别名 | 不需要别名时删除 getDefaultTableAlias() |
| 需要 join 后主表列歧义 | 保留 getDefaultTableAlias(),主表列统一走 getQualifiedColumn(...) |
简单子表 count/delete 写在 Manager |
下沉到对应 Repository 派生方法 |
自定义 RepositoryImpl 装配期求值 |
改为 Mono.defer(...)、getEntityClassMono() 或 getTableNameMono() |
最后更新:2026-06-02
仍有疑问?按反馈清单整理复现信息 →