65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2016年2月12日

CPOL

8分钟阅读

viewsIcon

38571

downloadIcon

414

开始使用 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 增加一。

这确实是一个非常基本的例子,但它揭示了这个功能有多么强大。

结论

正如我在之前的文章中所写,非关系型数据库将会长期存在。它们灵活、可靠、性能优越,也许更重要的是:开发人员社区越来越壮大。

这些使用新数据库的开发人员的崛起带来了快速的知识共享,这非常令人兴奋!

现在,需要跟上几乎每天都有的新功能和增强。

© . All rights reserved.