MongoDB C# получить последний документ из группы


У меня есть группа статусов притворных платежей, каждый с идентификатором платежа.

Я хочу получить последний статус для каждого идентификатора платежа. Тест, который я провел, создает некоторые фиктивные данные, а затем пытается запросить их. У меня есть это далеко:

[Test]
public void GetPaymentLatestStatuses()
{
    var client = new TestMongoClient();

    var database = client.GetDatabase("payments");

    var paymentRequestsCollection = database.GetCollection<BsonDocument>("paymentRequests");

    var statusesCollection = database.GetCollection<BsonDocument>("statuses");

    var payment = new BsonDocument { { "amount", RANDOM.Next(10) } };

    paymentRequestsCollection.InsertOne(payment);

    var paymentId = payment["_id"];

    var receivedStatus = new BsonDocument
                         {
                             { "payment", paymentId },
                             { "code", "received" },
                             { "date", DateTime.UtcNow }
                         };
    var acceptedStatus = new BsonDocument
                         {
                             { "payment", paymentId },
                             { "code", "accepted" },
                             { "date", DateTime.UtcNow.AddSeconds(-1) }
                         };
    var completedStatus = new BsonDocument
                          {
                              { "payment", paymentId },
                              { "code", "completed" },
                              { "date", DateTime.UtcNow.AddSeconds(-2) }
                          };

    statusesCollection.InsertMany(new [] { receivedStatus, acceptedStatus, completedStatus });

    var groupByPayments = new BsonDocument { {"_id", "$payment"} };

    var statuses = statusesCollection.Aggregate().Group(groupByPayments);

}
Но теперь я стою у кирпичной стены. Любое тычение в нужном направлении поможет. Я не уверен, что не смотрю не в ту сторону телескопа.

Обновление

Следующее дает мне идентификаторы правильного документы.

var groupByPayments = new BsonDocument
                      {
                          { "_id", "$payment" },
                          { "id", new BsonDocument { { "$first", "$_id" } } }
                      };

var sort = Builders<BsonDocument>.Sort.Descending(document => document["date"]);

var statuses = statusesCollection.Aggregate().Sort(sort).Group(groupByPayments).ToList();

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

3 3

3 ответа:

Давайте начнем с простого способа получить то, чего вы пытаетесь достичь. В драйвере C# 2.X из MongoDB вы можете найти метод расширения AsQueryable, который позволяет вам создавать запросы LINQ из ваших коллекций. Этот поставщик Linq был построен на платформе агрегации MongoDB, поэтому в конце ваш запрос ссылки будет переведен в конвейер агрегации. Итак, если у вас есть такой класс:

public class Status
{
  public ObjectId _id { get; set; }
  public ObjectId payment { get; set; }
  public string code { get; set; }
  public DateTime date { get; set; }
}

Можно создать запрос следующего вида:

 var statusesCollection = database.GetCollection<Status>("statuses");
 var result= statusesCollection.AsQueryable()
                               .OrderByDescending(e=>e.date)
                               .GroupBy(e=>e.payment)
                               .Select(g=>new Status{_id =g.First()._id,
                                                     payment = g.Key,
                                                     code=g.First().code,
                                                     date=g.First().date
                                                    }
                                       )
                               .ToList();

Теперь вы можете удивляться почему я должен был проецировать результат на новый экземпляр класса Status, Если я мог получить тот же результат, вызывая метод расширения First из каждой группы? К сожалению, это пока не поддерживается. Одна из причин заключается в том, что поставщик Linq использует операцию $first при построении конвейера агрегации, и именно так работает операция $first. Кроме того, как вы можете видеть в ссылке a shared ранее, когда вы используете $first в стадии $group, стадия $group должна следовать за стадией $sort, чтобы иметь ввод документов в определенном порядке.


Теперь, предположив, что вы не хотите использовать Linq и хотите работать над созданием конвейера агрегации самостоятельно, вы можете сделать следующее:

 var groupByPayments = new BsonDocument
                      {
                          { "_id", "$payment" },
                          { "statusId", new BsonDocument { { "$first", "$_id" } } },
                          { "code", new BsonDocument { { "$first", "$code" } } },
                          { "date", new BsonDocument { { "$first", "$date" } } }
                      };

var sort = Builders<BsonDocument>.Sort.Descending(document => document["date"]);

ProjectionDefinition<BsonDocument> projection = new BsonDocument
        {
            {"payment", "$_id"},
            {"id", "$statusId"},
            {"code", "$code"},
            {"date", "$date"},
        }; 
var statuses = statusesCollection.Aggregate().Sort(sort).Group(groupByPayments).Project(projection).ToList<BsonDocument>();

Преимущество этого решения заключается в том, что вы получаете данные в один круг, а недостаток заключается в том, что вам нужно спроецировать все поля, которые вы need.My вывод был бы, если документ не имеет много полей или вам не нужны все поля из вашего документа, я бы использовал этот вариант.

Это, как я достиг его. Но должен быть и лучший способ.

[Test]
public void GetPaymentLatestStatuses()
{
    var client = new TestMongoClient();

    var database = client.GetDatabase("payments");

    var paymentRequestsCollection = database.GetCollection<BsonDocument>("paymentRequests");

    var statusesCollection = database.GetCollection<BsonDocument>("statuses");

    var payment = new BsonDocument { { "amount", RANDOM.Next(10) } };

    paymentRequestsCollection.InsertOne(payment);

    var paymentId = payment["_id"];

    var receivedStatus = new BsonDocument
                         {
                             { "payment", paymentId },
                             { "code", "received" },
                             { "date", DateTime.UtcNow }
                         };
    var acceptedStatus = new BsonDocument
                         {
                             { "payment", paymentId },
                             { "code", "accepted" },
                             { "date", DateTime.UtcNow.AddSeconds(+1) }
                         };
    var completedStatus = new BsonDocument
                          {
                              { "payment", paymentId },
                              { "code", "completed" },
                              { "date", DateTime.UtcNow.AddSeconds(+2) }
                          };

    statusesCollection.InsertMany(new[] { receivedStatus, acceptedStatus, completedStatus });

    var groupByPayments = new BsonDocument
                          {
                              { "_id", "$payment" },
                              { "id", new BsonDocument { { "$first", "$_id" } } }
                          };

    var sort = Builders<BsonDocument>.Sort.Descending(document => document["date"]);

    var statuses = statusesCollection.Aggregate().Sort(sort).Group(groupByPayments).ToList();

    var statusIds = statuses.Select(x => x["id"]);

    var completedStatusDocumentsFilter =
        Builders<BsonDocument>.Filter.Where(document => statusIds.Contains(document["_id"]));

    var statusDocuments = statusesCollection.Find(completedStatusDocumentsFilter).ToList();

    foreach (var status in statusDocuments)
    {
        Assert.That(status["code"].AsString, Is.EqualTo("completed"));
    }
}
Хотя должен быть и лучший способ.

Начиная с 2.5.3, вы можете получить доступ к текущей группе внутри агрегации. Это позволяет построить универсальный метод доступа, который будет извлекать первый элемент из группировки с помощью собственного запроса mongo.

Во-первых, вспомогательный класс для десериализации. KeyValuePair<TKey,TValue> запечатан, поэтому мы сворачиваем наши собственные.

    /// <summary>
    /// Mongo-ified version of <see cref="KeyValuePair{TKey, TValue}"/>
    /// </summary>
    class InternalKeyValuePair<T, TKey>
    {
        [BsonId]
        public TKey Key { get; set; } 
        public T Value { get; set; }
    }
    //you may not need this method to be completely generic, 
    //but have the sortkey be the same helps
    interface IDateModified
    {
        DateTime DateAdded { get; set; }
    }
    private List<T> GroupFromMongo<T,TKey>(string KeyName) where T : IDateModified
    {
        //mongo linq driver doesn't support this syntax, so we make our own bsondocument. With blackjack. And Hookers. 
        BsonDocument groupDoc = MongoDB.Bson.BsonDocument.Parse(@"
         {
                _id: '$" + KeyName + @"',
                Value: { '$first': '$$CURRENT' }
        }");
        //you could use the same bsondocument parsing trick to get a generic 
        //sorting key as well as a generic grouping key, or you could use
        //expressions and lambdas and make it...perfect.
        SortDefinition<T> sort = Builders<T>.Sort.Descending(document => document.DateAdded);
        List<BsonDocument> intermediateResult = getCol<T>().Aggregate().Sort(sort).Group(groupDoc).ToList();
        InternalResult<T, TKey>[] list = intermediateResult.Select(r => MongoDB.Bson.Serialization.BsonSerializer.Deserialize<InternalResult<T, TKey>>(r)).ToArray();
        return list.Select(z => z.Value).ToList();
    }

Хорошо..Я обобщил его с некоторой помощью от https://stackoverflow.com/a/672212/346272

    /// <summary>
    /// Mongo-ified version of <see cref="KeyValuePair{TKey, TValue}"/>
    /// </summary>
    class MongoKeyValuePair<T, TKey>
    {
        [BsonId]
        public TKey Key { get; set; }
        public T Value { get; set; }
    }
    private MongoKeyValuePair<T, TKey>[] GroupFromMongo<T, TKey>(Expression<Func<T, TKey>> KeySelector, Expression<Func<T, object>> SortSelector)
    {
        //mongo linq driver doesn't support this syntax, so we make our own bsondocument. With blackjack. And Hookers. 
        BsonDocument groupDoc = MongoDB.Bson.BsonDocument.Parse(@"
         {
                _id: '$" + GetPropertyName(KeySelector) + @"',
                Value: { '$first': '$$CURRENT' }
        }");
        SortDefinition<T> sort = Builders<T>.Sort.Descending(SortSelector);
        List<BsonDocument> groupedResult = getCol<T>().Aggregate().Sort(sort).Group(groupDoc).ToList();
        MongoKeyValuePair<T, TKey>[] deserializedGroupedResult = groupedResult.Select(r => MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoKeyValuePair<T, TKey>>(r)).ToArray();
        return deserializedGroupedResult;
    }

    /* This was my original non-generic method with hardcoded strings, PhonesDocument is an abstract class with many implementations */
    public List<T> ListPhoneDocNames<T>() where T : PhonesDocument
    {
        return GroupFromMongo<T,String>(z=>z.FileName,z=>z.DateAdded).Select(z=>z.Value).ToList();
    }


    public string GetPropertyName<TSource, TProperty>(Expression<Func<TSource, TProperty>> propertyLambda)
    {
        Type type = typeof(TSource);

        MemberExpression member = propertyLambda.Body as MemberExpression;
        if (member == null)
            throw new ArgumentException(string.Format(
                "Expression '{0}' refers to a method, not a property.",
                propertyLambda.ToString()));

        PropertyInfo propInfo = member.Member as PropertyInfo;
        if (propInfo == null)
            throw new ArgumentException(string.Format(
                "Expression '{0}' refers to a field, not a property.",
                propertyLambda.ToString()));

        if (type != propInfo.ReflectedType &&
            !type.IsSubclassOf(propInfo.ReflectedType))
            throw new ArgumentException(string.Format(
                "Expresion '{0}' refers to a property that is not from type {1}.",
                propertyLambda.ToString(),
                type));

        return propInfo.Name;
    }

Для получения бонусных очков теперь вы можете легко выполнять любые другие операции группировки mongos, не сражаясь с помощниками linq. См. https://docs.mongodb.com/manual/reference/operator/aggregation/group/ для всех доступных операций группировки. Давайте добавим счет.

    class MongoKeyValuePair<T, TKey>
    {
        [BsonId]
        public TKey Key { get; set; }
        public T Value { get; set; }
        public long Count { get; set; }
    }

        BsonDocument groupDoc = MongoDB.Bson.BsonDocument.Parse(@"
         {
                _id: '$" + GetPropertyName(KeySelector) + @"',
                Value: { '$first': '$$CURRENT' },
                Count: { $sum: 1 }
        }");

Выполните агрегацию точно так же, как и раньше, и Ваше свойство count будет заполнено количеством документов, соответствующих вашему ключу groupkey. Аккуратно!