Поиск и группировка из двух полей в одной агрегации - PullRequest
0 голосов
/ 08 мая 2018

У меня есть агрегат, который выглядит следующим образом:

userSchema.statics.getCounts = function (req, type) {
  return this.aggregate([
    { $match: { organization: req.user.organization._id } },
    {
      $lookup: {
        from: 'tickets', localField: `${type}Tickets`, foreignField: '_id', as: `${type}_tickets`,
      },
    },
    { $unwind: `$${type}_tickets` },
    { $match: { [`${type}_tickets.createdAt`]: { $gte: new Date(moment().subtract(4, 'd').startOf('day').utc()), $lt: new Date(moment().endOf('day').utc()) } } },
    {
      $group: {
        _id: {
          groupDate: {
            $dateFromParts: {
              year: { $year: `$${type}_tickets.createdAt` },
              month: { $month: `$${type}_tickets.createdAt` },
              day: { $dayOfMonth: `$${type}_tickets.createdAt` },
            },
          },
          userId: `$${type}_tickets.assignee_id`,
        },
        ticketCount: {
          $sum: 1,
        },
      },
    },
    {
      $sort: { '_id.groupDate': -1 },
    },
    { $group: { _id: '$_id.userId', data: { $push: { groupDate: '$_id.groupDate', ticketCount: '$ticketCount' } } } },
  ]);
};

Который выводит данные как это:

[ 
  {
    _id: 5aeb6b71709f43359e0888bb,
    data: [ 
      { "groupDate": 2018-05-07T00:00:000Z", ticketCount: 4 }
  }
]

В идеале, я хотел бы иметь такие данные:

[ 
  {
    _id: 5aeb6b71709f43359e0888bb,
    data: [ 
      { "groupDate": 2018-05-07T00:00:000Z", assignedCount: 4, resolvedCount: 8 }
  }
]

Разница в том, что объект для пользователя будет выводить как общее количество назначенных билетов, так и общее количество разрешенных билетов для каждой даты.

Моя пользовательская схема выглядит следующим образом:

const userSchema = new Schema({
  firstName: String,
  lastName: String,
  assignedTickets: [
    {
      type: mongoose.Schema.ObjectId,
      ref: 'Ticket',
      index: true,
    },
  ],
  resolvedTickets: [
    {
      type: mongoose.Schema.ObjectId,
      ref: 'Ticket',
      index: true,
    },
  ],
}, {
  timestamps: true,
});

Пример документа пользователя выглядит так:

{
    "_id": "5aeb6b71709f43359e0888bb", 
    "assignedTickets": ["5aeb6ba7709f43359e0888bd", "5aeb6bf3709f43359e0888c2", "5aec7e0adcdd76b57af9e889"], 
    "resolvedTickets": ["5aeb6bc2709f43359e0888be", "5aeb6bc2709f43359e0888bf"], 
    "firstName": "Name", 
    "lastName": "Surname", 
}

Пример документа билета выглядит так:

{
    "_id": "5aeb6ba7709f43359e0888bd", 
    "ticket_id": 120292, 
    "type": "assigned", 
    "status": "Pending", 
    "assignee_email": "email@gmail.com", 
    "assignee_id": "5aeb6b71709f43359e0888bb", 
    "createdAt": "2018-05-02T20:05:59.147Z", 
    "updatedAt": "2018-05-03T20:05:59.147Z", 
}

Я пытался добавить несколько поисков и групповых этапов, но продолжаю получать пустой массив. Если я сделаю только один поиск и одну группу, я получу правильные значения для искомого поля, но я бы хотел, чтобы оба поля были в одном запросе. Можно ли иметь группу запросов в двух поисках?

Ответы [ 2 ]

0 голосов
/ 08 мая 2018

Короче говоря, вы, кажется, смирились с настройкой своих моделей в mongoose и перешли за рамки ссылок.На самом деле вы не должны хранить массивы в документах "User".На самом деле это «анти-паттерн», который изначально был просто мангустом, использовавшимся в качестве соглашения для хранения «ссылок» для популяции, где он не понимал, как вместо этого переводить ссылки, хранящиеся в «потомке», в «родителя»..

Фактически у вас есть эти данные в каждом "Ticket", и естественная форма $lookup состоит в том, чтобы использовать это "foreignField" в отношении подробностей из локальной коллекции.В этом случае "assignee_id" на билетах будет достаточно для просмотра соответствия обратно к "_id" из "User".Хотя вы этого и не заявите, ваш "status" должен быть индикатором того, действительно ли данные либо «назначены», как в состоянии «Ожидание», либо «разрешены», когда это не так.

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

По существу, тогдамы разрешаем одну $lookup операцию, фактически используя естественный «внешний ключ», в отличие от хранения отдельных массивов.

MongoDB 3.6 и выше

В идеале вы должны использовать функции MongoDB 3.6 с обработкой суб-конвейера здесь:

// Better date calculations
const oneDay = (1000 * 60 * 60 * 24);
var now = Date.now(),
    end = new Date((now - (now % oneDay)) + oneDay),
    start = new Date(end.valueOf() - (4 * oneDay));

User.aggregate([
  { "$match": { "organization": req.user.organization._id } },
  { "$lookup": {
    "from": Ticket.collection.name,
    "let": { "id": "$_id" },
    "pipeline": [
      { "$match": {
        "createdAt": { "$gte": start, "$lt": end },
        "$expr": {
          "$eq": [ "$$id", "$assignee_id" ]
        }
      }},
      { "$group": {
        "_id": {
          "status": "$status",
          "date": {
            "$dateFromParts": {
              "year": { "$year": "$createdAt" },
              "month": { "$month": "$createdAt" },
              "day": { "$dayOfMonth": "$createdAt" }
            }
          }
        },
        "count": { "$sum": 1 }
      }},
      { "$group": {
        "_id": "$_id.date",
        "data": {
          "$push": {
            "k": {
              "$cond": [
                { "$eq": ["$_id.status", "Pending"] },
                "assignedCount",
                "resolvedCount"
              ]
            },
            "v": "$count"
          }
        }
      }},
      { "$sort": { "_id": -1 } },
      { "$replaceRoot": {
        "newRoot": {
          "$mergeObjects": [
            { "groupDate": "$_id", "assignedCount": 0, "resolvedCount": 0 },
            { "$arrayToObject": "$data" }
          ]
        }
      }}
    ],
    "as": "data"
  }},
  { "$project": { "data": 1 } }
])

От MongoDB 3.0 и выше

Или, если вам не хватает этих функций, мы используем другой процесс конвейераи небольшое преобразование данных после того, как результаты будут возвращены с сервера:

User.aggregate([
  { "$match": { "organization": req.user.organization._id } },
  { "$lookup": {
    "from": Ticket.collection.name,
    "localField": "_id",
    "foreignField": "assignee_id",
    "as": "data"
  }},
  { "$unwind": "$data" },
  { "$match": {
    "data.createdAt": { "$gte": start, "$lt": end }
  }},
  { "$group": {
    "_id": { 
      "userId": "$_id",
      "date": {
        "$add": [
          { "$subtract": [
            { "$subtract": [ "$data.createdAt", new Date(0) ] },
            { "$mod": [
              { "$subtract": [ "$data.createdAt", new Date(0) ] },
              oneDay
            ]}
          ]},
          new Date(0)
        ]
      },
      "status": "$data.status"
    },
    "count": { "$sum": 1 }
  }},
  { "$group": {
    "_id": {
      "userId": "$_id.userId",
      "date": "$_id.date"
    },
    "data": {
      "$push": {
        "k": {
          "$cond": [
            { "$eq": [ "$_id.status", "Pending" ] },
            "assignedCount",
            "resolvedCount"
          ]
        },
        "v": "$count"
      }
    }
  }},
  { "$sort": { "_id.userId": 1, "_id.date": -1 } },
  { "$group": {
    "_id": "$_id.userId",
    "data": {
      "$push": {
        "groupDate": "$_id.date",
        "data": "$data"
      }
    }
  }}
])
.then( results => 
  results.map( ({ data, ...d }) => 
    ({
      ...d,
      data: data.map(di =>
        ({
          groupDate: di.groupDate,
          assignedCount: 0,
          resolvedCount: 0,
          ...di.data.reduce((acc,curr) => ({ ...acc, [curr.k]: curr.v }),{})
        })
      )
    })
  )
)

, что просто показывает, что даже с модными функциями в современныхаренда, вы действительно не нуждаетесь в них, потому что всегда были способы обойти это.Даже у частей JavaScript были только несколько более длинные версии, прежде чем был доступен текущий синтаксис «распространения объекта».

Так что это действительно то направление, в котором вам нужно идти. То, что вам определенно не нужно, это использовать «множественный»«$lookup этапов или даже применение $filter условий для потенциально больших массивов.Также обе формы здесь делают все возможное, чтобы «отфильтровать» количество элементов, «соединенных» из чужой коллекции, чтобы не нарушать ограничение BSON.

В частности, версия «pre 3.6» на самом деле имееттрюк, в котором $lookup + $unwind + $match происходит подряд, что вы можете увидеть в выводе объяснения.Все этапы фактически объединяются в «один» этап, который возвращает только те элементы, которые соответствуют условиям в $match из иностранной коллекции.Сохранение «раскручивания» до дальнейшего сокращения позволяет избежать проблем с ограничениями BSON, как это делает новая форма с MongoDB 3.6, где «под-конвейер» выполняет сокращение и группировку документов до того, как будут возвращены какие-либо результаты.

Ваш один образец документа будет возвращаться так:

    {
        "_id" : ObjectId("5aeb6b71709f43359e0888bb"),
        "data" : [
            {
                "groupDate" : ISODate("2018-05-02T00:00:00Z"),
                "assignedCount" : 1,
                "resolvedCount" : 0

            }
        ]
    }

Как только я расширю выбор даты, чтобы включить эту дату, что, конечно, выбор даты также может быть улучшен и исправлен из вашей исходной формы.

Так что, кажется, имеет смысл, что ваши отношения на самом деле определены таким образом, но вы просто записали их «дважды».Вам не нужно, и даже если это не определение, тогда вы должны вместо этого записывать в «потомок», а не в массив в родительском элементе.Мы можем манипулировать и объединять родительские массивы, но это контрпродуктивно для правильного установления отношений данных и их правильного использования.

0 голосов
/ 08 мая 2018

Как насчет этого?

db.users.aggregate([  
   {  
      $lookup:{ // lookup assigned tickets
         from:'tickets',
         localField:'assignedTickets',
         foreignField:'_id',
         as:'assigned',

      }
   },
   {  
      $lookup:{ // lookup resolved tickets 
         from:'tickets',
         localField:'resolvedTickets',
         foreignField:'_id',
         as:'resolved',

      }
   },
   {  
      $project:{  
         "tickets":{  // merge all tickets into one single array
            $concatArrays:[  
               "$assigned",
               "$resolved"
            ]
         }
      }
   },
   {  
      $unwind:'$tickets' // flatten the 'tickets' array into separate documents
   },
   {  
      $group:{ // group by 'createdAt' and 'assignee_id'
         _id:{  
            groupDate:{  
               $dateFromParts:{  
                  year:{ $year:'$tickets.createdAt' },
                  month:{ $month:'$tickets.createdAt' },
                  day:{ $dayOfMonth:'$tickets.createdAt' },

               },
            },
            userId:'$tickets.assignee_id',
         },
         assignedCount:{ // get the count of assigned tickets
            $sum:{  
               $cond:[  
                  {  // by checking the 'type' field for a value of 'assigned'
                     $eq:[  
                        '$tickets.type',
                        'assigned'
                     ]
                  },
                  1, // if matching count 1
                  0 // else 0
               ]
            }
         },
         resolvedCount:{  
            $sum:{  
               $cond:[  
                  {  // by checking the 'type' field for a value of 'resolved'
                     $eq:[  
                        '$tickets.type',
                        'resolved'
                     ]
                  },
                  1, // if matching count 1
                  0 // else 0
               ]
            }
         },
      },
   },
   {  
      $sort:{  // sort by 'groupDate' descending
         '_id.groupDate':-1
      },
   },
   {  
      $group:{  
         _id:'$_id.userId', // group again but only by userId
         data:{  
            $push:{  // create an array
               groupDate:'$_id.groupDate',
               assignedCount:{ 
                  $sum:'$assignedCount'
               },
               resolvedCount:{  
                  $sum:'$resolvedCount'
               }
            }
         }
      }
   }
])
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...