MongoDB:处理自增模型ID而不是Mongo的本地ObjectID

9
由于管理决策,我们现在将userId用于用户集合,postId用于帖子集合,topicId用于主题集合,而不是使用'_id'作为每个集合的唯一标识符。这会导致一些刚开始时的问题-我遇到的一个问题是upserts——使用Mongoose,我们有一个模式,限制了userId必须是唯一的值-但是当对一个用户模型进行更新,并将upsert设置为true时,MongoDB似乎只查看集合的ObjectIds来查看是否存在相同的对象,而不是检查是否已经存在具有相同userId的模型-因此Mongo会执行插入操作而不是更新操作。
让我用一些数据说明一下:
假设用户集合只有一个文档:
{
_id:'561b0fad638e99481ab6d84a'
userId:3,
name:'foo'
}

我们接下来运行:
User.update({userId:3},{"$set":{name:'bar'},{upsert:true},function(err,resp){

if(err){

   // "errMessage": "insertDocument :: caused by :: 11000 E11000 duplicate key error index: app42153482.users.$userId_1  dup key: { : 3 }",
}

});

有人可能会认为MongoDB会找到userId为3的现有文档并更新它,所以既然出现了重复键错误,我肯定做错了什么?


是的,我也觉得很奇怪,肯定是我做错了什么,但我不知道是什么。 - Alexander Mills
是的,具有userId:3的对象在那里 - 如果我删除它,则第一个更新将起作用并变为插入,但相同的更新将尝试执行另一个插入,因此会出现重复键错误。 - Alexander Mills
1
那个解决方法可行,但我想找出为什么原始问题会发生。 - Alexander Mills
你添加唯一约束后,是否已重新启动并重新索引了表格?数据中该字段是否已存在重复项? - Mike Brant
好的,我无法在shell上重现这个问题,这导致我得出结论,这不是MongoDB本身的问题。这又意味着我们可能有一个实现问题。由于您的代码似乎是正确的,我们很可能在node驱动程序本身或mongoose中有一个实现问题。我已经查看了两个票务系统,没有找到任何与您的问题匹配的票务。我认为我们需要深入挖掘一下。我们是在谈论分片集合吗? - Markus W Mahlberg
显示剩余9条评论
3个回答

6
通常默认值ObjectId更适合_id。在这种情况下,您可以覆盖默认_id,或者您可以拥有自己的id字段(例如,在您的情况下为userId)。
使用单独的计数器集合来跟踪上次使用的数字序列。_id字段包含序列名称,seq字段包含序列的最后一个值。
将初始值插入到计数器集合中,以用于userid:
db.counters.insert(    {
      _id: "userid",
      seq: 0    } )

创建一个名为 getNextSequence 的函数,该函数接受一个序列的名称。该函数使用 findAndModify() 方法来原子地递增 seq 值并返回这个新值:
function getNextSequence(name) {
   var ret = db.counters.findAndModify(
          {
            query: { _id: name },
            update: { $inc: { seq: 1 } },
            new: true
          }
   );

   return ret.seq;
}

insert() 中使用此 getNextSequence() 函数。

db.users.insert(
   {
     _id: getNextSequence("userid"),
     name: "Sarah C."
   }
)

db.users.insert(
   {
     _id: getNextSequence("userid"),
     name: "Bob D."
   }
)

这样,您可以在同一计数器集合中维护尽可能多的序列。有关upsert问题,请查看此链接创建自动递增序列字段中的乐观循环块。
第二种方法是使用mongoose中间件,例如mongodb-autoincrement
希望能对您有所帮助。

请问一下,您编写的getNextSequence()函数是在mongodb中还是在您的服务器上实现的? - Sudhanshu Gaur
@Sudhansu,你可以在服务器级别上创建它作为数据库实用函数。 - Rudra

5
我不知道你使用的是哪个版本的MongoDB和Mongoose,但我在MongoDB 3.0和Mongoose 4.1.10上无法重现你的问题。
我为您制作了一个示例,将创建并保存新用户,更新(使用upsert),并通过upsert创建另一个用户。尝试运行此代码:
"use strict";
var mongoose=require("mongoose");
var Schema = require('mongoose').Schema;
var ObjectId = mongoose.Schema.Types.ObjectId;

// Connect to test
mongoose.connect("mongodb://localhost:27017/test");

// Lets create your schema
var userSchema = new Schema({
    _id: ObjectId,
    userId: {type: Number, unique: true },
    name: String
});

var User = mongoose.model("User", userSchema, "Users");


User.remove() // Let's prune our collection to start clean
    .then( function() {
        // Create our sample record
        var myUser = new User({
            _id:'561b0fad638e99481ab6d84a',
            userId:3,
            name:'foo'
        });

        return myUser.save();
    })
    .then( function() {
        // Now its time to update (upsert userId 3)
        return User.update({userId:3},{"$set":{name:'bar'}},{upsert:true});
    })
    .then( function() {
        // Now its time to insert (upsert userId 4)
        return User.update({userId:4},{"$set":{name:'bee'}},{upsert:true});
    })
    .then( function() {
        // Lets show what we have inserted
        return User.find().then(function(data) {console.log(data)});
    })
    .catch( function(err) {
        // Show errors if anything goes wrong
        console.error("ERROR", err);
    })
    .then( function() {
        mongoose.disconnect();
    });

0

根据MongoDB 3.0的文档,upsert:true只有在查询条件匹配_id字段时才不会插入不存在的文档。

请参见:https://docs.mongodb.org/manual/reference/method/db.collection.update/#mongodb30-upsert-id

为什么不使用用户名称作为唯一标识符?

因为在MongoDB环境中,自动递增字段作为ID是一个不好的实践,特别是如果您想要使用分片 => 所有插入都将发生在最新的分片上 => MongoDB集群将经常重新平衡/重新分配数据。 (当前在您的系统上不会发生这种情况,因为您仍在使用生成的_id字段)

当然,您也可以在user_id字段上创建唯一索引: https://docs.mongodb.org/manual/core/index-unique/#index-type-unique


我不明白为什么使用数字递增设置用户ID对于分片是个问题。我认为创建用户配置文件只会发生一次,而如何设置用户ID完全未知于分片系统? - mskw

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接