在 Mongoose 中,_id
和 id
是与文档标识符(identifier)相关的两个字段,但它们有一些关键区别和特定的用途:
_id
字段:- MongoDB 内部字段:
_id
是 MongoDB 内部默认的主键字段,每个文档都有一个唯一的_id
字段,默认类型为 ObjectId。 - 默认生成:如果在插入文档时没有指定
_id
,MongoDB 会自动生成一个唯一的 ObjectId 作为_id
。 - 数据库层次:
_id
是实际存储在数据库中的字段,并且在查询、索引等操作中使用。
- MongoDB 内部字段:
id
字段:- 虚拟字段:
id
是 Mongoose 提供的虚拟字段,它是将_id
字段的值转换为字符串类型。默认情况下,Mongoose 会为每个文档创建一个id
虚拟字段,等同于this._id.toString()
。 - JavaScript 层次:
id
字段不是实际存储在数据库中的字段,而是在应用层面上通过 Mongoose 的模式(schema)提供的便利字段,便于使用。 - 简化操作:使用
id
字段可以避免直接处理 ObjectId 类型,特别是在需要将文档标识符传递到前端或进行 JSON 序列化时非常方便。
- 虚拟字段:
说白了,id 是_id 的一个计算属性,为了简化操作。
刚开始会觉得这个功能很便捷,并且mongoose做了很多自动转换,很多地方不需要区分他们俩,觉得很便捷。
但问题也随之而来,首先是代码中有的地方用id,有的用_id, 看起来很混乱;其次对于前端来讲,返回的_id 和id 都是字符串,没有区别,相似又冗余;再然后是,在服务端,对于一些嵌套doc , 就只有_id , 没有id。
对于findOne({_id}),严格上讲,要传入ObjectId 才能查询,但mongoose 做了自动转换,一般我们传入字符串就行。但在 Mongoose 的 aggregate
操作中,字符串形式的 id
不会自动转换为 ObjectId
。经常导致一些隐藏的问题,并且这个地方传错了也不会报错,只是查询结果不对。例如我今天遇到的一个bug.
Example.aggregate([ { $match: { _id: {$nin:[ObjectId('xxx'),'zzz']} } }, { $project: { name: 1 } } ])
后面的’zzz’ 因为是字符串id, 实际上不生效,但也不报错,最终结果不对,十分隐蔽的bug.
所以最后,虽然mongoose 做了很多兼容来简化_id 的操作,一开始大家觉得不用区分了,但踩到一些坑之后才明白,最好严格区分id 的类型和不同常见的用法,不要含糊使用。