# JPA QueryDSL 代码编写指南

本文约定业务模块中 `*-jpa` 子模块的标准 QueryDSL 写法。目标是让 JPA 自定义查询继续使用 QueryDSL 生成的 `Q*` 元数据，同时把实体字段投影、关联字段扩展和查询条件边界收口到稳定位置，避免业务 `Manager` 中散落重复字段清单。

## 核心原则

- JPA 自定义查询优先使用 Spring Data 派生查询或 QueryDSL，不写 native SQL。
- `buildVOQBean()` 中实体自身字段必须通过 `QuerydslUtil.projectionFields(entityClass, entityPath)` 或 `QuerydslUtil.projectionBean(entityClass, entityPath)` 提取。
- 只有 join 出来的扩展字段才在业务 `Manager` 中显式追加，例如 `group.groupKey.as("groupKey")`。
- 不要在 `buildVOQBean()` 中手写完整的 `table.id`、`table.tenantKey`、`table.createdTimestamp`、业务字段等长列表；这类列表应该由 core 按实体字段统一提取。
- `IBaseSearch#buildVOQBean()` 默认仍返回 `null`，不强制所有 JPA 查询走 QBean 投影；只有需要 join 填充额外 VO 字段时才覆盖。

## 实体字段投影

当查询只需要实体自身字段时，优先直接使用 core 工具：

```java
@Override
public QBean<SampleEntity> buildVOQBean() {
  return QuerydslUtil.projectionBean(SampleEntity.class, QSampleEntity.sampleEntity);
}
```

当查询还需要 join 字段时，实体字段仍由 core 提取，业务代码只追加额外字段：

```java
@Override
public QBean<Parameter> buildVOQBean() {
  final QParameter table = QParameter.parameter;
  final QParameterGroup group = QParameterGroup.parameterGroup;
  final List<Expression<?>> fields = new ArrayList<>(List.of(QuerydslUtil.projectionFields(Parameter.class, table)));
  fields.add(group.applicationKey.as("applicationKey"));
  fields.add(group.groupKey.as("groupKey"));
  fields.add(group.groupName.as("groupName"));
  //@formatter:off
  return Projections.fields(
      Parameter.class,
      fields.toArray(new Expression[0]));
  //@formatter:on
}
```

这种写法有两个好处：

- 实体新增、删除字段时，投影字段自动跟随实体持久化字段变化。
- join 字段一眼可见，代码审查时能快速识别哪些字段来自关联表。

## Join 查询

`buildQuery(...)` 只负责表达关联关系，不再顺带维护实体字段投影清单：

```java
@Override
public <T> JPAQuery<T> buildQuery(final JPAQuery<T> query) {
  final QParameter table = QParameter.parameter;
  final QParameterGroup group = QParameterGroup.parameterGroup;
  return query.leftJoin(group).on(group.id.eq(table.groupId));
}
```

如果 join 字段要映射到 VO / 查询返回对象，必须在 `buildVOQBean()` 中使用 `as("propertyName")` 明确别名，别名应对应目标对象属性名。

## 禁止写法

不要在 `buildVOQBean()` 中维护实体字段全量列表：

```java
return Projections.fields(
    Parameter.class,
    table.id,
    table.tenantKey,
    table.orderWeight,
    table.dataStatus,
    table.createdTimestamp,
    table.groupId,
    table.parameterKey,
    table.parameterName,
    table.parameterDescription,
    group.groupKey.as("groupKey"));
```

这个写法的问题是实体字段来源和 join 字段混在一起。实体字段变化时容易漏改，多个 Manager 也会产生重复字段列表。

## 验证

改造或新增 JPA QueryDSL 自定义查询后，至少执行对应模块编译：

```bash
mvn -pl <module>/<module>-jpa -am install -DskipTests
```

同时建议扫描业务 JPA Manager 是否仍残留全量实体字段手写投影：

```bash
rg -n "buildVOQBean|Projections\\.fields|table\\.id|table\\.tenantKey|table\\.createdTimestamp" <module> -g '*Manager.java'
```

允许保留少量业务 join 字段追加，但实体自身字段应优先来自 `QuerydslUtil.projectionFields(...)`。
