使用 MongoDB 3.2 和 C# 驱动程序 2.0 执行聚合和使用异步方法





5.00/5 (4投票s)
开始使用 MongoDB 3.2 和 C# 驱动程序 2.0
引言
大家好,我又回来为大家带来更多关于非关系型数据库的技巧和食谱。这次我们将讨论最新的 C# MongoDB 驱动程序。虽然它已经不是一个全新的版本了,但当我寻找一些指导来使用它进行基本的 CRUD 操作和聚合时,我发现官方的资料相当简洁。
这个版本最显著的区别是全栈异步支持,这非常棒。但是,如果你没有结合实际的示例来理解这个特性,会有点困难。
这篇文章就是关于这个的,在下面的章节中,我将展示一个完整可操作且直接的应用程序,使用所有主要的 CRUD 方法和一些聚合。
背景
目前行业中有许多非关系型数据库。如果你至少熟悉其中一个,你在这里会没问题的。否则,在 Code Project 社区中,你可以找到许多关于这个主题的入门文章。
安装
让我们从数据库设置开始!我整个文章都基于官方的 MongoDB 材料,所以我会时不时地引用它的网站。
首先,你需要从官方源下载安装程序。安装向导将引导你完成整个过程,完成后,请仔细检查 MongoDB 服务是否已启动并运行。
现在我们需要一些数据来处理,MongoDB 提供的数据集很适合我们。在这里你可以找到它(json 文件)以及导入它的命令行。
数据库 IDE
我使用的是 Razor SQL。这个工具很棒,唯一的问题是……没有免费版本。不过,你可以在 30 天的试用期内使用它。导入示例数据后,表/json 集合将显示为
创建一个演示应用程序
第一步是创建一个新的 Windows 窗体解决方案,或者更好的是,你可以在本文开头下载它。它的结构如下:
高亮显示的引用red是最重要的,你可以通过 NuGet 获取它们。请确保获取 2.0 C# 驱动程序版本。
全部蓝色区域是我决定如何组织项目的。相当标准:BLL 代表业务逻辑层,DAL 代表数据访问层,DTO 包含实体,View 包含我们的 Windows 窗体。
回到示例数据结构,你可能已经注意到它包含与餐厅相关的字段,并以层次格式组织。
{
"address": {
"building": "1007",
"coord": [ -73.856077, 40.848447 ],
"street": "Morris Park Ave",
"zipcode": "10462"
},
"borough": "Bronx",
"cuisine": "Bakery",
"grades": [
{ "date": { "$date": 1393804800000 }, "grade": "A", "score": 2 },
{ "date": { "$date": 1378857600000 }, "grade": "A", "score": 6 },
{ "date": { "$date": 1358985600000 }, "grade": "A", "score": 10 },
{ "date": { "$date": 1322006400000 }, "grade": "A", "score": 9 },
{ "date": { "$date": 1299715200000 }, "grade": "B", "score": 14 }
],
"name": "Morris Park Bake Shop",
"restaurant_id": "30075445"
}
而我们的 Windows 窗体将依次反映此架构如下:
数据库连接
如果你下载了演示应用程序,你可能会注意到有两个常量值到处出现。
namespace Mongodb_20Driver_CRUD.DTO { /// System constant values public static class Constants { /// BD name public const string DEFAULT_DB = "test"; /// Coll name public const string DEFAULT_COLLECTION = "restaurants"; } }
这两个值代表数据库名称(“test”)和我们之前导入的表/集合(“restaurants”)。从现在开始,你将到处看到这两个常量。
我们还需要一个类来充当 MongoDB 客户端的角色,它看起来像这样:
namespace Mongodb_20Driver_CRUD.DAL { /// Mongodb client public class MongodbClient { /// Client instance public IMongoClient Client { get; set; } /// Database instance public IMongoDatabase DataBase { get; set; } /// Constructor public MongodbClient() { Client = new MongoClient(); DataBase = Client.GetDatabase(DTO.Constants.DEFAULT_DB); } } }
是的,我知道,它看起来太简单了。但是,这就是我们需要连接并对 MongoDB 运行查询所需的一切。
例如,这是用于发现示例数据有多少行的方法:
public async Task<long> GetOverallCount() { long count = 0; try { var collection = _clientDAL.DataBase.GetCollection<BsonDocument> (DTO.Constants.DEFAULT_COLLECTION); var filter = new BsonDocument(); using (var cursor = await collection.FindAsync(filter)) { while (await cursor.MoveNextAsync()) { var batch = cursor.Current; foreach (var document in batch) { count++; } } } } catch (Exception ex) { throw new Exception("Error retrieving the overall count: " + ex.ToString()); } return count; }
请注意,此方法在此行使用了我之前展示的两个类:
var collection = _clientDAL.DataBase.GetCollection<BsonDocument>(DTO.Constants.DEFAULT_COLLECTION);
“_clientDAL” 是 MongoDBClient 实例,我们使用此命令检索“restaurants”表/集合。
有了这个集合的实例,我们就可以使用异步方法 “FindAsync” 的返回值来遍历所有餐厅项。
这可能看起来代码很多只是为了处理表计数,但当我们整合整个应用程序时,这种异步方法的优势就会显现出来。
插入/更新一行
现在你已经了解了我们演示的基本类,我们将开始编写我们的 BLL(业务逻辑)。这里的第一项功能将在数据库中插入和更新一家餐厅。
public async void Insert(DTO.Restaurant customer) { try { if (customer == null) return; //Create inner grades list: BsonArray grades = new BsonArray(); foreach (var grade in customer.Grades) { grades.Add(new BsonDocument { { "date", grade.Date }, { "grade", grade.GradeValue }, { "score", grade.Score } }); } //Create main object: var document = new BsonDocument { { "address" , new BsonDocument { { "street", customer.Address.Street }, { "zipcode", customer.Address.Zipcode }, { "building", customer.Address.Building }, { "coord", new BsonArray { customer.Address.Coord.Item1, customer.Address.Coord.Item2 } } } }, { "borough", customer.Borough }, { "cuisine", customer.Cuisine }, { "name", customer.Name }, { "restaurant_id", customer.RestaurantID } }; document.Add("grades", grades); //Insert it: var collection = _clientDAL.DataBase.GetCollection<BsonDocument> (DTO.Constants.DEFAULT_COLLECTION); await collection.InsertOneAsync(document); } catch (Exception ex) { throw new Exception("Error inserting new Restaurant: " + ex.ToString()); } }
有几点需要在此讨论:
首先,C# 驱动程序使用 BsonDocument 对象来表示 JSON 对象。还有其他重载,但是 BsonDocument 被广泛使用。
关于方法本身,它接收一个“restaurant”实体(请参考 DTO 命名空间查看所有实体),并将其解析为 BsonDocument 实例。由于餐厅的评分可能不止一个,这些对象被保存在一个列表中(BsonArray),并链接到主 BsonDocument 对象。
最后,有了我们熟悉的集合实例,我们只需要调用 “InsertOneAsync” 方法来完成操作。
我们仍然缺少这里的更新部分,幸运的是,C# 驱动程序提供了一种很好的处理方式。为此,你只需要将方法的最后两行替换为:
var filter = Builders<BsonDocument>.Filter.Eq("restaurant_id",customer.RestaurantID); var collection = _clientDAL.DataBase.GetCollection<BsonDocument> (DTO.Constants.DEFAULT_COLLECTION); var result = await collection.ReplaceOneAsync(filter, document, new UpdateOptions() { IsUpsert = true });
如果没有任何文档匹配更新条件,“ReplaceOneAsync” 方法的默认行为是什么都不做。但是,通过将 upsert 选项设置为 true,更新操作将更新匹配的文档或在没有匹配文档的情况下插入新文档。
删除一行
这是最简单的。
var collection = _clientDAL.DataBase.GetCollection<BsonDocument>(DTO.Constant.DEFAULT_COLLECTION); var filter = Builders<BsonDocument>.Filter.Eq("restaurant_id", id); var result = await collection.DeleteOneAsync(filter);
只需提供要删除的唯一 ID,将其包装在 Filter 对象中,然后调用 “DeleteOneAsync” 方法。C# 驱动程序提供了其他选项,例如可以通过 “DeleteManyAsync” 一次删除多行。
聚合
从业务角度来看,这是任何数据库最有价值的功能。这时你的解决方案将能够从大量数据中提取有意义的信息。
为了简单起见,我们将实现 3 种类型的聚合,每种类型允许 3 种过滤选项。我们的聚合 UI 设计如下:
是的,我知道!这就是一个后端开发人员尝试涉足 UI 设计领域时会发生什么,真是太棒了!无论如何,这里重要的是如何处理查询对象以请求聚合。让我们从菜肴计数开始。
public async Task<List<DTO.Aggregatedtem> > GetAggregatedCuisineList(DTO.Filter filter) { List<DTO.Aggregatedtem> returnList = new List<DTO.Aggregatedtem> (); var query = Builders<BsonDocument> .Filter.Empty; var criteriaFilter = Builders<BsonDocument> .Filter.Empty; try { //Filtering (block 1): if (string.IsNullOrEmpty(filter.Borough) == false) { criteriaFilter = Builders<BsonDocument>.Filter.Eq("borough",filter.Borough); query = query & criteriaFilter; } if (string.IsNullOrEmpty(filter.Cuisine) == false) { criteriaFilter = Builders<BsonDocument>.Filter.Eq("cuisine",filter.Cuisine); query = query & criteriaFilter; } if (string.IsNullOrEmpty(filter.Grade) == false) { criteriaFilter = Builders<BsonDocument>.Filter.Eq("grades.grade", filter.Grade); query = query & criteriaFilter; } //Querying (block 2): var collection = _clientDAL.DataBase.GetCollection<BsonDocument> (DTO.Constants .DEFAULT_COLLECTION); var results = await collection.Aggregate() .Match(query) .Group(new BsonDocument { { "_id", "$cuisine" }, { "count", new BsonDocument("$sum", 1) } }) .ToListAsync(); //Parsing results (block 3): foreach (BsonDocument item in results) { returnList.Add(new DTO.Aggregatedtem() { Label = item.Elements.ElementAtOrDefault(0).Value.AsString, Value = item.Elements.ElementAtOrDefault(1).Value.AsInt32 }); } } catch (Exception ex) { throw new Exception("Error preforming the aggregation: " + ex.ToString()); } return returnList.OrderByDescending(x => x.Value).ToList(); }
此方法大致分为 3 个块:
在第一个块中,它根据 DTO.Filter 的信息有条件地创建查询对象。
然后,是时候从我们已知的“collection”对象中调用一些 lambda 方法了。
- 首先,我们告知我们要聚合结果(Aggregate())。
- 我们还设置了查询对象(Match(query)),该对象是在第一步中创建的。
- 然后,我们按 $cuisine 字段(字段名称前带有美元符号 $)对结果进行分组。
- 最后,我们调用异步方法来检索聚合的数据集(TopstAsync())。
最后一步是将 BsonDocument 的返回值解析为我们的 DTO.Aggregatedtem。有更聪明的方法可以做到这一点,例如你可以使用 BsonDocument 本身的解析器。
要执行区域的聚合,逻辑完全相同。你只需要将分组字段名替换为 “$borough”。
对于最后一个 KPI,我们将按餐厅的最高分数聚合所有信息(这在带你的爱人出去吃晚餐时可能非常有用)。
public async Task<List<DTO.Aggregatedtem>> GetAggregatedMaxScore(DTO.Filter filter) { List<DTO.Aggregatedtem> returnList = new List<DTO.Aggregatedtem> (); var query = Builders<BsonDocument> .Filter.Empty; var criteriaFilter = Builders<BsonDocument> .Filter.Empty; try { //Filtering (block 1): if (string.IsNullOrEmpty(filter.Borough) == false) { criteriaFilter = Builders<BsonDocument>.Filter.Eq("borough", filter.Borough); query = query & criteriaFilter; } if (string.IsNullOrEmpty(filter.Cuisine) == false) { criteriaFilter = Builders<BsonDocument>.Filter.Eq("cuisine", filter.Cuisine); query = query & criteriaFilter; } if (string.IsNullOrEmpty(filter.Grade) == false) { criteriaFilter = Builders<BsonDocument>.Filter.Eq("grades.grade", filter.Grade); query = query & criteriaFilter; } //Querying (block 2): var collection = _clientDAL.DataBase.GetCollection<BsonDocument> (DTO.Constants.DEFAULT_COLLECTION); var results = await collection.Aggregate() .Match(query) .Group(new BsonDocument {{"_id", "$name" }, { "MaxScore", new BsonDocument("$max", "$grades.score")}) ToListAsync(); //Parsing (block 3): foreach (BsonDocument item in results) { string key = item.Elements.ElementAtOrDefault(0).Value.AsString; var value = item.Elements.ElementAtOrDefault(1).Value.AsBsonArray.Max(); if (value != null && value != BsonNull.Value) { returnList.Add(new DTO.Aggregatedtem() { Label = key, Value = double.Parse (value.ToString()) }); } } } catch (Exception ex) { throw new Exception("Error preforming the aggregation. " + ex.ToString()); } return returnList.OrderByDescending(x => x.Value).ToList(); }
为了使其尽可能具有教学意义,我保持了相同的结构。最大的区别在于我们如何聚合结果,即按餐厅名称。另一个重要方面是,我们没有执行计数,而是使用了评分的“$max”函数。
顺便说一句,这是你可以如何表示此类字段层次结构的示例:grades.score。
利用异步特性
正如我在开头所说,2.0 驱动程序最突出的特性之一是其原生的异步方法。通过正确使用它,你可以防止 UI 在执行其他繁重进程时冻结。
在我们的小演示中,你将通过重现以下场景来注意到这一点:
填写所有字段,添加一些评分,然后单击“保存”。这样做时,UI 将触发两个操作:保存餐厅,并在屏幕左下角更新“行计数”。
这就是诀窍,通常“保存”操作会在计数之前完成。但是,UI 不会因为等待计数过程完成而锁定,因此你会看到计数在保存操作完成后几秒钟内 nicely 增加一。
这确实是一个非常基本的例子,但它揭示了这个功能有多么强大。
结论
正如我在之前的文章中所写,非关系型数据库将会长期存在。它们灵活、可靠、性能优越,也许更重要的是:开发人员社区越来越壮大。
这些使用新数据库的开发人员的崛起带来了快速的知识共享,这非常令人兴奋!
现在,需要跟上几乎每天都有的新功能和增强。