# 数据访问与事务检查表

## 🔴 P0 级问题

### 1. 事务内 DataSet 在事务外使用
**检查点**:
- 在 `TX.required` / `TX.requiresNew` 事务块内查询的 DataSet，是否在事务块外部使用？

**风险**: 事务结束后 DataSet 关联的数据库连接已释放，继续使用会抛异常或返回错误数据

**修正方案**:
```java
// ❌ 错误写法 - 事务内查询的 DataSet 在事务外使用
DataSet ds;
try (TXHandle h = TX.required("query")) {
    try {
        ds = QueryServiceHelper.queryDataSet("q", "entity", "id,name", null);
    } catch (Throwable e) {
        h.markRollback();
        throw e;
    }
}
// ds 在事务外使用，连接已释放！
for (Row row : ds) { ... }

// ✅ 正确写法 - 在事务内完成数据处理
List<String> names = new ArrayList<>();
try (TXHandle h = TX.required("query")) {
    try {
        try (DataSet ds = QueryServiceHelper.queryDataSet("q", "entity", "id,name", null)) {
            for (Row row : ds) {
                names.add(row.getString("name"));
            }
        }
    } catch (Throwable e) {
        h.markRollback();
        throw e;
    }
}
```

---

### 2. 跨库写操作
**检查点**:
- 一个事务内是否写了两个或以上的物理库？
- 是否在同一事务中操作了不同微服务节点的数据？

**风险**: 平台禁止跨库写，DB 检测到会直接抛异常；跨节点事务同样被禁止

**修正方案**:
```java
// ❌ 错误写法 - 一个事务内写两个库
try (TXHandle h = TX.required("crossDb")) {
    try {
        SaveServiceHelper.save(entityA); // 库A
        SaveServiceHelper.save(entityB); // 库B - 跨库写！
    } catch (Throwable e) {
        h.markRollback();
        throw e;
    }
}

// ✅ 正确写法 - 使用 MQ 异步解耦
try (TXHandle h = TX.required("saveA")) {
    try {
        SaveServiceHelper.save(entityA);
        // 发送 MQ 消息，由消费者在另一个事务中写库B
        publisher.publish(message);
    } catch (Throwable e) {
        h.markRollback();
        throw e;
    }
}
```

---

### 3. 事务中未 markRollback
**检查点**:
- `TX.required` / `TX.requiresNew` 事务块的 catch 中是否调用了 `h.markRollback()`？
- TXHandle 是否使用了 try-with-resources？

**风险**: 业务异常未标记回滚，事务可能被错误提交，导致数据不一致

**修正方案**:
```java
// ❌ 错误写法 - 未标记回滚
try (TXHandle h = TX.required("save")) {
    try {
        doSomething();
    } catch (Exception e) {
        logger.error("失败", e);
        // 缺少 h.markRollback()，事务可能被提交！
    }
}

// ✅ 正确写法
try (TXHandle h = TX.required("save")) {
    try {
        doSomething();
    } catch (Throwable e) {
        h.markRollback();
        throw e;
    }
}
```

---

## 🟠 P1 级问题

### 4. QFilter 条件缺失导致全表扫描
**检查点**:
- `QueryServiceHelper.queryDataSet()` / `BusinessDataServiceHelper.load()` 的 filters 参数是否为 null 或空数组？
- 查询是否缺少组织、日期等基本过滤条件？

**风险**: 无过滤条件查询导致全表扫描，大数据量时严重影响性能

**修正方案**:
```java
// ❌ 错误写法 - 无过滤条件
DynamicObject[] all = BusinessDataServiceHelper.load("entity", "id,name", null);

// ✅ 正确写法 - 添加过滤条件
QFilter orgFilter = new QFilter("org", QCP.equals, orgId);
QFilter statusFilter = new QFilter("billstatus", QCP.equals, "C");
DynamicObject[] data = BusinessDataServiceHelper.load(
    "entity", "id,name",
    new QFilter[]{orgFilter.and(statusFilter)});
```

---

### 5. QFilter 字段层级过深
**检查点**:
- QFilter 或 selectFields 中的字段层级是否超过 4 层（如 `a.b.c.d.e`）？

**风险**: 多层级关联查询会产生笛卡尔积，数据量指数级增长，导致 OOM 或超时

**修正方案**:
```java
// ❌ 错误写法 - 字段层级过深
QFilter filter = new QFilter("entry.material.group.parent.name", QCP.equals, "xxx");

// ✅ 正确写法 - 拆分为多次查询
// 第一步：查询物料分组
DynamicObject group = QueryServiceHelper.queryOne(
    "bd_materialgroup", "id", new QFilter[]{new QFilter("parent.name", QCP.equals, "xxx")});
// 第二步：用分组ID查询
QFilter filter = new QFilter("entry.material.group", QCP.equals, group.getLong("id"));
```

---

### 6. 数据访问方法选择不当
**检查点**:
- 只需判断数据是否存在时，是否使用了 `loadSingle` 或 `load`？
- 只需读取少量字段时，是否使用了 `loadSingle`（加载完整数据包）？
- 需要层级结构数据时，是否使用了 `queryOne`（返回拉平结构）？

**方法选择指南**:
| 场景 | 推荐方法 | 不推荐 |
|------|---------|--------|
| 判断是否存在 | `QueryServiceHelper.queryOne` 查 id | `loadSingle` |
| 读取少量字段 | `QueryServiceHelper.queryOne/queryDataSet` | `loadSingle` |
| 读取完整数据包（含层级） | `BusinessDataServiceHelper.loadSingle` | `queryOne` |
| 大数据量流式处理 | `QueryServiceHelper.queryDataSet` | `load` |
| 批量加载 | `BusinessDataServiceHelper.load` | 循环 `loadSingle` |
| 利用缓存 | `BusinessDataServiceHelper.loadSingleFromCache` | `loadSingle` |

---

### 7. 大事务风险
**检查点**:
- 事务块内是否包含了大量数据操作或耗时逻辑？
- 事务块内是否调用了外部 HTTP 接口或微服务？

**风险**: 大事务执行效率低，可能造成长时间锁等待、锁冲突，进而发生阻塞或死锁

**修正方案**:
```java
// ❌ 错误写法 - 大事务包含外部调用
try (TXHandle h = TX.required("bigTx")) {
    try {
        SaveServiceHelper.save(entities);
        httpClient.callExternalApi(); // 外部调用在事务内！
        updateRelatedData();
    } catch (Throwable e) {
        h.markRollback();
        throw e;
    }
}

// ✅ 正确写法 - 拆分事务，外部调用放在事务外
try (TXHandle h = TX.required("saveTx")) {
    try {
        SaveServiceHelper.save(entities);
    } catch (Throwable e) {
        h.markRollback();
        throw e;
    }
}
// 事务外调用外部接口
httpClient.callExternalApi();
```

---

### 8. QFilter 操作符使用错误
**检查点**:
- 多值匹配是否使用了 `QCP.equals` 而非 `QCP.in`？
- 模糊查询是否使用了 `QCP.equals` 而非 `QCP.like`？
- 空值判断是否使用了 `QCP.equals` null 而非 `QCP.is_null`？

**修正方案**:
```java
// ❌ 错误写法
new QFilter("status", QCP.equals, Arrays.asList("A", "B")); // 应用 in
new QFilter("name", QCP.equals, null); // 应用 is_null

// ✅ 正确写法
new QFilter("status", QCP.in, new String[]{"A", "B"});
new QFilter("name", QCP.is_null, null);
new QFilter("name", QCP.like, "%关键字%");
```

---

## 🟡 P2 级问题

### 9. queryDataSet 的 algoKey 无意义
**检查点**:
- `QueryServiceHelper.queryDataSet()` 的第一个参数 algoKey 是否使用了有意义的标识？

**风险**: 无意义的 algoKey 不利于后续性能分析和问题定位

**修正方案**:
```java
// ❌ 错误写法
QueryServiceHelper.queryDataSet("q", "entity", "id", null);
QueryServiceHelper.queryDataSet("test", "entity", "id", null);

// ✅ 正确写法 - 使用有意义的标识
QueryServiceHelper.queryDataSet("scmc.ccm.queryMaterial", "bd_material", "id,name", filters);
```

---

### 10. 事务传播类型选择不当
**检查点**:
- 需要独立事务的场景（如写日志）是否使用了 `TX.required`（会加入外层事务）？
- 只读查询是否使用了 `TX.required`（不必要的事务开销）？

**事务传播选择指南**:
| 场景 | 推荐类型 |
|------|---------|
| 业务数据保存 | `TX.required` |
| 独立写日志/审计 | `TX.requiresNew` |
| 只读查询 | `TX.notSupported` 或不开事务 |
| 子操作允许部分失败 | `TX.nested` |
