Просто немного расширив ваш пример, чтобы включить «возможный» случай, когда ваш массив запросов фактически запрашивает нескольких пользователей из одной компании.
Необходимым условием запроса является просто переназначение входного массива и его использование в аргументе запроса $or
. который выглядит как:
{
"$or": [
{
"companyID": "6",
"accounts.userID": "3"
},
{
"companyID": "6",
"accounts.userID": "2"
},
{
"companyID": "4",
"accounts.userID": "1"
}
]
}
Это будет соответствовать «документам», но соответствующая «пользовательская» информация содержится в массиве "accounts"
. Чтобы извлечь только эти элементы, нам нужно применить условие $filter
, чтобы просто сохранить эти записи массива в соответствии с критериями. Тогда на самом деле нужно просто использовать $unwind
для оставшегося содержимого массива и немного изменить форму документа, чтобы преобразовать его в желаемый формат вывода с помощью $project
.
Весь сгенерированный оператор будет выглядеть так:
Company.aggregate([
{ '$match': {
'$or': [
{ companyID: '6', 'accounts.userID': '3' },
{ companyID: '6', 'accounts.userID': '2' },
{ companyID: '4', 'accounts.userID': '1' }
]
}},
{ '$addFields': {
accounts: {
'$filter': {
input: '$accounts',
cond: {
'$or': [
{ '$and': [
{ '$eq': [ '$companyID', '6' ] },
{ '$eq': [ '$$this.userID', '3' ] }
] },
{ '$and': [
{ '$eq': [ '$companyID', '6' ] },
{ '$eq': [ '$$this.userID', '2' ] }
] },
{ '$and': [
{ '$eq': [ '$companyID', '4' ] },
{ '$eq': [ '$$this.userID', '1' ] }
] }
]
}
}
}
}},
{ '$unwind': '$accounts' },
{ '$project': {
userID: '$accounts.userID',
companyID: 1,
preferences: '$accounts.preferences'
}}
])
Содержимое $or
для запроса и дополнительная форма $or
для $filter
в основном генерируются из ввода массив так:
let query = {
$or: input.map(({ userID, companyID }) =>
({ companyID, 'accounts.userID': userID }))
};
let condition = input.map(({ userID, companyID }) =>
({ "$and": [
{ "$eq": ["$companyID", companyID] },
{ "$eq": ["$$this.userID", userID] }
]})
);
И затем используется в качестве аргументов в остальной части конструкции конвейера, которая в основном является статической. Обратите внимание, что использование в $filter
как "cond"
требует «логических операторов агрегирования», которые возвращают логическое значение на основе того, что они тестируют. Таким образом, они отличаются от операторов запроса в форме функции.
То же самое «спаривание» применяется к каждому условию $or
, поэтому при поиске совпадения с этой комбинацией учитываются как значение "companyID"
, так и текущее значение "userID"
в массиве счетов. В пределах $filter
важно проверить "companyID"
снаружи массива, одновременно проверяя текущий элемент массива.
Причина, по которой мы не можем сделать это со стандартным позиционным $
оператором проекции, по существу, связана с условием $or
в запросе. Существует дополнительное ограничение «множественных совпадений», добавленное здесь для демонстрации, но из-за $or
в запросе MongoDB не может определить, какой из «набора условий» фактически удовлетворяет позиции совпадения элемента для любого в любом случае отдельный документ.
Так что на самом деле не имеет значения, хотите ли вы, чтобы «один» пользователь соответствовал каждой компании или «многим», так как один и тот же фильтр агрегации должен применяться для извлечения правильных сведений о «пользователях» в любом случае.
Ниже приведен полный список для демонстрации:
const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost/test';
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
let input = [
{
"userID": 3,
"companyID": 6
},
{
"userID": 2,
"companyID": 6
},
{
"userID": 1,
"companyID": 4
}
];
// non-strict for testing
const Company = mongoose.model('Company', new Schema({},{ strict: false }));
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri);
// Clean data to actually be matching strings
input = input.map(({ userID, companyID }) =>
({ userID: userID.toString(), companyID: companyID.toString() }));
let query = {
$or: input.map(({ userID, companyID }) =>
({ companyID, 'accounts.userID': userID }))
};
log(query);
let condition = input.map(({ userID, companyID }) =>
({ "$and": [
{ "$eq": ["$companyID", companyID] },
{ "$eq": ["$$this.userID", userID] }
]})
);
log(condition);
let result = await Company.aggregate([
{ "$match": query },
{ "$addFields": {
"accounts": {
"$filter": {
"input": "$accounts",
"cond": { "$or": condition }
}
}
}},
{ "$unwind": "$accounts" },
{ "$project": {
"userID": "$accounts.userID",
"companyID": 1,
"preferences": "$accounts.preferences"
}}
]);
log(result);
mongoose.disconnect();
} catch(e) {
console.error(e)
} finally {
process.exit()
}
})()
И это дает вывод как:
[
{
"_id": "5aeaf7c73c3e9de82d91e439",
"companyID": "4",
"userID": "1",
"preferences": [
{
"emailNotification": true,
"smsNotification": true,
"pushNotification": false,
"webNotification": false,
"lastUpdatedBy": "SYSTEM",
"_id": "5aeaf7c720262a1db759edf7",
"preferenceID": "6fbd6c-4c56-11e8-842f-0ed5f89f718b",
"createdAt": "2018-05-03T11:51:35.509Z",
"updatedAt": "2018-05-03T11:51:35.509Z"
},
{
"emailNotification": true,
"smsNotification": true,
"pushNotification": false,
"webNotification": false,
"lastUpdatedBy": "SYSTEM",
"_id": "5aeaf7c720262a1db759edf6",
"preferenceID": "6fb118-4c56-11e8-842f-0ed5f89f718b",
"createdAt": "2018-05-03T11:51:35.509Z",
"updatedAt": "2018-05-03T11:51:35.509Z"
}
]
},
{
"_id": "5aeafe6d3c3e9de82d91e43b",
"companyID": "6",
"userID": "2",
"preferences": [
{
"emailNotification": true,
"smsNotification": true,
"pushNotification": false,
"webNotification": false,
"lastUpdatedBy": "SYSTEM",
"_id": "5aeafe738b1d5f2057419ca1",
"preferenceID": "6fbd6c-4c56-11e8-842f-0ed5f89f718b",
"createdAt": "2018-05-03T12:20:03.987Z",
"updatedAt": "2018-05-03T12:20:03.987Z"
},
{
"emailNotification": true,
"smsNotification": true,
"pushNotification": false,
"webNotification": false,
"lastUpdatedBy": "SYSTEM",
"_id": "5aeafe738b1d5f2057419ca0",
"preferenceID": "6fb118-4c56-11e8-842f-0ed5f89f718b",
"createdAt": "2018-05-03T12:20:03.987Z",
"updatedAt": "2018-05-03T12:20:03.987Z"
}
]
},
{
"_id": "5aeafe6d3c3e9de82d91e43b",
"companyID": "6",
"userID": "3",
"preferences": [
{
"emailNotification": true,
"smsNotification": true,
"pushNotification": false,
"webNotification": false,
"lastUpdatedBy": "SYSTEM",
"_id": "5aeafe778b1d5f2057419ca4",
"preferenceID": "6fbd6c-4c56-11e8-842f-0ed5f89f718b",
"createdAt": "2018-05-03T12:20:07.062Z",
"updatedAt": "2018-05-03T12:20:07.062Z"
},
{
"emailNotification": true,
"smsNotification": true,
"pushNotification": false,
"webNotification": false,
"lastUpdatedBy": "SYSTEM",
"_id": "5aeafe778b1d5f2057419ca3",
"preferenceID": "6fb118-4c56-11e8-842f-0ed5f89f718b",
"createdAt": "2018-05-03T12:20:07.062Z",
"updatedAt": "2018-05-03T12:20:07.062Z"
}
]
}
]
Что показывает, что мы вернули из документов только соответствующие компании и пользовательские комбинации.
ПРИМЕЧАНИЕ Не уверен, что при введенной вами выборке было "преднамеренное" , что значения даны как "числовые", а не как "строки", где, конечно, они на самом деле "строки" в данных, которым он должен соответствовать. Существует простая строка кода, которая преобразует числовые значения в строки, что, разумеется, не является необходимым, если и ваши входные типы, и сохраненные типы уже совпадают.
Кроме того, хотя mongoose обычно "приводил" эти значения при обычных операциях запроса к тому, что было в схеме, это не происходит с конвейерами агрегации. Любые условия, которые вы применяете для сопоставления в операциях конвейера агрегации, требуют "вы" для приведения значений к правильному типу самостоятельно.
Mongoose не делает этого, потому что он не может сделать «предположение» на этапе конвейерного агрегирования, что данные, представленные в то время, находятся в том же состоянии, о котором знает схема. Операции агрегации обычно связаны с «переформированием документов», и по этой причине «схема» здесь не применяется.