数据访问

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() 读取实体类型。实体类型在 RepositoryManager 同时声明,会形成两套来源,后续很容易出现不一致。

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

仍有疑问?按反馈清单整理复现信息 →