【问题标题】:Add unique value to every element in array为数组中的每个元素添加唯一值
【发布时间】:2018-06-06 18:17:54
【问题描述】:

我是 MongoDB 的新手,我正在尝试在 MongoDB 集合中合并一个嵌入式数组,我的项目集合的架构如下:

Projects:
{
    _id: ObjectId(),
    client_id: String,
    description: String,
    samples: [
        {
            location: String,      //Unique
            name: String,
        }
      ...
    ]
}

用户可以上传格式如下的 JSON 文件:

[
    {
        location: String,     //Same location as in above schema
        concentration: float
    }
  ...
]

样本数组的长度与上传的数据数组的长度相同。我试图弄清楚如何将数据字段添加到我的示例数组的每个元素中,但我无法根据 MongoDB 文档找到如何做到这一点。我可以将我的 json 数据作为“数据”加载,并且我想根据常见的“位置”字段进行合并:

db.projects.update({_id: myId}, {$set : {samples.$[].data : data[location]}});

但是我想不出如何在更新查询中获取json数组上的索引,而且我在mongodb文档中也找不到任何示例,或者这样的问题。

任何帮助将不胜感激!

【问题讨论】:

  • 如果您需要键/值类型的东西,为什么不使用字典?
  • 您认为提供的答案中是否有某些内容无法解决您的问题?如果是这样,请对答案发表评论,以澄清究竟需要解决哪些尚未解决的问题。如果它确实回答了您提出的问题,请注意Accept your Answers您提出的问题
  • 确实解决了,抱歉,我对这个网站还很陌生,刚刚接受了您的回答,再次感谢!

标签: node.js mongodb mongoose mongodb-query


【解决方案1】:

MongoDB 3.6 位置过滤更新

因此,您实际上使用 positional all $[] 运算符处于正确的“球场”,但问题是它仅适用于“每个”数组元素。由于您想要的是“匹配”条目,因此您实际上需要 positional filtered $[<identifier>] 运算符。

正如您所注意到的,您的 "location" 将是唯一的并且在数组中。使用“索引位置”对于原子更新确实不可靠,但实际上匹配“唯一”属性是可靠的。基本上你需要从这样的东西中获取:

let input = [
  { location: "A", concentration: 3, other: "c" },
  { location: "C", concentration: 4, other: "a" }
];

到这里:

{
  "$set": {
    "samples.$[l0].concentration": 3,
    "samples.$[l0].other": "c",
    "samples.$[l1].concentration": 4,
    "samples.$[l1].other": "a"
  },
  "arrayFilters": [
    {
      "l0.location": "A"
    },
    {
      "l1.location": "C"
    }
  ]
}

这实际上只是将一些基本函数应用于提供的输入数组:

let arrayFilters = input.map(({ location },i) => ({ [`l${i}.location`]: location }));

let $set = input.reduce((o,{ location, ...e },i) =>
  ({
    ...o,
    ...Object.entries(e).reduce((oe,[k,v]) => ({ ...oe, [`samples.$[l${i}].${k}`]: v }),{})
  }),
  {}
);

log({ $set, arrayFilters });

Array.map() 只需获取input 的值并创建标识符列表以匹配arrayFilters 中的location 值。 $set 语句的构造使用Array.reduce(),在从考虑中删除location 之后,两次迭代能够合并每个已处理的数组元素和该数组元素中存在的每个键的键,因为它没有被更新。

或者,循环使用for..of:

let arrayFilters = [];
let $set = {};

for ( let [i, { location, ...e }] of Object.entries(input) ) {
  arrayFilters.push({ [`l${i}.location`]: location });
  for ( let [k,v] of Object.entries(e) ) {
    $set[`samples.$[l${i}].${k}`] = v;
  }
}

请注意,我们在这里使用Object.entries() 以及在构造中使用"object spread" ...。如果您发现自己处于一个没有此支持的 JavaScript 环境中,那么 Object.keys() 和 Object.assign() 基本上是几乎没有变化的替代品。

然后这些实际上可以在更新中应用,如下所示:

Project.update({ client_id: 'ClientA' }, { $set }, { arrayFilters });

所以positional filtered $[<identifier>] 实际上在这里用于在$set 修饰符和update() 的arrayFilters 选项内创建条目的“匹配对”。因此,对于每个"location",我们创建一个与arrayFilters 中的值匹配的标识符,然后在实际的$set 语句中使用相同的标识符,以便仅更新与标识符条件匹配的数组条目。

“标识符”的唯一真正规则是不能以数字开头,并且它们“应该”是唯一的,但这不是规则,无论如何您只需获得第一个匹配项。但是更新只会触及那些实际匹配条件的条目。

早期的 MongoDB 固定索引

如果没有对此的支持,那么您基本上会退回到“索引位置”,这真的不那么可靠。通常情况下,您实际上需要阅读每个文档并在更新之前确定数组中的内容。但是至少假设索引位置到位的“奇偶校验”:

let input = [
  { location: "A", concentration: 3 },
  { location: "B", concentration: 5 },
  { location: "C", concentration: 4 }
];

let $set = input.reduce((o,e,i) =>
  ({ ...o, [`samples.${i}.concentration`]: e.concentration }),{}
);

log({ $set });

生成如下更新语句:

{
  "$set": {
    "samples.0.concentration": 3,
    "samples.1.concentration": 5,
    "samples.2.concentration": 4
  }
}

或者没有奇偶校验:

let input = [
  { location: "A", concentration: 3, other: "c" },
  { location: "C", concentration: 4, other: "a" }
];


// Need to get the document to compare without parity
let doc = await Project.findOne({ "client_id": "ClientA" });

let $set = input.reduce((o,e,i) =>
  ({
    ...o,
    ...Object.entries(e).filter(([k,v]) => k !== "location")
      .reduce((oe,[k,v]) =>
        ({
          ...oe,
          [`samples.${doc.samples.map(c => c.location).indexOf(e.location)}`
            + `.${k}`]: v
        }),
        {}
      )
  }),
  {}
);

log({ $set });


await Project.update({ client_id: 'ClientA' },{ $set });

在索引上生成语句匹配(在您实际阅读文档之后):

{
  "$set": {
    "samples.0.concentration": 3,
    "samples.0.other": "c",
    "samples.2.concentration": 4,
    "samples.2.other": "a"
  }
}

当然要注意,对于每个“更新集”,除了首先从文档中读取以确定要更新的索引之外,您实际上没有其他选择。这通常不是一个好主意,因为除了需要在写入之前读取每个文档的开销之外,不能绝对保证数组本身在读取和写入之间被其他进程保持不变,因此使用“硬索引" 假设一切都是一样的,但实际上可能并非如此。

早期的 MongoDB 位置匹配

在数据允许的情况下,通常最好改为循环标准 positional matched $ 更新。这里location 确实是独一无二的,所以它是一个很好的候选者,最重要的是你不需要阅读现有的文档来比较数组的索引:

let input = [
  { location: "A", concentration: 3, other: "c" },
  { location: "C", concentration: 4, other: "a" }
];

let batch = input.map(({ location, ...e }) =>
  ({
    updateOne: {
      filter: { client_id: "ClientA", 'samples.location': location },
      update: {
        $set: Object.entries(e)
          .reduce((oe,[k,v]) => ({ ...oe,  [`samples.$.${k}`]: v }), {})
      }
    }
  })
);

log({ batch });

await Project.bulkWrite(batch);

bulkWrite() 发送多个更新操作,但它使用单个请求和响应来完成,就像任何其他更新操作一样。实际上,如果您正在处理“更改列表”,则返回文档以进行比较,然后构造一个大的bulkWrite(),而不是单独写入,这实际上甚至适用于所有先前的示例。

最大的区别是更改集中的“每个数组元素一个更新指令”。这是在没有“位置过滤”支持的版本中做事的安全方式,即使这意味着更多的写入操作。

演示

演示中的完整列表如下。请注意,为了简单起见,我在这里使用“猫鼬”,但对于实际更新本身并没有真正的“猫鼬特定”。这同样适用于任何实现,尤其是在这种情况下,使用 Array.map() 和 Array.reduce() 处理列表以进行构造的 JavaScript 示例。

const { Schema } = mongoose = require('mongoose');

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

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

const sampleSchema = new Schema({
  location: String,
  name: String,
  concentration: Number,
  other: String
});

const projectSchema = new Schema({
  client_id: String,
  description: String,
  samples: [sampleSchema]
});

const Project = mongoose.model('Project', projectSchema);

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 Project.create({
      client_id: "ClientA",
      description: "A Client",
      samples: [
        { location: "A", name: "Location A" },
        { location: "B", name: "Location B" },
        { location: "C", name: "Location C" }
      ]
    });

    let input = [
      { location: "A", concentration: 3, other: "c" },
      { location: "C", concentration: 4, other: "a" }
    ];

    let arrayFilters = input.map(({ location },i) => ({ [`l${i}.location`]: location }));

    let $set = input.reduce((o,{ location, ...e },i) =>
      ({
        ...o,
        ...Object.entries(e).reduce((oe,[k,v]) => ({ ...oe, [`samples.$[l${i}].${k}`]: v }),{})
      }),
      {}
    );

    log({ $set, arrayFilters });

    await Project.update(
      { client_id: 'ClientA' },
      { $set },
      { arrayFilters }
    );

    let project = await Project.findOne();
    log(project);

    mongoose.disconnect();

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

})()

对于那些懒得跑的人,输出显示匹配的数组元素已更新:

Mongoose: projects.remove({}, {})
Mongoose: projects.insertOne({ _id: ObjectId("5b1778605c59470ecaf10fac"), client_id: 'ClientA', description: 'A Client', samples: [ { _id: ObjectId("5b1778605c59470ecaf10faf"), location: 'A', name: 'Location A' }, { _id: ObjectId("5b1778605c59470ecaf10fae"), location: 'B', name: 'Location B' }, { _id: ObjectId("5b1778605c59470ecaf10fad"), location: 'C', name: 'Location C' } ], __v: 0 })
{
  "$set": {
    "samples.$[l0].concentration": 3,
    "samples.$[l0].other": "c",
    "samples.$[l1].concentration": 4,
    "samples.$[l1].other": "a"
  },
  "arrayFilters": [
    {
      "l0.location": "A"
    },
    {
      "l1.location": "C"
    }
  ]
}
Mongoose: projects.update({ client_id: 'ClientA' }, { '$set': { 'samples.$[l0].concentration': 3, 'samples.$[l0].other': 'c', 'samples.$[l1].concentration': 4, 'samples.$[l1].other': 'a' } }, { arrayFilters: [ { 'l0.location': 'A' }, { 'l1.location': 'C' } ] })
Mongoose: projects.findOne({}, { fields: {} })
{
  "_id": "5b1778605c59470ecaf10fac",
  "client_id": "ClientA",
  "description": "A Client",
  "samples": [
    {
      "_id": "5b1778605c59470ecaf10faf",
      "location": "A",
      "name": "Location A",
      "concentration": 3,
      "other": "c"
    },
    {
      "_id": "5b1778605c59470ecaf10fae",
      "location": "B",
      "name": "Location B"
    },
    {
      "_id": "5b1778605c59470ecaf10fad",
      "location": "C",
      "name": "Location C",
      "concentration": 4,
      "other": "a"
    }
  ],
  "__v": 0
}

或者通过硬索引:

const { Schema } = mongoose = require('mongoose');

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

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

const sampleSchema = new Schema({
  location: String,
  name: String,
  concentration: Number,
  other: String
});

const projectSchema = new Schema({
  client_id: String,
  description: String,
  samples: [sampleSchema]
});

const Project = mongoose.model('Project', projectSchema);

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 Project.create({
      client_id: "ClientA",
      description: "A Client",
      samples: [
        { location: "A", name: "Location A" },
        { location: "B", name: "Location B" },
        { location: "C", name: "Location C" }
      ]
    });

    let input = [
      { location: "A", concentration: 3, other: "c" },
      { location: "C", concentration: 4, other: "a" }
    ];


    // Need to get the document to compare without parity
    let doc = await Project.findOne({ "client_id": "ClientA" });

    let $set = input.reduce((o,e,i) =>
      ({
        ...o,
        ...Object.entries(e).filter(([k,v]) => k !== "location")
          .reduce((oe,[k,v]) =>
            ({
              ...oe,
              [`samples.${doc.samples.map(c => c.location).indexOf(e.location)}`
                + `.${k}`]: v
            }),
            {}
          )
      }),
      {}
    );

    log({ $set });


    await Project.update(
      { client_id: 'ClientA' },
      { $set },
    );

    let project = await Project.findOne();
    log(project);

    mongoose.disconnect();

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

})()

还有输出:

Mongoose: projects.remove({}, {})
Mongoose: projects.insertOne({ _id: ObjectId("5b1778e0f7be250f2b7c3fc8"), client_id: 'ClientA', description: 'A Client', samples: [ { _id: ObjectId("5b1778e0f7be250f2b7c3fcb"), location: 'A', name: 'Location A' }, { _id: ObjectId("5b1778e0f7be250f2b7c3fca"), location: 'B', name: 'Location B' }, { _id: ObjectId("5b1778e0f7be250f2b7c3fc9"), location: 'C', name: 'Location C' } ], __v: 0 })
Mongoose: projects.findOne({ client_id: 'ClientA' }, { fields: {} })
{
  "$set": {
    "samples.0.concentration": 3,
    "samples.0.other": "c",
    "samples.2.concentration": 4,
    "samples.2.other": "a"
  }
}
Mongoose: projects.update({ client_id: 'ClientA' }, { '$set': { 'samples.0.concentration': 3, 'samples.0.other': 'c', 'samples.2.concentration': 4, 'samples.2.other': 'a' } }, {})
Mongoose: projects.findOne({}, { fields: {} })
{
  "_id": "5b1778e0f7be250f2b7c3fc8",
  "client_id": "ClientA",
  "description": "A Client",
  "samples": [
    {
      "_id": "5b1778e0f7be250f2b7c3fcb",
      "location": "A",
      "name": "Location A",
      "concentration": 3,
      "other": "c"
    },
    {
      "_id": "5b1778e0f7be250f2b7c3fca",
      "location": "B",
      "name": "Location B"
    },
    {
      "_id": "5b1778e0f7be250f2b7c3fc9",
      "location": "C",
      "name": "Location C",
      "concentration": 4,
      "other": "a"
    }
  ],
  "__v": 0
}

当然还有标准的"positional" $ 语法和更新:

const { Schema } = mongoose = require('mongoose');

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

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

const sampleSchema = new Schema({
  location: String,
  name: String,
  concentration: Number,
  other: String
});

const projectSchema = new Schema({
  client_id: String,
  description: String,
  samples: [sampleSchema]
});

const Project = mongoose.model('Project', projectSchema);

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 Project.create({
      client_id: "ClientA",
      description: "A Client",
      samples: [
        { location: "A", name: "Location A" },
        { location: "B", name: "Location B" },
        { location: "C", name: "Location C" }
      ]
    });

    let input = [
      { location: "A", concentration: 3, other: "c" },
      { location: "C", concentration: 4, other: "a" }
    ];

    let batch = input.map(({ location, ...e }) =>
      ({
        updateOne: {
          filter: { client_id: "ClientA", 'samples.location': location },
          update: {
            $set: Object.entries(e)
              .reduce((oe,[k,v]) => ({ ...oe,  [`samples.$.${k}`]: v }), {})
          }
        }
      })
    );

    log({ batch });

    await Project.bulkWrite(batch);

    let project = await Project.findOne();
    log(project);

    mongoose.disconnect();

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

})()

然后输出:

Mongoose: projects.remove({}, {})
Mongoose: projects.insertOne({ _id: ObjectId("5b179142662616160853ba4a"), client_id: 'ClientA', description: 'A Client', samples: [ { _id: ObjectId("5b179142662616160853ba4d"), location: 'A', name: 'Location A' }, { _id: ObjectId("5b179142662616160853ba4c"), location: 'B', name: 'Location B' }, { _id: ObjectId("5b179142662616160853ba4b"), location: 'C', name: 'Location C' } ], __v: 0 })
{
  "batch": [
    {
      "updateOne": {
        "filter": {
          "client_id": "ClientA",
          "samples.location": "A"
        },
        "update": {
          "$set": {
            "samples.$.concentration": 3,
            "samples.$.other": "c"
          }
        }
      }
    },
    {
      "updateOne": {
        "filter": {
          "client_id": "ClientA",
          "samples.location": "C"
        },
        "update": {
          "$set": {
            "samples.$.concentration": 4,
            "samples.$.other": "a"
          }
        }
      }
    }
  ]
}
Mongoose: projects.bulkWrite([ { updateOne: { filter: { client_id: 'ClientA', 'samples.location': 'A' }, update: { '$set': { 'samples.$.concentration': 3, 'samples.$.other': 'c' } } } }, { updateOne: { filter: { client_id: 'ClientA', 'samples.location': 'C' }, update: { '$set': { 'samples.$.concentration': 4, 'samples.$.other': 'a' } } } } ], {})
Mongoose: projects.findOne({}, { fields: {} })
{
  "_id": "5b179142662616160853ba4a",
  "client_id": "ClientA",
  "description": "A Client",
  "samples": [
    {
      "_id": "5b179142662616160853ba4d",
      "location": "A",
      "name": "Location A",
      "concentration": 3,
      "other": "c"
    },
    {
      "_id": "5b179142662616160853ba4c",
      "location": "B",
      "name": "Location B"
    },
    {
      "_id": "5b179142662616160853ba4b",
      "location": "C",
      "name": "Location C",
      "concentration": 4,
      "other": "a"
    }
  ],
  "__v": 0
}

【讨论】:

  • 这很棒,而且效果很好,可以说我有要更新的属性,例如为每个样本设置浓度和体积。我正在尝试遍历输入文档中的键:让键 = Object.keys(input),然后在 reduce 函数中使用它:let $set = result.reduce((o, e, i) => keys.forEach((key) => ({ ...o, keys.forEach((key) => ([`samples.$[l${i}].${key}`]: e[key) }), {} ); 但这对我不起作用,有什么建议吗?
  • 我相信这是因为我正在运行 mongo 3.4.10,并且根据this 它还没有在我的版本中实现
  • @nrichman 嗯,这正是答案告诉您的内容,它非常具体地说明了作为 MongoDB 3.6 的特性以及在文档本身上的突出地位。我还向您展示的是使用实际的“索引”,尽管不好的做法也可以。查看带有"samples.0.concentration": 3 之类的行。请注意,除非您的输入是精确副本,否则您需要读取每个文档,读取数组并进行比较,然后执行更新。
  • @nrichman 扩展了不同的解决方案,在每个可能的“更改集”中提供了更多上下文、示例和location 的任意数量的附加键的用法。您确实应该查看带有位置$ 过滤运算符和用法的部分,因为对于具有“位置过滤”支持的早期 MongoDB 版本而言,这通常是更好的选择。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-06-23
  • 2018-07-26
  • 1970-01-01
  • 2021-09-15
  • 2021-03-20
  • 1970-01-01
相关资源
最近更新 更多