MongoDB C# 从组中获取最新文档
MongoDB C# Get latest document from group
我有一组模拟付款状态,每个状态都有一个付款ID。
我想获取每个付款 ID 的最新状态。我的测试创建了一些虚拟数据,然后尝试查询它。我已经走到这一步了:
[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);
}
但现在我在一堵砖墙前。
任何朝着正确方向的探索都会有所帮助。我不确定我是不是看错了 telescope.
更新
下面给出了正确文档的 ID。
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();
我可以通过单个查询获取完整的文档吗,还是我现在必须re-issue一个命令来获取该列表中的所有文档?
我就是这样实现的。不过必须有更好的方法。
[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"));
}
}
让我们从实现您想要实现的目标的简单方法开始。在 MongoDB 的 C# 驱动程序 2.X 中,您可以找到 AsQueryable
扩展方法,让您可以从集合中创建 LINQ 查询。此 Linq 提供程序是在 MongoDB 的聚合框架之上构建的,因此最后您的 link 查询将被转换为聚合管道。所以,如果你有这样的 class:
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
class 的新实例,如果我可以从每个组调用 First
扩展方法得到相同的结果?不幸的是,目前还不支持。原因之一是因为 Linq 提供程序在构建聚合管道时使用 $first 操作,这就是 $first
操作的工作方式。此外,正如您在之前分享的 link 中看到的那样,当您在 $group
阶段使用 $first
时,$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 的结论将是字段或者您不需要文档中的所有字段我会使用此变体。
There has to be a better way though.
从 2.5.3 开始,您可以访问聚合内部的当前组。这让我们可以构建一个通用访问器,它将通过本机 mongo 查询从分组中检索第一个元素。
首先,一个用于反序列化的助手 class。 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();
}
好的..我在
的帮助下将其通用化
/// <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;
}
为了加分,您现在可以轻松地执行 mongo 的任何其他分组操作,而无需与 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 }
}");
运行 聚合与之前完全相同,您的计数 属性 将填充与您的 groupkey 匹配的文档数量。整洁!
在已接受的答案的基础上,有时您需要指定一个无法使用 Linq Queriable 接口表达的过滤器,但您也不想诉诸手写 BSON 然后还必须强制转换BSON 回到你的对象。您可以将这两个示例结合起来,两全其美。仍然希望你可以 return g.First 但这也有效。
var filter = Builders<Status>.Filter.GeoWithinCenterSphere(x => x.LongLatField, longitude, latitude, radians);
var res = await statusesCollection.Aggregate().Match(filter).Group(x => x.PersistantId,
g=>new Status{_id =g.First()._id,
payment = g.Key,
code=g.First().code,
date=g.First().date
}
))
.ToListAsync();
我有一组模拟付款状态,每个状态都有一个付款ID。
我想获取每个付款 ID 的最新状态。我的测试创建了一些虚拟数据,然后尝试查询它。我已经走到这一步了:
[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);
}
但现在我在一堵砖墙前。
任何朝着正确方向的探索都会有所帮助。我不确定我是不是看错了 telescope.
更新
下面给出了正确文档的 ID。
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();
我可以通过单个查询获取完整的文档吗,还是我现在必须re-issue一个命令来获取该列表中的所有文档?
我就是这样实现的。不过必须有更好的方法。
[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"));
}
}
让我们从实现您想要实现的目标的简单方法开始。在 MongoDB 的 C# 驱动程序 2.X 中,您可以找到 AsQueryable
扩展方法,让您可以从集合中创建 LINQ 查询。此 Linq 提供程序是在 MongoDB 的聚合框架之上构建的,因此最后您的 link 查询将被转换为聚合管道。所以,如果你有这样的 class:
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
class 的新实例,如果我可以从每个组调用 First
扩展方法得到相同的结果?不幸的是,目前还不支持。原因之一是因为 Linq 提供程序在构建聚合管道时使用 $first 操作,这就是 $first
操作的工作方式。此外,正如您在之前分享的 link 中看到的那样,当您在 $group
阶段使用 $first
时,$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 的结论将是字段或者您不需要文档中的所有字段我会使用此变体。
There has to be a better way though.
从 2.5.3 开始,您可以访问聚合内部的当前组。这让我们可以构建一个通用访问器,它将通过本机 mongo 查询从分组中检索第一个元素。
首先,一个用于反序列化的助手 class。 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();
}
好的..我在
的帮助下将其通用化 /// <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;
}
为了加分,您现在可以轻松地执行 mongo 的任何其他分组操作,而无需与 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 }
}");
运行 聚合与之前完全相同,您的计数 属性 将填充与您的 groupkey 匹配的文档数量。整洁!
在已接受的答案的基础上,有时您需要指定一个无法使用 Linq Queriable 接口表达的过滤器,但您也不想诉诸手写 BSON 然后还必须强制转换BSON 回到你的对象。您可以将这两个示例结合起来,两全其美。仍然希望你可以 return g.First 但这也有效。
var filter = Builders<Status>.Filter.GeoWithinCenterSphere(x => x.LongLatField, longitude, latitude, radians);
var res = await statusesCollection.Aggregate().Match(filter).Group(x => x.PersistantId,
g=>new Status{_id =g.First()._id,
payment = g.Key,
code=g.First().code,
date=g.First().date
}
))
.ToListAsync();