# MyBatis-Flex 代码编写指南

本文约定业务模块中 `*-mybatis-flex` 子模块的标准写法。目标是让 MyBatis-Flex 和 JPA / QueryDSL 一样，使用编译期生成的结构化元数据表达表、列、关联和排序，避免业务代码散落表名、列名字符串。

## 基本原则

- `mybatis-flex-processor` 生成的 `entity.table.*TableDef` 是 MyBatis-Flex 查询元数据的唯一业务来源。
- 业务 `Mapper` 优先组合 core 标准能力接口，例如 `CreateMapper`、`UpdateMapper`、`FindByIdMapper`；这些能力接口已经统一继承 `MybatisFlexOperation<E>`。
- 业务 `Mapper` 必须实现 `getTableDef()`；没有任何标准能力接口可继承的特殊 mapper，才直接继承 `MybatisFlexOperation<E>`。
- 业务 `Manager` 只提供 `getOperation()` 作为 MyBatis-Flex 持久化入口，不再提供 `getMapper()`。
- 业务 `Manager` 不实现 `getEntityClass()`；core 默认通过 `getOperation().getEntityClass()` 获取实体类型，`Mapper` 再由 `getTableDef()` 解析实体类型。
- 查询、删除、计数、join、orderBy 优先使用 `QueryWrapper` + generated `*TableDef`。
- 关系表、子表、从表的 `findBy...` / `countBy...` / `deleteBy...` 放在具体 `Mapper`，`Manager` 只做业务编排。
- 业务代码不要使用 `QueryMethods.column("...")`、表名字符串或列名字符串拼查询。
- 如果生成类缺失，先修 `pom.xml` 的 processor 配置，不退回手写字符串 SQL。

## Processor 配置

每个 `*-mybatis-flex` 模块必须独立配置 `mybatis-flex-processor`，类似 JPA 模块接入 QueryDSL APT。

```xml
<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <configuration>
        <annotationProcessorPaths combine.children="append">
          <path>
            <groupId>com.mybatis-flex</groupId>
            <artifactId>mybatis-flex-processor</artifactId>
            <version>${mybatis-flex.version}</version>
          </path>
        </annotationProcessorPaths>
      </configuration>
    </plugin>
  </plugins>
</build>
```

生成类路径应为：

```text
{basePackage}.mybatisflex.entity.table.{Entity}TableDef
```

业务代码直接 import 生成类，例如：

```java
import com.ineed.config.parameter.mybatisflex.entity.table.ParameterTableDef;
```

## Mapper 写法

业务 `Mapper` 负责持久化操作入口和明确的表级查询方法。标准能力接口已经继承 `MybatisFlexOperation<E>`，所以业务 mapper 通常只组合需要的能力接口并实现 `getTableDef()`：

```java
@Mapper
public interface ParameterMapper extends
//@formatter:off
  CreateMapper<Parameter>,
  UpdateMapper<Parameter>,
  PhysicsDeleteMapper<Parameter>,
  LastModifiedMapper<Parameter>,
  DataStatusMapper<Parameter>,
  FindByIdMapper<Parameter>,
  LogicDeleteMapper<Parameter> {
//@formatter:on

  @Override
  default ParameterTableDef getTableDef() {
    return ParameterTableDef.PARAMETER;
  }
}
```

如果某个 mapper 不需要任何标准能力接口，但仍需要作为 MyBatis-Flex 持久化入口使用，可以直接继承 `MybatisFlexOperation<E>`：

```java
@Mapper
public interface ParameterSnapshotMapper extends MybatisFlexOperation<ParameterSnapshot> {

  @Override
  default ParameterSnapshotTableDef getTableDef() {
    return ParameterSnapshotTableDef.PARAMETER_SNAPSHOT;
  }
}
```

不要在业务 `Mapper` 中再写：

```java
default Class<Parameter> getEntityClass() {
  return Parameter.class;
}
```

`MybatisFlexOperation` 通过 `GetTableDefOperation` 承接实体类型解析，`getTableDef()` 默认解析实体类型。如果实体包名和生成类包名不符合 `entity` / `entity.table` 约定，应先修包结构或生成配置，不在业务代码里补第二套实体类型声明。

## Mapper 查询方法

子表、关系表、从表的直接外键访问放在具体 `Mapper`：

```java
default long countByGroupIdAndDeleted(final String groupId, final Byte deleted) {
  final QueryWrapper queryWrapper = QueryWrapper.create();
  queryWrapper.where(getTableDef().GROUP_ID.eq(groupId));
  queryWrapper.and(getTableDef().DELETED.eq(deleted));
  final Long count = selectCountByQuery(queryWrapper);
  return count == null ? 0L : count;
}

default void deleteByGroupId(final String groupId) {
  final QueryWrapper queryWrapper = QueryWrapper.create();
  queryWrapper.where(getTableDef().GROUP_ID.eq(groupId));
  deleteByQuery(queryWrapper);
}
```

不要把这类逻辑写成 `Manager` 中的通用 helper：

```java
// 禁止
countActiveValue(BaseMapper<?> mapper, String columnName, String columnValue)
deleteValue(BaseMapper<?> mapper, String columnName, String columnValue)
```

也不要写：

```java
// 禁止
QueryMethods.column("group_id").eq(groupId)
```

正确写法是：

```java
getTableDef().GROUP_ID.eq(groupId)
```

## Manager 写法

业务 `Manager` 组合 core 默认能力，只提供 `getOperation()` / converter 入口，不再提供 `getMapper()`，也不提供 `getEntityClass()`。需要实体类型时走 core 默认实现：`getOperation().getEntityClass()`。

```java
@Component
@RequiredArgsConstructor
@Slf4j
public class ParameterManager extends AbstractMybatisFlexManager implements
//@formatter:off
  IParameterManager,
  CreateOrRefreshManager<Parameter, ParameterAO, ParameterVO, ParameterMapper>,
  EnableManager<Parameter, ParameterMapper>,
  LockManager<Parameter, ParameterMapper>,
  PublishManager<Parameter, ParameterMapper>,
  FindByIdManager<Parameter, ParameterVO, ParameterMapper>,
  LogicDeleteManager<Parameter, ParameterMapper>,
  PhysicsDeleteManager<Parameter, ParameterMapper>,
  SearchManager<Parameter, ParameterQuery, ParameterVO>,
  SearchRecycleManager<Parameter, ParameterQuery, ParameterVO> {
//@formatter:on

  private static final ParameterTableDef PARAMETER = ParameterTableDef.PARAMETER;

  private final ParameterConverter parameterConverter;

  private final ParameterMapper parameterMapper;

  @Override
  public ParameterMapper getOperation() {
    return parameterMapper;
  }

  @Override
  public AoEntityConverter<Parameter, ParameterAO> getCreateEntityConverter() {
    return parameterConverter;
  }
}
```

`getOperation()` 返回当前 mapper，是 Manager 到 MyBatis-Flex 的唯一持久化入口。core 的 search、find、delete、data status 等默认能力都通过这个入口调用 mapper 方法、读取 `getTableDef()` 和 `getEntityClass()`。不要为了“语义清晰”再在业务 Manager 中补一个 `getMapper()` 委托方法，否则同一对象会出现两套入口，后续很容易出现返回对象不一致的问题。

`Manager` 中可以声明当前查询需要的 `TableDef` 常量。需要 join 时使用 alias：

```java
private static final ParameterTableDef PARAMETER_TABLE = ParameterTableDef.PARAMETER.as("p");

private static final ParameterGroupTableDef PARAMETER_GROUP_TABLE = ParameterGroupTableDef.PARAMETER_GROUP.as("g");
```

## Search 写法

`getKeySearchColumns()` 返回 `QueryColumn[]`，必须直接返回 generated 列：

```java
@Override
public QueryColumn[] getKeySearchColumns() {
  //@formatter:off
  return new QueryColumn[] {
      PARAMETER.PARAMETER_KEY,
      PARAMETER.PARAMETER_NAME
  };
  //@formatter:on
}
```

不要返回字符串：

```java
// 禁止
return new QueryColumn[] {
    "parameter_key",
    "parameter_name"
};
```

`appendWhereOfQuery(...)` 使用 generated 列：

```java
@Override
public void appendWhereOfQuery(final QueryWrapper wb, final ParameterQuery query) {
  if (query == null) {
    return;
  }
  if (StringUtils.isNotBlank(query.getGroupId())) {
    wb.and(PARAMETER.GROUP_ID.eq(query.getGroupId()));
  }
  if (StringUtils.isNotBlank(query.getParameterName())) {
    wb.and(PARAMETER.PARAMETER_NAME.like("%" + query.getParameterName() + "%"));
  }
}
```

如果 `query` 中有字段，但当前实体表没有对应列，不能为了编译通过回退到 `QueryMethods.column("...")`。应先判断：

- 字段是否是契约遗留，应从当前 MyBatis-Flex 查询中移除。
- 字段是否本应存在于实体，应先补实体 `@Column` 和数据库字段，再由 processor 生成列。
- 字段是否来自关联表，应通过 join 引入对应表的 generated `TableDef`。

## Join 写法

join 必须使用 generated `TableDef` 和 alias，不写表名字符串：

```java
//@formatter:off
final QueryWrapper queryWrapper = QueryWrapper.create()
    .select(PARAMETER_TABLE.DEFAULT_COLUMNS)
    .select(
        PARAMETER_GROUP_TABLE.GROUP_KEY.as("groupKey"),
        PARAMETER_GROUP_TABLE.GROUP_NAME.as("groupName"))
    .from(PARAMETER_TABLE)
    .leftJoin(PARAMETER_GROUP_TABLE)
    .on(PARAMETER_GROUP_TABLE.ID.eq(PARAMETER_TABLE.GROUP_ID))
    .where(PARAMETER_TABLE.DELETED.eq(CoreYesNo.NO.getByte()));
//@formatter:on
```

跨表删除前检查也要指向被检查表的 `TableDef`：

```java
//@formatter:off
final long count = dataScopeRuleMapper.selectCountByQuery(QueryWrapper.create()
    .from(DATA_SCOPE_RULE_VALUE)
    .where(DATA_SCOPE_RULE_VALUE.RULE_ID.eq(entity.getId()))
    .and(DATA_SCOPE_RULE_VALUE.DELETED.eq(CoreYesNo.NO.getByte())));
//@formatter:on
```

不要写：

```java
// 禁止
.from("isec_data_scope_rule_value")
.where(QueryMethods.column("rule_id").eq(entity.getId()))
```

## 字段命名

generated `TableDef` 字段名来自实体属性，而不一定等同于数据库列名的直译。

例如实体字段：

```java
@Column("group_id")
protected String workGroupId;
```

生成列可能是：

```java
WORK_GROUP_ID
```

业务代码必须使用 generated 字段：

```java
WORK_GROUP_USER.WORK_GROUP_ID.eq(workGroupId)
```

不要凭数据库列名猜成：

```java
// 禁止
WORK_GROUP_USER.GROUP_ID.eq(workGroupId)
```

遇到编译错误时，以 `target/generated-sources/annotations/.../*TableDef.java` 为准。

## QueryMethods 使用边界

业务模块禁止使用 `QueryMethods.column("...")` 生成列。以下场景例外：

- core 内部工具类为了兼容通用字段名、函数表达式或默认排序做集中转换。
- MyBatis-Flex 没有 `QueryWrapper` 等价 API 的函数表达式，例如 `QueryMethods.distinct(tableDef.FIELD)`，前提是参数列来自 generated `TableDef`。

业务代码中如果只是 where / join / orderBy / select 普通列，必须使用 generated `TableDef`。

## 删除和关系维护

物理删除前后的关系处理保持分层：

- `Manager` 的 `beforePhysicsDelete` / `afterPhysicsDelete` 只编排业务动作。
- 关系表删除、子表计数、外键查询落到具体 `Mapper`。
- 查询条件使用关系表自己的 `TableDef`，不要拿主表 `TableDef` 查子表字段。

示例：

```java
@Override
public void beforePhysicsDelete(final Parameter beDelete) {
  parameterValueMapper.deleteByParameterId(beDelete.getId());
}
```

对应 mapper：

```java
default void deleteByParameterId(final String parameterId) {
  final QueryWrapper queryWrapper = QueryWrapper.create();
  queryWrapper.where(getTableDef().PARAMETER_ID.eq(parameterId));
  deleteByQuery(queryWrapper);
}
```

## 验证命令

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

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

批量改造后应扫描业务 MyBatis-Flex 源码：

```bash
find . -path '*/target/*' -prune -o \
  -path '*mybatis*flex*/src/main/java/*' -type f -name '*.java' -print0 \
  | xargs -0 rg "QueryMethods\\.column\\(|GetEntityClassOperation|default Class<[^>]+> getEntityClass\\(|public Class<[^>]+> getEntityClass\\("
```

同时扫描 MyBatis-Flex Manager 是否残留 `getMapper()`：

```bash
find . -path '*/target/*' -prune -o \
  -path '*mybatis*flex*/src/main/java/*/manager/*.java' -type f -name '*.java' -print0 \
  | xargs -0 rg "public .* getMapper\\("
```

预期结果：

- 业务 MyBatis-Flex 模块没有 `QueryMethods.column(...)`。
- 业务 MyBatis-Flex 模块没有直接继承 `GetEntityClassOperation`。
- 业务 mapper / manager 没有手写 `getEntityClass()`。
- 业务 Manager 没有 `getMapper()`，MyBatis-Flex 持久化入口统一为 `getOperation()`。
- `GetEntityClassOperation` 只由 core 的 `GetTableDefOperation` / `MybatisFlexOperation` 间接承接。

## 常见错误

| 错误 | 正确处理 |
|------|----------|
| 找不到 `*TableDef` | 检查 `mybatis-flex-processor` 是否配置，先编译生成 |
| `TableDef` 没有某列 | 以实体字段和生成类为准；不要手写字符串列绕过 |
| `Mapper` 不知道继承什么 | 优先继承 core 标准 mapper 能力接口；没有标准能力时再直接继承 `MybatisFlexOperation<E>` |
| `Manager` 需要实体类型 | 不实现 `getEntityClass()`；通过 `getOperation().getEntityClass()` 走 core 默认实现 |
| `Manager` 想拿 mapper | 直接使用 `getOperation()`；不要新增 `getMapper()` 委托 |
| 子表计数写在 `Manager` | 下沉到子表 `Mapper` 的明确方法 |
| join 中写 `.from("table")` | 改为 `.from(TABLE_DEF)` 或 `.from(TABLE_DEF.as("alias"))` |
| key search 返回字符串 | 改为 `QueryColumn[]` generated 列 |
| generated 字段名与列名不一致 | 使用 generated 字段名，不按数据库列名猜测 |

> 最后更新：2026-06-02
