为数组中的每个元素添加唯一值

时间:2018-06-04 20:20:25

标签: node.js mongodb mongoose mongodb-query

我对MongoDB相当新,我试图在MongoDB集合中合并嵌入式数组,我的Project集合的架构如下:

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
    }
  ...
]

samples数组的长度与上传的数据数组的长度相同。我试图弄清楚如何将数据字段添加到我的样本数组的每个元素中,但我无法根据MongoDB文档找到如何执行此操作。我可以将我的json数据加载到" data"我想基于共同的"位置"进行合并。字段:

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

但是我无法想到如何在更新查询中获取json数组的索引,而且我还没有找到mongodb文档中的任何示例,或者像这样的问题。

非常感谢任何帮助!

1 个答案:

答案 0 :(得分:1)

MongoDB 3.6位置过滤更新

所以,你实际上是在正确的&#34;棒球场&#34;使用positional all $[]运算符,但问题是只是简单地应用于&#34;每个&#34;数组元素。因为你想要的是&#34;匹配&#34;您确实需要positional filtered $[<identifier>]运算符的条目。

如您所知,"location"将在阵列中独一无二。使用&#34;索引位置&#34;原子更新确实不可靠,但实际上匹配了&#34; unique&#34;属性是。基本上你需要得到这样的东西:

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的值,并创建一个标识符列表,以匹配location中的arrayFilters值。 $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>]实际上用于创建&#34;匹配对&#34; $set修饰符中的条目以及arrayFilters的{​​{1}}选项中的条目。因此,对于每个update(),我们创建一个与"location"中的值匹配的标识符,然后在实际的$set语句中使用相同的标识符,以便只更新与条件匹配的数组条目为标识符。

&#34;标识符&#34;唯一真正的规则是不能从一个数字开始,他们&#34;应该&#34;是独一无二的,但它不是一个规则,你只是得到第一场比赛。但是更新只会触及那些与条件实际匹配的条目。

Ealier MongoDB固定索引

如果没有得到支持,那么你基本上会回到指数位置&#34;那真的不那么可靠。通常情况下,您实际上需要阅读每个文档,并在更新之前确定数组中的内容。但至少假设&#34;平价&#34;指数位置到位的地方:

arrayFilters

制作更新声明,如:

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 });

当然注意到每个&#34;更新集&#34;除了首先阅读文档以确定要更新哪些索引之外,你真的没有其他选择。这通常不是一个好主意,因为除了在写入之前需要读取每个文档的开销之外,没有绝对保证数组本身在读取和写入之间的其他进程保持不变,因此使用&#34 ;硬索引&#34;正在假设一切都是一样的,而实际情况可能并非如此。

较早的MongoDB位置匹配

数据允许通常更好地循环标准positional matched $更新。这里{ "$set": { "samples.0.concentration": 3, "samples.0.other": "c", "samples.2.concentration": 4, "samples.2.other": "a" } } 确实是唯一的,因此它是一个很好的候选者,最重要的是你不需要阅读现有的文档来比较索引的数组:

location

bulkWrite()发送多个更新操作,但它只需一个请求和响应,就像任何其他更新操作一样。确实,如果您正在处理一个&#34;更改列表&#34;然后返回文档以进行比较,然后构建一个大的bulkWrite()是进入而不是单独写入的方向,实际上甚至也适用于所有先前的示例。

最大的区别是&#34;每个数组元素一个更新指令&#34;在变更集中。这是在没有&#34;位置过滤的情况下在发布中执行操作的安全方法。支持,即使这意味着更多的写操作。

示范

演示中的完整列表如下。注意我使用&#34; mongoose&#34;这里为了简单起见,但没有什么真正的&#34; mongoose具体&#34;关于实际更新本身。这同样适用于任何实现,特别是在这种情况下,使用Array.map()Array.reduce()处理列表进行构建的JavaScript示例。

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);

那些无法运行的人的输出显示匹配的数组元素已更新:

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()
  }

})()

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

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
}

输出:

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()
  }

})()