【问题标题】:Mongoose Nested Array $push猫鼬嵌套数组 $push
【发布时间】:2018-10-30 15:32:34
【问题描述】:

我正在尝试将$pushObject 放入一个嵌套数组,但它似乎不起作用。我不确定我做错了什么。

我的数据库是这样的:

{
  customers: {
    name: String,
    address: String,
    proj_managers: [
      {
        name: String,
        username: String,
        projects: [
          name: String,
          tags: [
            {
              tag_no: Number,
              tag_id: String,
              time: String,
              product_id: String,
              urls: [
                url: String,
                count: Number
              ],
              gps: String,
              deactivate: Boolean
            }
          ]
        ]
      }
    ]
  }
}

所以我想要做的是$push 将一组标签放入tags,用于特定的project。我的后端使用GraphQL:

/index.js

// GraphQL schema
import schema from './schema'
// Mongoose Models
import Customer from './models/Customer'
import Manager from './models/Manager'
// Access to GraphQL API
app.use('/graphql', bodyParser.json(), graphqlExpress({ schema, context: { Customer, Manager } }))
app.use('/graphiql', graphiqlExpress({ endpointURL: '/graphql' }))

/schema/index.js

import { bundle } from 'graphql-modules'
import { makeExecutableSchema } from 'graphql-tools'

// GraphQL Modules
import Manager from './Manager'
import Customer from './Customer'
import ProjectManager from './ProjectManager'
import Project from './Project'
import Tag from './Tag'
import URL from './URL'

const modules = [Manager, Customer, ProjectManager, Project, Tag, URL]

export default makeExecutableSchema(bundle(modules))

/schema/Project.js

const schema = `
  type Project {
    _id: ID,
    name: String!,
    description: String,
    tags: [Tag],
    deactivate: Boolean!
  }
  input TagInput {
    tagNo: Int!,
    tagId: String!,
    time: String,
    productId: String,
    url1: String,
    url2: String,
    gps: String
  }
`

const queries = `
  projects(customerUsername: String!): [Project],
  project(projectID: ID!): Project
`

const mutations = `
  editProject(id: ID!, name: String, description: String, deactivate: Boolean, customerUsername: String!, pmUsername: String!): String,
  addProject(name: String!, description: String, customerID: ID!, pmUsername: String!): String,
  pushTags(customerID: String!, username: String!, projectID: ID!, isManager: Boolean!, tags: [TagInput]!): String
`

const pushTags = async (root, { tags, customerID, username, projectID, isManager }, { Customer }) => {
  let result = ''
  let query = { _id: customerID }
  let update = {}
  let ts = []
  let options = {
    arrayFilters: [
      { 'a.username': username },
      { 'b._id': projectID }
    ]
  }
  tags.forEach(tag => {
    if (isManager) {
      ts.push({
        tag_no: tag.tagNo,
        tag_id: tag.tagId,
        time: new Date(),
        product_id: tag.productId,
        urls: [
          { url: tag.url1, count: 0 },
          { url: tag.url2, count: 0 }
        ],
        gps: tag.gps,
        deactivate: false
      })
    } else {
      update = {
        $set: {
          'proj_managers.$[a].projects.$[b].tags': {
            product_id: tag.productId,
            urls: [
              { url: tag.url1 },
              { url: tag.url2 }
            ],
            gps: tag.gps
          }
        }
      }
    }
  })
  if (isManager) {
    update = {
      $push: {
        'proj_managers.$[a].projects.$[b].tags': {
          $each: ts
        }
      }
    }
    result = await Customer.update(query, update, options)
  }
  return result.ok && result.nModified ? 'Success' : 'Failed'
}

const resolvers = {
  queries: {
    projects,
    project
  },
  mutations: {
    addProject,
    editProject,
    pushTags
  }
}

export default () => ({
  schema,
  queries,
  mutations,
  resolvers
})

正在发送到pushTags 突变的标签是:

[
  {
    "tagNo":"1",
    "tagId":"02F9AMCGA38O7L",
    "productId":"",
    "url1":"",
    "url2":"",
    "gps":""
  },{
    "tagNo":"2",
    "tagId":"028MFL6EV5L904",
    "productId":"",
    "url1":"",
    "url2":"",
    "gps":""
  },{
    "tagNo":"3",
    "tagId":"02XDWCIL6W2IIX",
    "productId":"",
    "url1":"",
    "url2":"",
    "gps":""
  }
];

文档

{
  "_id": ObjectId("5b0216f1cf14851f18e4312b"),
  "deactivate": false,
  "name": "Razer",
  "address": "201 3rd Street, Suite 900 San Francisco, CA 94103 USA",
  "phone_no": "0987654321",
  "proj_managers": [
    {
      "deactivate": false,
      "_id": ObjectId("5b021750cf14851f18e4312c"),
      "name": "Liang-Shih Lin",
      "username": "troopy",
      "phone_no": "0987654321",
      "password": "$2b$10$eOVoRkfmkHQyHkc6XaDanunUuyi0EFy.oZ.dRgKJYxBciMLYUVy0W",
      "projects": [
        {
          "deactivate": false,
          "_id": ObjectId("5b0217d4cf14851f18e4312d"),
          "name": "Razer Godzilla",
          "description": "A Godzilla Mouse",
          "tags": [ ]
        }
      ]
    }
  ],
  "__v": 0
}

我尝试过使用findByIdAndUpdateupdateOne、使用forEach() 辅助函数循环遍历标签和$push 将其一一进入数据库,但似乎没有任何效果。我以为可能是我的arrayFilters,我将b._id 更改为b.name,但这也没有用。

我尝试在mongo shell 中使用它,并使用以下查询:

db.customers.update({ _id: "5afe642ed42aee261cb3292e" }, { $push: { "proj_managers.$[a].projects.$[b].tags": { tag_no: 1, tag_id: "0476F06A594980", time: "2018-05-20T23:18:18.824Z", product_id: "xr235Yf4", urls: [{url: "example.com", count: 0}, {url: "example2.com", count: 0}], gps: "", deactivate: false} } }, { arrayFilters: [{ "a.username": "joliver" }, { "b.id": "5b01367b6d053860e90e0f9f" }] })

结果:

WriteResult({
  "nMatched": 0,
  "nUpserted": 0,
  "nModified": 0
})

如果你想看看整个项目,这里是link

【问题讨论】:

  • 最后我们有一个可重现的问题。我怀疑有这样的事情,所以我有一个示例中显示的“缩减”版本的数据以供回答。

标签: mongodb mongoose mongodb-query


【解决方案1】:

您在尝试中错过的是 arrayFilters 条目不会像猫鼬操作中的其他属性那样根据“模式”的值“自动转换”。这是因为没有任何东西将条件与定义的模式中的特定细节联系起来,或者至少就当前的 mongoose 版本处理它而言。

因此,如果您匹配arrayFilters 中的_id,您需要自己“转换”ObjectId 值,其中源来自“字符串”:

let updated = await Customer.findOneAndUpdate(
  {
    "_id": "5b0216f1cf14851f18e4312b",              //<-- mongoose can autocast these
    "proj_managers": {
      "$elemMatch": {
        "username": "troopy",
        "projects._id": "5b0217d4cf14851f18e4312d" //<-- here as well
      }
    }
  },
  {
    "$push": {
      "proj_managers.$[a].projects.$[b].tags": { "$each": tags }
    }
  },
  {
    "new": true,
    // But not in here
    "arrayFilters": [
      { "a.username": "troopy" },
      { "b._id": ObjectId("5b0217d4cf14851f18e4312d") }  // <-- Cast manually
    ]
  }
);

然后你会得到你应该得到的结果。剪掉一点只是为了演示:

{
  "_id": "5b0216f1cf14851f18e4312b",
  "name": "Bill",
  "address": "1 some street",
  "proj_managers": [
    {
      "projects": [
        {
          "tags": [
            {
              "_id": "5b0239cc0a7a34219b0efdab",
              "tagNo": 1,
              "tagId": "02F9AMCGA38O7L",
              "productId": "",
              "url1": "",
              "url2": "",
              "gps": ""
            },
            {
              "_id": "5b0239cc0a7a34219b0efdaa",
              "tagNo": 2,
              "tagId": "028MFL6EV5L904",
              "productId": "",
              "url1": "",
              "url2": "",
              "gps": ""
            },
            {
              "_id": "5b0239cc0a7a34219b0efda9",
              "tagNo": 3,
              "tagId": "02XDWCIL6W2IIX",
              "productId": "",
              "url1": "",
              "url2": "",
              "gps": ""
            }
          ],
          "_id": "5b0217d4cf14851f18e4312d",
          "name": "Razer Godzilla"
        }
      ],
      "_id": "5b021750cf14851f18e4312c",
      "name": "Ted",
      "username": "troopy"
    }
  ],
  "__v": 0
}

所以这里主要是从Types.ObjectId 导入ObjectId 方法并实际转换您拥有的任何字符串。来自外部请求的输入通常是“字符串”。

所以现在,只要您想将这些值与 positional filtered $[&lt;identifier&gt;] 运算符和 arrayFilters 的匹配项结合起来,请记住实际“转换类型”。

请注意,在此处使用$elemMatch 用于数组上的相同匹配条件实际上并不是“要求”,但可能始终应被视为最佳实践。原因是虽然arrayFilters 条件实际上将决定选择实际更改的内容,但使用“查询”条件支持它以确保数组上确实存在相同的条件,从而确保文档甚至不会被考虑,这实际上减少了处理开销。

还请注意,由于您在每个数组成员中使用了一个唯一的 _id 值,这是猫鼬模式固有的,那么您基本上可以“摆脱”

let wider = await Customer.findOneAndUpdate(
  { "_id": "5b0216f1cf14851f18e4312b" },
  { "$push": {
    "proj_managers.$[].projects.$[b].tags": { "$each": extra }
  }},
  {
    "new": true,
    "arrayFilters": [
      { "b._id": ObjectId("5b0217d4cf14851f18e4312d") }
    ]
  }
);

所以实际上使用positional all $[] 代替并且如上所述简单地跳过其他条件以支持“唯一”ObjectId 值。它看起来更轻,但实际上通过不必要地检查可能的各种数组路径增加了几个 cpu 周期,更不用说如果数组根本不满足其他条件,文档本身的匹配。

如果没有 "caveat",我也不能真正离开,尽管现代 MongoDB 版本会支持这一点,但仍然不建议使用嵌套数组。是的,可以使用现代功能对其进行更新,但“查询”它们仍然比使用更扁平的数组结构甚至在单独的集合中完全扁平化的数据要复杂得多,具体取决于需要。

Updating a Nested Array with MongoDBFind in Double Nested Array MongoDB 有更多描述,但在大多数情况下,将结构视为“嵌套”的感知“组织”确实不存在,这确实是一个障碍。

以及展示工作更新的完整清单:

const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');

const uri = 'mongodb://localhost/test';

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

const tagSchema = new Schema({
  tagNo: Number,
  tagId: String,
  productId: String,
  url1: String,
  url2: String,
  gps: String
})

const projectSchema = new Schema({
  name: String,
  tags: [tagSchema]
})

const projManagerSchema = new Schema({
  name: String,
  username: String,
  projects: [projectSchema]
});

const customerSchema = new Schema({
  name: String,
  address: String,
  proj_managers: [projManagerSchema]
});


const Customer = mongoose.model('Customer', customerSchema);

const log = data => console.log(JSON.stringify(data, undefined, 2));

(async function() {

  try {

    const conn = await mongoose.connect(uri);

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

    await Customer.create({
      _id: "5b0216f1cf14851f18e4312b",
      name: 'Bill',
      address: '1 some street',
      proj_managers: [
        {
          _id: "5b021750cf14851f18e4312c",
          name: "Ted",
          username: "troopy",
          projects: [
            {
              _id: "5b0217d4cf14851f18e4312d",
              name: "Razer Godzilla",
              tags: [  ]
            }
          ]
        }
      ]
    });

    const tags = [
      {
        "tagNo":"1",
        "tagId":"02F9AMCGA38O7L",
        "productId":"",
        "url1":"",
        "url2":"",
        "gps":""
      },{
        "tagNo":"2",
        "tagId":"028MFL6EV5L904",
        "productId":"",
        "url1":"",
        "url2":"",
        "gps":""
      },{
        "tagNo":"3",
        "tagId":"02XDWCIL6W2IIX",
        "productId":"",
        "url1":"",
        "url2":"",
        "gps":""
      }
    ];

    const extra = [{
      "tagNo":"4",
      "tagId":"02YIVGMFZBC9OI",
      "productId":"",
      "url1":"",
      "url2":"",
      "gps":""
    }];

    let cust = await Customer.findOne({
      "_id": "5b0216f1cf14851f18e4312b",
      "proj_managers": {
        "$elemMatch": {
          "username": "troopy",
          "projects._id": "5b0217d4cf14851f18e4312d"
        }
      }
    });
    log(cust);
    let updated = await Customer.findOneAndUpdate(
      {
        "_id": "5b0216f1cf14851f18e4312b",
        "proj_managers": {
          "$elemMatch": {
            "username": "troopy",
            "projects._id": "5b0217d4cf14851f18e4312d"
          }
        }
      },
      {
        "$push": {
          "proj_managers.$[a].projects.$[b].tags": { "$each": tags }
        }
      },
      {
        "new": true,
        "arrayFilters": [
          { "a.username": "troopy" },
          { "b._id": ObjectId("5b0217d4cf14851f18e4312d") }
        ]
      }
    );
    log(updated);

    let wider = await Customer.findOneAndUpdate(
      { "_id": "5b0216f1cf14851f18e4312b" },
      { "$push": {
        "proj_managers.$[].projects.$[b].tags": { "$each": extra }
      }},
      {
        "new": true,
        "arrayFilters": [
          { "b._id": ObjectId("5b0217d4cf14851f18e4312d") }
        ]
      }
    );
    log(wider);

    mongoose.disconnect();

  } catch(e) {
    console.error(e)
  } finally {
    process.exit()
  }

})()

【讨论】:

  • 解决了!谢谢你,很抱歉你提到document时我没明白你的意思
  • @Liang-ShihLin 现在你知道了。如果您在提问时确实显示了集合中的内容,那么我们可以重现。就目前而言,一旦“应该”有一种方法可以根据更新中使用的“密钥”的内容匹配arrayFilters 条件,我可能会尽快为猫鼬发送一个补丁。只需要一点花哨的编码,但应该可以修复。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-17
  • 2021-06-29
  • 2021-04-14
  • 2021-10-17
  • 2013-11-24
  • 2015-11-24
相关资源
最近更新 更多