Виртуальное заполнение и агрегаты Mongoose

Я пытаюсь выполнить агрегирование схем Mongoose, в которых используется новая функция виртуального заполнения (с использованием Mongoose 4.13, Mongo 3.6).

Допустим, у меня есть следующие (упрощенные для целей иллюстрации) схемы:

const ProjectSchema = new mongoose.Schema({
  projectId: Number,
  description: String
});

ProjectSchema.virtual('tasks', {
  ref: 'Task',
  localField: 'projectId',
  foreignField: 'projectId' 
  justOne: false
]);

const TaskSchema = new mongoose.Schema({
  taskId: Number,
  projectId: Number
  hours: Number
});

const Project = mongoose.model('Project', ProjectSchema);
const Task = mongoose.model('Task', TaskSchema);

Запросы в Project и заполнение связанных задач отлично работают с использованием .populate (), например:

Project.find({projectId: <id>}).populate('tasks');

Но теперь я хотел бы суммировать часы по задачам по проектам (оставил часть $ sum ниже, кстати ...). Я получаю обратно только пустые массивы, несмотря ни на что. Разве нельзя агрегировать с виртуальным заполнением или?

const result = await Project.aggregate([
   { $match : { projectId: <id> } },
   { $lookup: { 
       from: 'tasks',
       localField: 'projectId',
       foreignField: 'projectId',
       as: 'tasks'
     },
     {
        $unwind: '$tasks' 
     }
   }
 ]);

person Ramon    schedule 06.12.2017    source источник


Ответы (3)


Ссылка на документацию mongoose для Model.aggregate:

Аргументы не приводятся в схему модели, потому что операторы $ project позволяют переопределить «форму» документов на любой стадии конвейера, что может привести к тому, что документы окажутся в несовместимом формате.

По сути, это означает, что магия всего, что позволяет мангуст с помощью схемы, не применяется при использовании агрегатов. Фактически, вам нужно будет напрямую обратиться к документации MongoDB для агрегирования (в частности, $ lookup).

const ProjectSchema = new mongoose.Schema({
  projectId: Number,
  description: String
});

const TaskSchema = new mongoose.Schema({
  taskId: Number,
  projectId: Number,
  hours: Number
});

const Project = connection.model('Project', ProjectSchema);
const Task = connection.model('Task', TaskSchema);

const project1 = new Project({ projectId: 1, description: 'Foo'});
const project2 = new Project({ projectId: 2, description: 'Foo'});
const task1 = new Task({ task: 1, projectId: 1, hours: 1});
const task2 = new Task({ task: 2, projectId: 1, hours: 2});
const task3 = new Task({ task: 3, projectId: 2, hours: 1});

Promise.all([
  project1.save(),
  project2.save(),
  task1.save(),
  task2.save(),
  task3.save()
]).then(() => {
  return Project.aggregate([
    { $match: { projectId: 1 }},
    {
      $lookup: {
        from: 'tasks',
        localField: 'projectId',
        foreignField: 'projectId',
        as: 'tasks'
      }
    }
  ]).exec();
}).then((result) => {
  console.dir(result, { depth: null, color: true });
});

Выводит следующее:

[{
  _id: ObjectID {/*ID*/},
  projectId: 1,
  description: 'Foo',
  __v: 0,
  tasks: [{
      _id: ObjectID {/*ID*/},
      projectId: 1,
      hours: 2,
      __v: 0
    },
    {
      _id: ObjectID {/*ID*/},
      projectId: 1,
      hours: 1,
      __v: 0
    }
  ]
}]

Но вы можете спросить: «Это в основном то, что у меня есть, так почему это работает ?!»

Я не уверен, был ли ваш примерный код скопирован / вставлен, но в приведенном примере этап $unwind, похоже, был случайно включен во второй этап ($lookup) вашего конвейера. Если бы вы добавили его как 3-й этап, как показывает следующий код, то выходные данные были бы изменены, как показано.

return Project.aggregate([
  { $match: { projectId: 1 }},
  {
    $lookup: {
      from: 'tasks',
      localField: 'projectId',
      foreignField: 'projectId',
      as: 'tasks'
    }
  },
  { $unwind: '$tasks' }
]).exec();

Выходы:

[{
    _id: ObjectID {/*ID*/},
    projectId: 1,
    description: 'Foo',
    __v: 0,
    tasks: {
      _id: ObjectID {/*ID*/},
      projectId: 1,
      hours: 1,
      __v: 0
    }
  },
  {

    _id: ObjectID {/*ID*/},
    projectId: 1,
    description: 'Foo',
    __v: 0,
    tasks: {
      _id: ObjectID {/*ID*/},
      projectId: 1,
      hours: 2,
      __v: 0
    }
  }
]
person Jason Cust    schedule 15.12.2017
comment
Спасибо, очень признателен. Я нашел проблему, с которой столкнулся. Я использую определенные пользователем имена коллекций в своих схемах, чтобы их было легче различать. Поле 'from' в агрегации $ lookup указывает имя коллекции, я пытался сослаться на имя схемы, которое в моем случае не то же самое. Теперь все работает нормально! - person Ramon; 21.12.2017

Виртуальное заполнение не будет работать с агрегатом, потому что «возвращенные документы представляют собой простые объекты javascript, а не документы мангуста (поскольку может быть возвращена любая форма документа)» (c), дополнительная информация в документации - Агрегат Mongoose

person vasylOk    schedule 06.12.2017
comment
Хм .. Я задал этот вопрос, где упоминается для использования $ lookup с виртуальными заполнениями. Есть мысли по этому поводу? - person Ramon; 06.12.2017

mongoose> = 3.6 поддерживает метод Model.populate ().

Итак, вы можете передать result агрегированного метода методу populate следующим образом.

var opts = [{ path: 'tasks'}];
const populatedResult = await Project.populate(result, opts);
person ruwan800    schedule 12.11.2019