# R2DBC 代码编写指南

本文约定业务模块中 `*-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` 负责暴露持久化能力和实体类型：

```java
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 / 生命周期钩子 / 查询条件：

```java
@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` 中再写：

```java
@Override
public Class<ParameterValue> getEntityClass() {
  return ParameterValue.class;
}
```

R2DBC search、find、data status 等 core 默认能力都从 `getOperation().getEntityClass()` 读取实体类型。实体类型在 `Repository` 和 `Manager` 同时声明，会形成两套来源，后续很容易出现不一致。

## Search 写法

`getKeySearchColumns()` 返回 SQL 列表达式，必须使用 `getQualifiedColumn(...)`：

```java
@Override
public String[] getKeySearchColumns() {
  //@formatter:off
  return new String[] {
      getQualifiedColumn("parameter_value")
  };
  //@formatter:on
}
```

不要直接返回裸列名：

```java
// 禁止
return new String[] {
    "parameter_value"
};
```

`appendWhereOfQuery(...)` 同样使用 `getQualifiedColumn(...)`：

```java
@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());
  }
}
```

不要写：

```java
// 禁止
conditions.add("parameter_id = :parameterId");
```

core 默认的租户、经营场所、归属、逻辑删除、数据状态、排序条件都会通过 `getQualifiedColumn(...)` 走同一套列限定逻辑。业务查询条件如果直接写裸列名，在主表设置别名或引入 join 后很容易出现歧义。

## Join 写法

只有查询结果确实需要关联表字段，或者过滤条件必须跨表时，才覆盖 join 相关方法：

```java
@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 时不要为了统一格式强行覆盖：

```java
// 禁止
@Override
public String buildFrom() {
  return "table_name p";
}
```

默认 `buildFrom()` 已经会根据 `getDefaultTableAlias()` 生成 `table alias`。如果没有默认别名，默认查询就是实体表名。

## RepositoryImpl 写法

自定义 R2DBC repository 实现必须保持响应式订阅期语义：

```java
return getTableNameMono().flatMap(tableName -> {
  final String sql = "SELECT * FROM " + tableName + " WHERE id = :id";
  return databaseClient.sql(sql)
      .bind("id", id)
      .map(getEntityRowMapper())
      .one();
});
```

不要在 Publisher 装配期提前求值：

```java
// 禁止
final Class<E> entityClass = getEntityClass();
return Mono.just(getTableName());
```

这类写法会把“不支持该操作”从订阅期提前成同步异常，破坏 reactive 调用契约。

## 验证命令

改造或新增 R2DBC 模块后，至少执行对应模块编译：

```bash
mvn -pl :{module}-r2dbc -am -DskipTests compile
```

批量改造后扫描业务 R2DBC Manager：

```bash
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
