使用 arrayFilters 更新 MongoDB 中的嵌套子文档

Posted

技术标签:

【中文标题】使用 arrayFilters 更新 MongoDB 中的嵌套子文档【英文标题】:Update nested subdocuments in MongoDB with arrayFilters 【发布时间】:2018-04-23 19:43:01 【问题描述】:

我需要修改另一个数组中的数组中的文档。 我知道 MongoDB 不支持多个 '$' 同时迭代多个数组,但他们为此引入了 arrayFilters。 见:https://jira.mongodb.org/browse/SERVER-831

MongoDB的示例代码:

db.coll.update(, $set: “a.$[i].c.$[j].d”: 2, arrayFilters: [“i.b”: 0, “j.d”: 0])
Input: a: [b: 0, c: [d: 0, d: 1], b: 1, c: [d: 0, d: 1]]
Output: a: [b: 0, c: [d: 2, d: 1], b: 1, c: [d: 0, d: 1]]

文档的设置方式如下:


    "_id" : ObjectId("5a05a8b7e0ce3444f8ec5bd7"),
    "name" : "support",
    "contactTypes" : 
        "nonWorkingHours" : [],
        "workingHours" : []
    ,
    "workingDays" : [],
    "people" : [ 
        
            "enabled" : true,
            "level" : "1",
            "name" : "Someone",
            "_id" : ObjectId("5a05a8c3e0ce3444f8ec5bd8"),
            "contacts" : [ 
                
                    "_id" : ObjectId("5a05a8dee0ce3444f8ec5bda"),
                    "retries" : "1",
                    "priority" : "1",
                    "type" : "email",
                    "data" : "some.email@email.com"
                
            ]
        
    ],
    "__v" : 0

这是架构:

const ContactSchema = new Schema(
    data: String,
    type: String,
    priority: String,
    retries: String
);

const PersonSchema = new Schema(
    name: String,
    level: String,
    priority: String,
    enabled: Boolean,
    contacts: [ContactSchema]
);

const GroupSchema = new Schema(
    name: String,
    people: [PersonSchema],
    workingHours:  start: String, end: String ,
    workingDays: [Number],
    contactTypes:  workingHours: [String], nonWorkingHours: [String] 
);

我需要更新联系人。这是我尝试使用 arrayFilters 的方法:

Group.update(
    ,
    '$set': 'people.$[i].contacts.$[j].data': 'new data',
    arrayFilters: [
        'i._id': mongoose.Types.ObjectId(req.params.personId),
        'j._id': mongoose.Types.ObjectId(req.params.contactId)],
    function(err, doc) 
        if (err) 
            res.status(500).send(err);
        
        res.send(doc);
    
);

文档从未更新,我收到以下回复:


    "ok": 0,
    "n": 0,
    "nModified": 0

我做错了什么?

【问题讨论】:

乍一看似乎是正确的。你确定你在req.params.personIdreq.params.contactId 中的值是正确的吗?尝试对它们进行硬编码以确保不是问题...另外,您是否尝试过通过另一个客户端运行该查询以确保它不是 Mongoose 问题? 我发现我使用的是 3.5.12 以下的 MongoDB 版本,它实现了这个 'arrayFilters' 功能。我目前正在设置 3.5.13,我将在此处发布结果。 我将 MongoDB 更新到 3.5.13,但仍然无法编辑嵌套文档。得到相同的结果:0, 0, 0 如果您打开“调试”mongoose.set('debug', true),那么您实际上会看到"arrayFilters" is actually being **stripped** from the statement and not being sent to MongoDB at all. I also want to strongly note that MongoDB 3.5 is a **development release** ( so are current 3.6 release candidates ) and as such arrayFilters` 实际上还没有正式向全世界发布。更新的驱动程序正在为即将发布的实际版本开发。当从与发行版匹配的mongo shell 发出时,该命令运行良好。 相关:Mongodb 3.6.0-rc3 array filters not working?。所以它确实工作得很好。只是您正在使用的“已发布”驱动程序实际上并没有赶上允许传递必要的参数。 【参考方案1】:

所以arrayFilters 选项和positional filtered $[<identifier>] 确实适用于自 MongoDB 3.5.12 以来的开发版本系列以及 MongoDB 3.6 系列的当前候选版本,这实际上将正式发布。唯一的问题当然是正在使用的“驱动程序”实际上还没有赶上这一点。

重复我已经放在Updating a Nested Array with MongoDB 上的相同内容:

注意有点讽刺的是,由于这是在 .update() 和类似方法的“选项”参数中指定的,因此语法通常与所有最新发布的驱动程序版本兼容。

但是mongo shell 并非如此,因为该方法在那里实现的方式(“具有讽刺意味的是,为了向后兼容”)arrayFilters 参数无法被解析选项的内部方法识别和删除为了提供与先前 MongoDB 服务器版本的“向后兼容性”和“遗留”.update() API 调用语法。

因此,如果您想在mongo shell 或其他“基于 shell”的产品(尤其是 Robo 3T)中使用该命令,您需要来自开发分支或生产版本的 3.6 或更高版本的最新版本。

这意味着.update() 的当前“驱动程序”实现实际上“删除”了arrayFilters 定义的必要参数。对于 NodeJS,这将在驱动程序的 3.x 版本系列中解决,当然“mongoose”在该版本之后可能需要一些时间来实现它自己对更新驱动程序的依赖,然后不再“剥离”此类行为。

但是,您仍然可以在支持的服务器实例上运行它,方法是退回到基本的"update command" 语法用法,因为它绕过了已实现的驱动程序方法:

const mongoose = require('mongoose'),
      Schema = mongoose.Schema,
      ObjectId = mongoose.Types.ObjectId;

mongoose.Promise = global.Promise;
mongoose.set('debug',true);

const uri = 'mongodb://localhost/test',
      options =  useMongoClient: true ;

const contactSchema = new Schema(
  data: String,
  type: String,
  priority: String,
  retries: String
);

const personSchema = new Schema(
  name: String,
  level: String,
  priority: String,
  enabled: Boolean,
  contacts: [contactSchema]
);

const groupSchema = new Schema(
  name: String,
  people: [personSchema],
  workingHours:  start: String, end: String ,
  workingDays:  type: [Number], default: undefined ,
  contactTypes: 
    workingHours:  type: [String], default: undefined ,
    contactTypes:  type: [String], default: undefined 
  
);

const Group = mongoose.model('Group', groupSchema);

function log(data) 
  console.log(JSON.stringify(data, undefined, 2))


(async function() 

  try 

    const conn = await mongoose.connect(uri,options);

    // Clean data
    await Promise.all(
      Object.entries(conn.models).map(([k,m]) => m.remove() )
    );

    // Create sample

    await Group.create(
      name: "support",
      people: [
        
          "_id": ObjectId("5a05a8c3e0ce3444f8ec5bd8"),
          "enabled": true,
          "level": "1",
          "name": "Someone",
          "contacts": [
            
              "type": "email",
              "data": "adifferent.email@example.com"
            ,
            
              "_id": ObjectId("5a05a8dee0ce3444f8ec5bda"),
              "retries": "1",
              "priority": "1",
              "type": "email",
              "data": "some.email@example.com"
            
          ]
        
      ]
    );

    let result = await conn.db.command(
      "update": Group.collection.name,
      "updates": [
        
          "q": ,
          "u":  "$set":  "people.$[i].contacts.$[j].data": "new data"  ,
          "multi": true,
          "arrayFilters": [
             "i._id": ObjectId("5a05a8c3e0ce3444f8ec5bd8") ,
             "j._id": ObjectId("5a05a8dee0ce3444f8ec5bda") 
          ]
        
      ]
    );

    log(result);

    let group = await Group.findOne();
    log(group);

   catch(e) 
    console.error(e);
   finally 
    mongoose.disconnect();
  

)()

由于直接将“命令”发送到服务器,我们看到预期的更新确实发生了:

Mongoose: groups.remove(, )
Mongoose: groups.insert( name: 'support', _id: ObjectId("5a06557fb568aa0ad793c5e4"), people: [  _id: ObjectId("5a05a8c3e0ce3444f8ec5bd8"), enabled: true, level: '1', name: 'Someone', contacts: [  type: 'email', data: 'adifferent.email@example.com', _id: ObjectId("5a06557fb568aa0ad793c5e5") ,  _id: ObjectId("5a05a8dee0ce3444f8ec5bda"), retries: '1', priority: '1', type: 'email', data: 'some.email@example.com'  ]  ], __v: 0 )
 n: 1,
  nModified: 1,
  opTime:
    ts: Timestamp  _bsontype: 'Timestamp', low_: 3, high_: 1510364543 ,
     t: 24 ,
  electionId: 7fffffff0000000000000018,
  ok: 1,
  operationTime: Timestamp  _bsontype: 'Timestamp', low_: 3, high_: 1510364543 ,
  '$clusterTime':
    clusterTime: Timestamp  _bsontype: 'Timestamp', low_: 3, high_: 1510364543 ,
     signature:  hash: [Object], keyId: 0   
Mongoose: groups.findOne(,  fields:  )

  "_id": "5a06557fb568aa0ad793c5e4",
  "name": "support",
  "__v": 0,
  "people": [
    
      "_id": "5a05a8c3e0ce3444f8ec5bd8",
      "enabled": true,
      "level": "1",
      "name": "Someone",
      "contacts": [
        
          "type": "email",
          "data": "adifferent.email@example.com",
          "_id": "5a06557fb568aa0ad793c5e5"
        ,
        
          "_id": "5a05a8dee0ce3444f8ec5bda",
          "retries": "1",
          "priority": "1",
          "type": "email",
          "data": "new data"            // <-- updated here
        
      ]
    
  ]

所以正确“现在”[1]“现成”可用的驱动程序实际上并没有实现.update(),或者它是其他实现对应物的方式与实际通过必要的arrayFilters 参数兼容。因此,如果您正在“玩”开发系列或发布候选服务器,那么您真的应该准备好同时使用“前沿”和未发布的驱动程序。

但您实际上可以按照在任何驱动程序中演示的那样以正确的形式执行此操作,其中发出的命令不会被更改。

[1] 截至 2017 年 11 月 11 日撰写本文时,没有 “官方” 版本的 MongoDB 或实际实现的受支持驱动程序这。生产使用应仅基于服务器的官方版本和支持的驱动程序。

【讨论】:

以上是关于使用 arrayFilters 更新 MongoDB 中的嵌套子文档的主要内容,如果未能解决你的问题,请参考以下文章

UI未使用ko.utils.arrayFilter进行更新

MongoDB中的arrayFilters

MongoDB中的arrayFilters

你如何使用mongoose 5.x.x的arrayFilters? [重复]

Node.js Mongoose .update 与 ArrayFilters

使用数组过滤器更新多个嵌套数组在猫鼬中不起作用