# 聚合与仓储 DSL 扩展规划（Aggregate / Repository）

> 状态：**规划中（未实现）**
> 关联代码：`dsl/src/db.ts`（TableSchema）、`dsl/src/dao.ts`（DaoSchema）、`dsl/src/service.ts`（ServiceSchema）
> 背景对话：ts-libs 会话「dd DDD 扩展讨论」（商城下单用例）

## 1. 背景：pylon 现状与 DDD 缺口

**现状**：

- `TableSchema` 是全局扁平数据字典（"表无物理归属"），orders / order_items 是两张互相独立声明、无聚合关系的表；
- `DaoSchema` 是单表单 SQL（knex 透明代理），粒度 = 表，无跨表能力；
- 多表读写由 service flow 手工编排：`dao.insert(orders) → dao.insert(order_items)` + 手工 `@Trans()`，"总额 = Σ明细"这类一致性靠开发者自觉，无强制约束。

**DDD 缺口对照**（战术模式）：

| DDD 概念 | dsl 现状 | 缺口 |
|---------|---------|------|
| 聚合 / 聚合根 | 无 | **全新概念** |
| 值对象 | mock 识别"金额/手机号"语义 | 无声明层 |
| 领域事件 | 只有 UI 组件事件（event.ts） | **全新概念** |
| 领域服务 | 全混在应用服务（service_schema） | 需拆分 |
| 仓储 | 无（DAO 粒度是表） | **全新概念** |
| 应用服务编排 | service_schema + flow ✅ | 已具备 |
| 防腐层 | ThirdServiceSchema 只隔离 | 缺模型映射 |

## 2. 什么情况下需要聚合？

**核心判断标准**：

> 创建 / 保存 / 删除时，是否必须一起做？
> - 是 → 聚合（成员）
> - 否 → 不聚合（引用）

### 2.1 成员 vs 引用

| 维度 | 聚合成员 | 聚合间引用 |
|------|---------|-----------|
| 生命周期 | 与根同生共死 | 独立生命周期 |
| 创建 | 随根一起创建 | 不随根创建 |
| 保存 | 随根一起保存 | 不随根保存 |
| 删除 | 随根级联删除 | 不随根删除 |
| 关联 | 成员表 FK 指向根 / 扩展表 extends 根 | 本表 FK 指向其他聚合根 |
| 示例 | order_item / order_address 快照 | order.user_id / order.address_id |

### 2.2 案例：地址可复用 vs 地址快照

**场景**：一个订单对应一个地址，但地址可以被多个订单复用。

- 如果地址可复用 → 不是聚合成员，是引用：
  - 创建订单不会创建地址；
  - 删除订单不会删除地址；
  - 多个订单可以指向同一个地址；
  - 建模为 `references: { address: 'AddressAggregate' }`，外键不需要 unique。

- 如果地址是下单时的快照（copy）→ 是聚合成员：
  - 下单时复制一份地址到订单；
  - 之后修改地址簿不影响已下单订单；
  - 删除订单时该快照一起删除；
  - 建模为 `members: { address: orderAddress }`；
  - 此时 `orderAddress` 应为**扩展表**（`extends: order`），主键与根主键同义、类型一致，天然保证一对一。

**lint 规则**：声明为一对一成员时，成员表必须是扩展表（`extends` 指向根），主键与根主键同义且类型一致；否则它要么是一对多，要么应改为 references。

## 3. 扩展一：AggregateSchema（核心）

**作用**：显式声明"哪些表属于同一个聚合、谁是聚合根、成员如何挂载、跨成员不变式、聚合间引用规则"。这是把"多表一致性从约定变约束"的落点。

```ts
defineAggregate({
  root: ordersTable,                        // 聚合根表
  members: {                                // 成员表：数组=一对多，非数组=一对一
    items:   [orderItemsTable],             // 1:N，外键自动推导
    address: orderAddressTable,             // 1:1，扩展表
  },
  invariants: [                             // 跨成员表不变式，挂聚合上，可被生成代码消费
    { name: 'total = sum(items.price * items.qty)',
      check: 'totalAmount == sum(items.price * items.qty)' },
  ],
  references: { productId: 'ProductAggregate' },  // 聚合间只按 ID 引用
});
```

| 声明项 | 含义 | 缺了会怎样 |
|--------|------|-----------|
| `root` | 谁是聚合根 | 分不清一致性入口 |
| `members` | 包含哪几个 table schema；数组=普通表=1:N，非数组=扩展表=1:1 | 无法推导成员关系 |
| 成员外键 | 由 TableSchema.foreignKeys / extends 自动推导 | 工具无法推导 join / 级联关系 |
| `invariants` | 跨成员一致性规则 | "总额=Σ明细"又回到 flow 里手工写 |
| `references` | 聚合间只按 ID 引用 | 无法 lint 跨聚合直接持表引用 |

**消费方**（声明一旦存在即可自动推导）：

1. **生成 Repository**：按"加载 / 保存 / 删除"三套固定骨架自动产出（见下），`orders + order_items` 自动同事务，不再手工 `@Trans()`；
2. **lint 约束**：禁止聚合外代码直接 `insert/update/delete` 成员表（只准经 root 走）；
3. **不变式挂载**：save 前后强制校验。

**多表映射三种模式**（聚合↔表）：A 单表=单聚合（1:1，几乎透明）；B 一聚合=多表（1:N，最常见，Order 案例）；C 多聚合共享表（N:1，DDD 不推荐）。

## 4. 扩展二：RepositorySchema（可推导，也可显式声明）

**作用**：聚合粒度的存储入口，把"哪些表一起查、怎么拼成聚合"的知识从 service flow 下沉到仓储。调用方只面对领域概念（Order），不面对表。

```ts
defineRepository({
  name: 'OrderRepository',
  aggregate: orderAggregate,   // 绑定聚合
  // 内部如何落到 DAO 由 generator 按 aggregate 结构自动展开：
  //   save(order)     = tx { ordersDao.upsert + orderItemsDao 级联 }
  //   findByOrderNo() = ordersDao.get + orderItemsDao 按 order_no 查
});
```

**聚合内 join 下沉、聚合间禁止 join**：

- 聚合内：加载整个 Order（根 + 明细 + 地址）由 Repository 内部完成，调用方一行，join 知识由成员表 `foreignKeys` / `extends` 自动推导；
- 聚合间：只按 ID 引用，跨聚合 join 是 DDD 禁止的；展示商品名这类信息走读模型 / 查询服务（CQRS 的 Q 侧），或应用服务分步查 + 内存组装。

**分层对照**：

| 层 | 操作单元 | 一次操作覆盖 | dsl |
|----|---------|------------|-----|
| Service | 用例 | 跨多个聚合/服务 | service_schema ✅ |
| Repository | **聚合** | orders + order_items 一个事务 | **本扩展** |
| DAO | **表** | 单表一条 SQL | dao_schema ✅ |

## 5. 扩展三：引入支持 DDD 的 TS 库（选型，待决策）

dsl 是**声明期**（`defineAggregate` 声明结构），引入的库是**运行期**（代码跑起来时持久化/发事件）。两者互补，不冲突——声明可翻译为运行期库的配置。

| 库 | 类型 | 聚合能力 | 与本扩展关系 |
|----|------|---------|-------------|
| **MikroORM** | ORM | Entity / Repository / Unit of Work / Identity Map / cascade persist | **首选参考**：`@OneToMany(cascade, orphanRemoval)` + `em.persist(order)` 就是"orders + order_items 同事务整体落库"的标准实现；AggregateSchema 声明可翻译成它的映射配置 |
| **Remesh** | DDD 框架 | CQRS + 领域事件 + Command/Query 分离 | 领域事件 / CQRS 参考 |
| **Emmett** | 事件溯源 | 聚合状态由事件重建 | 事件溯源聚合参考 |
| TypeORM | ORM | Repository + cascade，无 UoW / Identity Map | 弱支持，不推荐 |
| Prisma / Drizzle | 查询构建器 | 不支持聚合 | 需手工包 Repository |

**建议**：运行时持久化参考/选用 **MikroORM**（聚合持久化设计最完整）；领域事件参考 **Remesh**。dsl 的 `AggregateSchema` 声明层本身无现成开源，属本项目的增量设计空间。

## 6. 落地步骤（待办，未开工）

1. `AggregateSchema` 类型 + `defineAggregate` + 定义期校验（root 必须在其表内、成员表必须有外键指向 root 或 extends root、成员表不能是其他聚合的 root 等）；
2. `RepositorySchema` 类型 + `defineRepository`（或从 aggregate 自动推导生成）；
3. `gen` 生成 Repository 代码：加载/保存/删除三套骨架 + 同事务包装（`@Trans()` 从声明推导）；
4. `lint` 聚合边界检查：聚合外禁止直接改成员表、聚合间禁止跨表引用；
5. 运行期库选型落地（MikroORM 或保持 DAO 同事务包装）。
