beanie 不干预文档模型设计,仅映射 python 类到 mongodb;模型优劣取决于业务读写模式与 mongodb 物理限制。内嵌适用于小规模 1:1 或 1:n,大数组或 n:n 必须引用;字段需显式声明可空性与默认值;索引须手动声明并调用 create_indexes 创建;字段变更需兼容处理,内嵌更新慎用全量 $set。

直接说结论:Beanie 本身不干预文档模型设计,它只是把 Python 类映射成 MongoDB 文档——真正决定模型好坏的,是业务读写模式和 MongoDB 的物理限制,不是 Beanie 的语法。
内嵌 vs 引用:先看数据增长规律,再选语法
MongoDB 文档有 16MB 上限,数组无限增长会拖慢查询、增加内存压力。Beanie 的 Document 类支持字段嵌套,但不会自动帮你判断该不该嵌套。
- 1:1 或小规模 1:N(比如用户 + 头像 URL、订单 + 支付状态),直接用
class Address(Document)内嵌进User,字段类型写成address: Address或addresses: List[Address] - N:N 或大数组(如用户关注列表超 5000 条、日志条目持续追加),必须拆成独立集合 +
Link或 ObjectId 引用,否则find()会加载整块巨文档 - Beanie 的
Link类型只做类型提示,不自动 fetch 关联数据;真要 join,得用$lookup聚合,且目标集合不能是分片表
字段定义:别依赖 Python 类型推断,显式声明可空性与默认值
Beanie 基于 Pydantic v2,Optional[str] 和 str | None 都合法,但 MongoDB 没有“空字符串”和“null”的语义区分,容易在查询时漏掉数据。
- 必填字段写成
name: str,Beanie 插入时校验,MongoDB 存为{"name": "alice"} - 可空字段明确用
name: str | None = None,否则字段缺失时会报ValidationError,而不是存成null - 数组默认值别写
tags: List[str] = []—— 这是类属性共享,所有实例共用一个 list 对象;正确写法是tags: List[str] = Field(default_factory=list)
索引与性能:Beanie 不自动生成索引,得手动声明
没索引的 find_one(name == "xxx") 是全表扫描,哪怕文档只有 100 条,在高并发下也会卡住。Beanie 的 Document 类提供 __indexes__ 声明机制,但不保证创建。
- 单字段索引:
__indexes__ = ["name"] - 复合索引:
__indexes__ = [[("user_id", 1), ("created_at", -1)]] - 唯一索引:
__indexes__ = [IndexModel([("email", 1)], unique=True)] - 索引需在应用启动时调用
await Document.get_motor_collection().create_indexes(...)才生效,Beanie 不自动执行
版本与演进:字段删了,旧文档还在,别指望 Beanie 自动清理
MongoDB 是 schema-less 的,Beanie 的模型只是读写时的校验层。字段从模型里删掉,不影响已有文档存储,但下次 find() 会因缺失字段抛 ValidationError。
- 加字段:设默认值或允许
None,旧文档能正常加载 - 改字段名:用
Field(alias="old_field_name")兼容过渡期 - 删字段:先确保所有业务代码不再读写该字段,再用
update_many({}, {"$unset": {"old_field": ""}})清理,别跳过这步
最常被忽略的点:内嵌文档修改后,Beanie 默认用 $set 全量更新整个字段,而不是只更新子字段;如果只改 user.profile.phone,却触发整个 profile 对象重写,可能覆盖并发写入的其他子字段。这时候得手写 update_one({"_id": ...}, {"$set": {"profile.phone": "123"}}),绕过 Beanie 的 ORM 封装。











