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 ответа:
Давайте начнем с простого способа получить то, чего вы пытаетесь достичь. В драйвере 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. Аккуратно!