MongoDB/Mongoose:如果不为空则唯一

160
我想知道是否有一种方法可以强制使唯一集合条目 但仅当条目不为空时
示例模式:
var UsersSchema = new Schema({
    name  : {type: String, trim: true, index: true, required: true},
    email : {type: String, trim: true, index: true, unique: true}
});

在这种情况下,“email”不是必需的,但如果保存了“email”,我希望确保此条目在数据库级别上是唯一的。

空条目似乎会获得值“null”,因此每个没有电子邮件的条目都会与“unique”选项崩溃(如果有一个不同的用户没有电子邮件)。

现在,我正在应用程序层面上解决它,但很想保存那个数据库查询。

谢谢

4个回答

242

从MongoDB v1.8+开始,您可以通过在定义索引时将 sparse选项设置为true来获得确保唯一值但允许多个文档不包含该字段的所需行为。示例如下:

email : {type: String, trim: true, index: true, unique: true, sparse: true}

或者在 shell 中执行:

db.users.ensureIndex({email: 1}, {unique: true, sparse: true});
请注意,即使使用唯一的稀疏索引,仍然不允许多个具有空值email字段的文档,只能允许多个没有email字段的文档。
请参见http://docs.mongodb.org/manual/core/index-sparse/

28
太棒了!对于像我这样的新手来说,这绝对是最好的答案!注意:如果只是在模式中添加 sparse: true,Mongoose 不会更新您的唯一索引为稀疏索引。您必须删除并重新添加该索引。不知道这是否是预期行为还是错误。 - Adam A
10
注意:如果数据库中已存在该索引,则不会被替换。 - damphat
1
我认为这个回答并不正确,因为没有特定字段的几个文档与该字段上具有空值的几个文档(无法唯一索引)是不同的。 - kako-nawao
1
@kako-nawao 这是真的,它只适用于没有 email 字段的文档,而不是实际上有一个值为 null 的字段。请查看更新后的答案。 - JohnnyHK
2
缺少字段时无法工作。也许在后续版本的mongodb中更改了行为。应该更新答案。 - joniba
显示剩余4条评论

92

简短版

在MongoDB v3.2+ 中,你可以使用部分唯一索引和筛选表达式来实现多个字段为 null 或未定义时,强制实际值的唯一性。

要求:

  • 提前知道具体值类型 (例如,非 null 时始终是 stringobject)。

如果您不想了解详情,请跳至implementation部分。

详细版

补充 @Nolan's 的回答,从MongoDB v3.2开始,你可以使用带有过滤表达式的部分唯一索引。

部分过滤表达式有限制。只能包括以下内容:

  • 相等表达式(即 field: value 或使用 $eq 运算符),
  • $exists: true 表达式,
  • $gt, $gte, $lt, $lte 表达式,
  • $type 表达式,
  • $and 运算符仅在顶层可用

这意味着无法使用简单的表达式 {"yourField"{$ne: null}}

但是,假设你的字段始终使用相同的类型,你可以使用$type 表达式

{ field: { $type: <BSON type number> | <String alias> } }

MongoDB v3.6添加了对指定多种可能类型的支持,可以通过数组传递:

{ field: { $type: [ <BSON type1> , <BSON type2>, ... ] } }
这意味着当值不为null时,它允许该值是多种类型中的任何一种。
因此,如果我们想要在下面的示例中允许email字段接受stringbinary data值,适当的$type表达式将是:
{email: {$type: ["string", "binData"]}}

实现

mongoose

您可以在 mongoose 模式中指定它:

const UsersSchema = new Schema({
  name: {type: String, trim: true, index: true, required: true},
  email: {
    type: String, trim: true, index: {
      unique: true,
      partialFilterExpression: {email: {$type: "string"}}
    }
  }
});

或者直接将其添加到集合中(该集合使用本地的node.js驱动程序):

User.collection.createIndex("email", {
  unique: true,
  partialFilterExpression: {
    "email": {
      $type: "string"
    }
  }
});

原生 mongodb 驱动程序

使用 collection.createIndex 方法

db.collection('users').createIndex({
    "email": 1
  }, {
    unique: true,
    partialFilterExpression: {
      "email": {
        $type: "string"
      }
    }
  },
  function (err, results) {
    // ...
  }
);

MongoDB Shell

使用 db.collection.createIndex 方法:

db.users.createIndex({
  "email": 1
}, {
  unique: true, 
  partialFilterExpression: {
    "email": {$type: "string"}
  }
})

这将允许插入多条记录,其中包含一个null的电子邮件或没有电子邮件字段,但不允许插入具有相同电子邮件字符串的记录。


太棒了,你是救星。 - r3wt
这个答案对我也起作用了。 - Emmanuel N K
2
这个问题的大多数被接受的答案都涉及确保您没有显式地将 null 值设置为索引键,而是应该传递 undefined。我正在这样做,但仍然出现错误(在使用 uniquesparse 时)。我根据这个答案更新了我的模式,删除了现有的索引,结果非常完美。 - Phil
投票支持这个答案,因为它提供了知识和可能的答案,基于大多数人在首次查看此SO答案时遇到的最常见情况。感谢详细的回答!:+1: - anothercoder

8

对于正在研究此主题的人,以下是一个简短的更新。

所选答案可行,但您可能希望考虑使用部分索引。

版本3.2中更改:从MongoDB 3.2开始,MongoDB提供了创建部分索引的选项。 部分索引提供了稀疏索引功能的超集。 如果您使用的是MongoDB 3.2或更高版本,则应优先使用部分索引而不是稀疏索引。

有关部分索引的更多文档:https://docs.mongodb.com/manual/core/index-partial/


2

实际上,仅当“email”字段不存在时,第一个文档才能成功保存。随后的保存操作中,如果没有“email”字段,则会失败并显示错误(请参阅下面的代码片段)。关于唯一索引和缺失键,请查看MongoDB官方文档http://www.mongodb.org/display/DOCS/Indexes#Indexes-UniqueIndexes

  // NOTE: Code to executed in mongo console.

  db.things.ensureIndex({firstname: 1}, {unique: true});
  db.things.save({lastname: "Smith"});

  // Next operation will fail because of the unique index on firstname.
  db.things.save({lastname: "Jones"});

根据定义,唯一索引只允许一个值仅存储一次。如果您将null视为这样的值,则只能插入一次!通过在应用程序级别确保并验证,您的方法是正确的。就是这样可以做到。

您可能还想阅读这篇文章:http://www.mongodb.org/display/DOCS/Querying+and+nulls


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