На самом деле сейчас нет времени на все объяснения (извините) ,
Объяснение
Основная проблема здесь заключается в том, что использование $unwind
- это ваша проблема, и она вам не нужна.Вместо этого используйте $map
для содержимого созданного массива, объединяющегося с массивом "decks"
.Тогда вы можете получить nulls
.
То, что вы хотите сделать здесь, это получить значения из $lookup
из вашей "tenants"
коллекции , транспонированные вСуществующий массив в вашей коллекции "beds/bedspaces"
для его собственных существующих значений "tenant"
, которые являются ObjectId
ссылками для внешней коллекции.
Этап $lookup
не может сделать это простоименование пути к полю в выводе "as"
, где этот путь уже находится внутри другого массива, и на самом деле вывод $lookup
равен всегда массиву результатов, полученных из стороннегоколлекция.Вам нужно единичные значения для каждого фактического соответствия, и, конечно, вы ожидаете, что null
будет на месте, где ничего не совпадает, и, конечно, сохранит исходный массив документов "decks"
без изменений, но только с учетомпосторонние сведения о том, где они были найдены.
Ваша попытка кода кажется частично осведомленной об этой точке, когда вы используете $unwind
на $lookup
результат для коллекции ""tenants"
во «временный массив» (но вы вставляете в существующий путь, который перезаписывает содержимое) и затем пытается «перегруппировать» как массив через $group
и $push
.Но проблема, конечно, в том, что результат $lookup
не применим к каждому элементу массива в "decks"
, поэтому вы получите меньше результатов, чем хотите.
Реальное решение -не "условное $lookup
" , а вместо этого транспонирование «временного массива» содержимого из результата в существующий "decks"
записей.Это делается с помощью $map
для обработки элементов массива и $arrayElemAt
вместе с $indexOfArray
для возврата соответствующих элементов из «временный массив» с помощью сопоставления _id
значений с "tenant"
.
{ "$lookup": {
"from": Tenant.collection.name,
"let": { "tenant": "$decks.tenant" },
"pipeline": [
{ "$match": {
"$expr": { "$in": [ "$_id", "$$tenant" ] }
}}
],
"as": "tenant"
}},
{ "$addFields": {
"decks": {
"$map": {
"input": "$decks",
"in": {
"$mergeObjects": [
"$$this",
{
"tenant": {
"$cond": {
"if": {
"$eq": [
{ "$indexOfArray": ["$tenant._id", "$$this.tenant"] },
-1
]
},
"then": null,
"else": {
"$arrayElemAt": [
"$tenant",
{ "$indexOfArray": ["$tenant._id", "$$this.tenant"]}
]
}
}
}
}
Заметим, что мы используем $mergeObjects
внутри $map
, чтобы сохранить существующее содержимое массива "decks"
и заменить (или «объединить») только перезаписанное представление "tenant"
для каждого элемента массива.Вы уже используете выразительный $lookup
, и, например, $mergeObjects
- это функция MongoDB 3.6.
Просто для интереса то же самое можно сделать, простоуказав каждое поле в массиве.то есть:
"decks": {
"$map": {
"input": "$decks",
"in": {
"_id": "$$this._id",
"number": "$$this.number",
"tenant": {
// same expression
},
"__v": "$$this.__v" // just because it's mongoose
}
}
}
То же самое можно сказать и о $$REMOVE
, используемом в $addFields
, который также является другой функцией MongoDB 3.6.Вы можете поочередно просто использовать $project
и просто опускать ненужные поля:
{ "$project": {
"number": "$number",
"decks": {
"$map": { /* same expression */ }
},
"__v": "$__v"
// note we don't use the "tenant" temporary array
}}
Но в принципе это работает.Взяв результат $lookup
и затем транспонируя эти результаты обратно в исходный массив в документе.
Пример списка
Также абстрагируясь отваши данные из предыдущих вопросов здесь, что немного лучше, чем то, что вы опубликовали в вопросе здесь.Список для запуска для демонстрации:
const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/hotel';
const opts = { useNewUrlParser: true };
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndexes', true);
mongoose.set('debug', true);
const tenantSchema = new Schema({
name: String,
age: Number
});
const deckSchema = new Schema({
number: Number,
tenant: { type: Schema.Types.ObjectId, ref: 'Tenant' }
});
const bedSchema = new Schema({
number: Number,
decks: [deckSchema]
});
const roomSchema = new Schema({
bedspaces: [{ type: Schema.Types.ObjectId, ref: 'Bed' }]
});
const Tenant = mongoose.model('Tenant', tenantSchema);
const Bed = mongoose.model('Bed', bedSchema);
const Room = mongoose.model('Room', roomSchema);
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri, opts);
// Clean data
await Promise.all(
Object.entries(conn.models).map(([k, m]) => m.deleteMany())
);
// Insert data
let [john, jane, bilbo ] = await Tenant.insertMany([
{
_id: ObjectId("5c964ae7f5097e3020d1926c"),
name: "john doe",
age: 11
},
{
_id: ObjectId("5c964b2531bc162fdce64f15"),
name: "jane doe",
age: 12
},
{
_id: ObjectId("5caa5454494558d863513b24"),
name: "bilbo",
age: 111
}
]);
let bedspaces = await Bed.insertMany([
{
_id: ObjectId("5c98d89c6bd5fc26a4c2851b"),
number: 1,
decks: [
{
number: 1,
tenant: john
},
{
number: 1,
tenant: jane
}
]
},
{
_id: ObjectId("5c98d89f6bd5fc26a4c28522"),
number: 2,
decks: [
{
number: 2,
tenant: bilbo
},
{
number: 3
}
]
}
]);
await Room.create({ bedspaces });
// Aggregate
let results = await Room.aggregate([
{ "$lookup": {
"from": Bed.collection.name,
"let": { "bedspaces": "$bedspaces" },
"pipeline": [
{ "$match": {
"$expr": { "$in": [ "$_id", "$$bedspaces" ] }
}},
{ "$lookup": {
"from": Tenant.collection.name,
"let": { "tenant": "$decks.tenant" },
"pipeline": [
{ "$match": {
"$expr": { "$in": [ "$_id", "$$tenant" ] }
}}
],
"as": "tenant"
}},
{ "$addFields": {
"decks": {
"$map": {
"input": "$decks",
"in": {
"$mergeObjects": [
"$$this",
{
"tenant": {
"$cond": {
"if": {
"$eq": [
{ "$indexOfArray": ["$tenant._id", "$$this.tenant"] },
-1
]
},
"then": null,
"else": {
"$arrayElemAt": [
"$tenant",
{ "$indexOfArray": ["$tenant._id", "$$this.tenant"]}
]
}
}
}
}
]
}
}
},
"tenant": "$$REMOVE"
}}
],
"as": "bedspaces"
}}
]);
log(results);
} catch (e) {
console.error(e)
} finally {
mongoose.disconnect();
}
})()
Возвращает:
Mongoose: tenants.deleteMany({}, {})
Mongoose: beds.deleteMany({}, {})
Mongoose: rooms.deleteMany({}, {})
Mongoose: tenants.insertMany([ { _id: 5c964ae7f5097e3020d1926c, name: 'john doe', age: 11, __v: 0 }, { _id: 5c964b2531bc162fdce64f15, name: 'jane doe', age: 12, __v: 0 }, { _id: 5caa5454494558d863513b24, name: 'bilbo', age: 111, __v: 0 } ], {})
Mongoose: beds.insertMany([ { _id: 5c98d89c6bd5fc26a4c2851b, number: 1, decks: [ { _id: 5caa5af6ed3dce1c3ed72cef, number: 1, tenant: 5c964ae7f5097e3020d1926c }, { _id: 5caa5af6ed3dce1c3ed72cee, number: 1, tenant: 5c964b2531bc162fdce64f15 } ], __v: 0 }, { _id: 5c98d89f6bd5fc26a4c28522, number: 2, decks: [ { _id: 5caa5af6ed3dce1c3ed72cf2, number: 2, tenant: 5caa5454494558d863513b24 }, { _id: 5caa5af6ed3dce1c3ed72cf1, number: 3 } ], __v: 0 } ], {})
Mongoose: rooms.insertOne({ bedspaces: [ ObjectId("5c98d89c6bd5fc26a4c2851b"), ObjectId("5c98d89f6bd5fc26a4c28522") ], _id: ObjectId("5caa5af6ed3dce1c3ed72cf3"), __v: 0 })
Mongoose: rooms.aggregate([ { '$lookup': { from: 'beds', let: { bedspaces: '$bedspaces' }, pipeline: [ { '$match': { '$expr': { '$in': [ '$_id', '$$bedspaces' ] } } }, { '$lookup': { from: 'tenants', let: { tenant: '$decks.tenant' }, pipeline: [ { '$match': { '$expr': { '$in': [ '$_id', '$$tenant' ] } } } ], as: 'tenant' } }, { '$addFields': { decks: { '$map': { input: '$decks', in: { '$mergeObjects': [ '$$this', { tenant: [Object] } ] } } }, tenant: '$$REMOVE' } } ], as: 'bedspaces' } } ], {})
[
{
"_id": "5caa5af6ed3dce1c3ed72cf3",
"bedspaces": [
{
"_id": "5c98d89c6bd5fc26a4c2851b",
"number": 1,
"decks": [
{
"_id": "5caa5af6ed3dce1c3ed72cef",
"number": 1,
"tenant": {
"_id": "5c964ae7f5097e3020d1926c",
"name": "john doe",
"age": 11,
"__v": 0
}
},
{
"_id": "5caa5af6ed3dce1c3ed72cee",
"number": 1,
"tenant": {
"_id": "5c964b2531bc162fdce64f15",
"name": "jane doe",
"age": 12,
"__v": 0
}
}
],
"__v": 0
},
{
"_id": "5c98d89f6bd5fc26a4c28522",
"number": 2,
"decks": [
{
"_id": "5caa5af6ed3dce1c3ed72cf2",
"number": 2,
"tenant": {
"_id": "5caa5454494558d863513b24",
"name": "bilbo",
"age": 111,
"__v": 0
}
},
{
"_id": "5caa5af6ed3dce1c3ed72cf1",
"number": 3,
"tenant": null
}
],
"__v": 0
}
],
"__v": 0
}
]
Показывает null
для второй записи второй записи в массиве bedspaces
, как и ожидалось.