让领域模型和仓储模型各自负责

在业务系统逐渐变复杂时,一个对象往往同时被拿来承载业务规则、数据库字段和 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。自增主键、唯一约束和更新时间属于存储设计: ...

2026年9月9日 · 2 分钟 · map[name:Jeanphilo]

JSON 不只是键值对:Python 列表如何映射到 SQLAlchemy JSON 列

JSON 不只有键值对,还可以表示数组、字符串、数字和布尔值。本文用关键词订阅示例解释 Python tuple、list、dict 与 SQLAlchemy JSON 列之间的映射关系。

2026年9月8日 · 3 分钟 · map[name:Jeanphilo]

Active Record 的限制与缺陷:为什么它不适合复杂领域

副标题 / 摘要 Active Record 让开发变快,但在复杂领域模型中常会失控。本文解释其限制与替代思路。 目标读者 使用 ORM 的后端工程师 设计领域模型的团队 关注架构演进的技术负责人 背景 / 动机 Active Record 把数据与行为放在同一个类中,适合简单 CRUD。 当业务复杂时,领域逻辑会被持久化细节污染。 核心概念 Active Record:模型自带持久化行为 领域模型污染:业务逻辑与数据访问耦合 事务边界:难以清晰控制 实践指南 / 步骤 识别业务复杂度与规则数量 评估是否需要明确的领域层 复杂场景考虑 Data Mapper 将持久化逻辑下沉到仓储层 可运行示例 # Active Record:模型自带保存逻辑 class User: def __init__(self, name): self.name = name def save(self): # 这里直接访问数据库 return f"save {self.name}" if __name__ == "__main__": u = User("Alice") print(u.save()) 解释与原理 Active Record 的优点是简单直接,但会让业务逻辑与持久化高度耦合。 当规则变复杂时,测试与演进成本显著上升。 常见问题与注意事项 Active Record 真的不适合大型系统吗? 并非绝对,但复杂业务会更难维护。 是否必须迁移到 Data Mapper? 只有在复杂规则与多聚合情况下才建议。 ...

2026年1月24日 · 1 分钟 · map[name:Jeanphilo]

Active Record vs Data Mapper:差异、优缺点与选型

副标题 / 摘要 Active Record 把数据与持久化绑定在一起,Data Mapper 把持久化隔离为独立层。本文对比二者并给出选型建议。 目标读者 使用 ORM 的后端工程师 设计领域模型的开发者 需要做架构取舍的团队 背景 / 动机 项目变复杂时,持久化模型往往开始“侵入”业务逻辑。 理解 Active Record 与 Data Mapper 的差异,是避免架构污染的关键。 核心概念 Active Record:对象自己保存/加载(数据与持久化耦合) Data Mapper:持久化逻辑在独立映射层 领域模型纯度:业务模型是否被 ORM 污染 实践指南 / 步骤 小型项目可用 Active Record 复杂领域建议 Data Mapper 明确领域边界,避免 ORM 侵入 用 Repository 隔离持久化 测试业务逻辑时替换存储层 可运行示例 # Active Record 风格 class UserAR: def __init__(self, name): self.name = name def save(self): print("save", self.name) # Data Mapper 风格 class User: def __init__(self, name): self.name = name class UserMapper: def save(self, user: User): print("save", user.name) if __name__ == "__main__": UserAR("Alice").save() UserMapper().save(User("Bob")) 解释与原理 Active Record 简单直观,但把持久化耦合进领域模型。 Data Mapper 更复杂,但让业务逻辑更纯粹、更易测试。 ...

2026年1月24日 · 1 分钟 · map[name:Jeanphilo]

什么是 N+1 查询问题:成因、检测与优化

副标题 / 摘要 N+1 问题是 ORM 中最常见的性能陷阱。本文解释其成因,并给出可落地的优化方法。 目标读者 使用 ORM 的后端工程师 关注性能与成本的开发者 需要优化数据库访问的团队 背景 / 动机 N+1 问题会让一次请求变成大量 SQL,导致响应变慢和数据库压力暴涨。 它常在数据量增大后才暴露,因此必须提前识别。 核心概念 N+1 查询:先查 N 条主记录,再为每条查询子记录 延迟加载:触发隐式查询 预加载:一次性拿到相关数据 实践指南 / 步骤 在日志中统计 SQL 数量 识别循环内的 ORM 查询 用 JOIN / 预加载替代逐条查询 使用批量查询 引入性能基准测试 可运行示例 # 伪代码:展示 N+1 模式 users = db.query("SELECT * FROM users") for u in users: orders = db.query("SELECT * FROM orders WHERE user_id = ?", u.id) 优化: SELECT u.*, o.* FROM users u LEFT JOIN orders o ON o.user_id = u.id; 解释与原理 N+1 的本质是“隐式循环查询”。 ORM 的延迟加载机制在循环中触发,导致查询数量指数级增长。 ...

2026年1月24日 · 1 分钟 · map[name:Jeanphilo]

延迟加载(Lazy Loading)是什么:适用场景与代价

副标题 / 摘要 延迟加载可以减少初始加载成本,但也可能引发 N+1 和不可预期的查询。本文给出适用场景与风险。 目标读者 使用 ORM 的后端工程师 需要优化查询策略的开发者 关注性能与成本的团队 背景 / 动机 延迟加载让你“用到才查”,在数据量大时可以避免一次性加载过多。 但如果使用不当,会触发 N+1 查询和隐式性能问题。 核心概念 Lazy Loading:访问关联数据时才触发查询 Eager Loading:一次性加载相关数据 隐式查询:调用属性触发 SQL 实践指南 / 步骤 默认使用延迟加载,但关键路径要显式控制 在批量访问前使用预加载 监控 SQL 数量与慢查询 避免在循环中触发懒加载 可运行示例 # 伪代码:访问属性触发 SQL user = session.query(User).first() orders = user.orders # 这里触发查询 解释与原理 延迟加载把查询时机推迟到真正访问数据时。 如果访问发生在循环内,就可能触发 N+1。 常见问题与注意事项 延迟加载一定更快吗? 不一定,频繁访问时反而更慢。 可以完全禁用延迟加载吗? 可以,但会增加初始化成本。 如何判断是否该用? 看访问路径与数据规模。 最佳实践与建议 对高频接口禁用隐式懒加载 明确预加载策略 使用 ORM 的加载策略配置 小结 / 结论 延迟加载是性能优化工具,但必须与访问模式匹配。 不加控制的懒加载会造成严重性能问题。 参考与延伸阅读 SQLAlchemy / Hibernate Loading Strategies ORM 性能优化实践 元信息 阅读时长:7~9 分钟 标签:延迟加载、ORM、性能 SEO 关键词:Lazy Loading, ORM 元描述:解释延迟加载的适用场景与缺陷。 行动号召(CTA) 找一个慢接口,检查是否存在隐式懒加载,再考虑预加载优化。

2026年1月24日 · 1 分钟 · map[name:Jeanphilo]

什么是 O/R 阻抗失衡:对象世界与关系模型的冲突

副标题 / 摘要 O/R 阻抗失衡指对象模型与关系模型的结构和语义不一致,导致映射复杂、性能问题和维护成本上升。本文给出可落地的缓解策略。 目标读者 使用 ORM 的后端工程师 负责数据建模与性能优化的开发者 想理解“为什么 ORM 不是银弹”的团队负责人 背景 / 动机 对象世界是图结构(引用、继承、聚合),关系世界是表结构(行、列、外键)。 两者语义不同,映射时必然损失与扭曲,这就是 O/R 阻抗失衡。 核心概念 对象图:一对多、多对多关系 关系模型:表与外键,依赖 JOIN 映射成本:查询复杂、N+1、延迟加载等 实践指南 / 步骤 先设计数据访问模式,再设计模型结构 为读与写设计不同模型(CQRS 思路) 控制对象图深度,避免自动级联查询 使用 DTO 作为边界,减少 ORM 泄漏 对关键路径手写 SQL 可运行示例 下面示例展示对象与关系的差异: import sqlite3 conn = sqlite3.connect(":memory:") cur = conn.cursor() cur.execute("CREATE TABLE user(id INTEGER PRIMARY KEY, name TEXT)") cur.execute("CREATE TABLE orders(id INTEGER PRIMARY KEY, user_id INTEGER, amount INTEGER)") cur.execute("INSERT INTO user VALUES (1, 'Alice')") cur.executemany("INSERT INTO orders VALUES (?, ?, ?)", [(1, 1, 100), (2, 1, 200)]) cur.execute("SELECT u.name, o.amount FROM user u JOIN orders o ON u.id = o.user_id") print(cur.fetchall()) 解释与原理 对象模型喜欢“引用”和“聚合”,而关系模型喜欢“表”和“JOIN”。 ORM 需要在两种语义之间做折中,这就产生了性能与复杂度问题。 ...

2026年1月24日 · 1 分钟 · map[name:Jeanphilo]