MongoDB 双 ID 体系:为什么同时保留 _id 与自增 id
后端工程31 阅读约 6 分钟
在我的博客后端里,几乎每个核心模型(文章、分类、标签、作者)都同时拥有两个标识:MongoDB 自带的 _id(ObjectId),以及一个从 1 开始自增的数字 id。第一次看到的人都会问:这不是冗余吗?
不是。这是个有意的设计,我把它叫做「双 ID 体系」。
两种 ID 各自的问题
ObjectId(_id) 是 MongoDB 的默认主键,全局唯一、分布式友好、自带时间戳。但它有两个使用上的痛点:
- 它是 24 位十六进制字符串,丑且长,放进 URL 里像
/article/65f1a2b3c4d5e6f7a8b9c0d1,对人和 SEO 都不友好; - 它不可读、不可排序成「第几篇」,对外暴露还会泄露一点写入时间信息。
纯自增数字 id 则相反:/article/42 简洁、可读、对 SEO 友好,但它不是 MongoDB 原生能力,需要插件维护一个计数器。
我的选择是:两个都留,各司其职。
实现:Typegoose + auto-increment 插件
借助 @typegoose/auto-increment,给模型挂一个自增字段即可:
@plugin(AutoIncrementID, { field: 'id', startAt: 1 })
@modelOptions({
schemaOptions: {
collection: 'articles',
timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' },
versionKey: false,
},
})
export class Article {
@prop({ unique: true, index: true })
id?: number
@prop({ required: true, trim: true, maxlength: 64 })
title: string
// ...
}
_id 仍由 MongoDB 自动生成、做物理主键;id 是带唯一索引的业务主键,对外的一切引用都用它。
关键:所有关联都用数字 id
这套体系真正的价值在「关联」上。我没有用 Mongoose 的 ObjectId ref + populate,而是让文章用数字 id 数组引用分类、标签、作者:
@prop({ default: null, index: true })
category_id: number | null
@prop({ type: () => [Number], default: [], index: true })
tag_ids: number[]
查询时手动做一次「批量取 + 内联」,避免 N+1:
const categoryIds = [...new Set(articles.map((a) => a.category_id).filter(Boolean))]
const tagIds = [...new Set(articles.flatMap((a) => a.tag_ids ?? []))]
const [categories, tags] = await Promise.all([
this.categoryModel.find({ id: { $in: categoryIds } }).lean().exec(),
this.tagModel.find({ id: { $in: tagIds } }).lean().exec(),
])
这样做的好处:
- 外部引用稳定且可读:前端、AI 知识库、其他服务引用文章时用
42而不是一串 ObjectId; - 解耦存储细节:万一哪天换底层存储,业务主键
id的语义不变; - 可控的关联展开:用
$in一次性取齐再Map内联,性能和行为都在自己手里,不依赖populate的隐式行为。
兼容历史数据的额外收益
我这套后端是从旧 Koa 版本迁移来的,旧库里的文章本来就用数字 id 互相引用。保留自增 id 让我可以无损迁移:旧文章的 id、它们之间的分类/标签关系原样保留,前端的文章链接(/article/123)也不会全部失效。如果当初只用 ObjectId,迁移就得重建所有引用、所有外链全挂。
代价与注意点
- 自增 id 依赖一个计数器文档,高并发写入下要注意它不是「无锁」的;个人博客写入量极低,完全无所谓。
- 要约束纪律:对内可以用
_id,对外一律用id。一旦混用,又回到丑 URL 的老路。
小结
_id 负责「机器视角」的唯一性与存储,id 负责「人 / 外部系统视角」的可读引用。一点点冗余,换来 URL 友好、引用稳定、迁移无损——对一个要长期维护、还要被外部(AI 服务、前端、SEO)大量引用的内容系统,这笔账很值。
相关文章
评论 (1)
hellojs
NestJS 重写的架构取舍讲得很坦诚,受用





