让领域模型和仓储模型各自负责
在业务系统逐渐变复杂时,一个对象往往同时被拿来承载业务规则、数据库字段和 HTTP 响应。开始时这很省事,后来却会出现连锁问题:数据库新增一个内部字段,接口结构跟着变化;领域代码为了查询数据而依赖 ORM;仓储把 SQLAlchemy 行对象直接交给控制器。 更稳定的做法,是让不同边界拥有不同模型,并通过转换连接它们: Controller -> Domain Model -> Repository Contract -> ORM Model -> Database | ^ +------ API Request/Response ---+ 这里的重点不是增加更多目录,而是明确每个模型对谁负责。 四类模型,各自回答一个问题 领域模型回答“什么状态符合业务规则”。例如商品必须有 SKU 和名称,价格不能为负数。SKU 是业务标识,数据库自增 id 则不必进入领域对象。领域模型不应该导入 SQLAlchemy、Pydantic 或 Web 框架。 ORM 模型回答“如何把数据存进数据库”。它可以拥有自增主键、外键、索引、时间戳和数据库约束,但这些细节不必进入领域对象。 Repository 契约回答“应用可以怎样保存和读取数据”。它的参数和返回值属于仓储边界:保存商品可以返回 StoredProduct,其中包含领域对象和 updated_at;分页查询可以返回 ProductPage 这样的查询模型。 API 模型回答“客户端能发送和接收什么”。请求模型负责输入形状,响应模型负责 HTTP 输出。例如商品响应可以包含 updated_at,并将其序列化为日期字符串;这不意味着商品领域对象也需要这个字段。 一个最小例子 假设一个后台允许按 SKU 保存商品,再查询商品信息。下面使用同步 SQLAlchemy 2.x 展示关键代码,省略数据库连接和 HTTP 路由配置。 领域对象放在 app/domain/product.py,只表达业务状态。价格统一使用人民币分,避免浮点数表示金额: from dataclasses import dataclass @dataclass(frozen=True) class Product: sku: str name: str price_cents: int def __post_init__(self): if not self.sku or not self.name: raise ValueError("商品 SKU 和名称不能为空") if self.price_cents < 0: raise ValueError("商品价格不能为负数") 数据库模型放在 infra/db/models.py。自增主键、唯一约束和更新时间属于存储设计: ...