MongoDB 3.6. Позиционированные отфильтрованные обновления
Таким образом, вы на самом деле находитесь в правильном «подходе» с позиционным оператором all $[]
, но проблема в том, что это просто относится к "каждый элемент массива.Поскольку вам нужны «совпадающие» записи, вы фактически хотите вместо этого использовать оператор $[<identifier>]
1008 * с позиционной фильтрацией.
Как вы заметили, ваш "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
и создает список идентификаторов, соответствующих значениям 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()
здесь, а также "Объект распространения "... в строительстве.Если вы окажетесь в среде JavaScript без этой поддержки, то Object.keys()
и Object.assign()
в основном будут заменены с небольшим изменением.
Тогда они могутфактически применяется в обновлении, как в:
Project.update({ client_id: 'ClientA' }, { $set }, { arrayFilters });
Таким образом, позиционная фильтрация $[<identifier>]
фактически используется здесь для создания "совпадающих пар" записей в пределах $set
модификатор и в опции arrayFilters
update()
.Таким образом, для каждого "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
Там, где позволяют данные, обычно лучше циклически использовать стандартное позиционноевместо $
обновлений.Здесь 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);
A bulkWrite()
отправляет несколько операций обновления, но делает это с помощью одного запроса и ответа, как и любая другая операция обновления.В самом деле, если вы обрабатываете «список изменений», то возвращение документа для сравнения каждого из них, а затем построение одной большой bulkWrite()
- это направление, в которое нужно идти вместо отдельных записей, и это на самом деле даже относится ктакже все предыдущие примеры.
Большая разница - "одна инструкция обновления на элемент массива" в наборе изменений.Это безопасный способ делать вещи в выпусках без поддержки «позиционной фильтрации», даже если это означает больше операций записи.
Демонстрация
Ниже приведен полный список в демонстрации.Заметьте, я здесь для простоты использую mongoose, но в самих обновлениях нет ничего действительно специфичного для mongoose.То же самое относится к любой реализации, и, в частности, в данном случае примеры использования JavaScript Array.map()
и Array.reduce()
для обработки списка для построения.
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
}
И, конечно, со стандартным "позиционным" $
синтаксисом и обновлениями:
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
}