【www.bbyears.com--C语言】
这里主要是讲我封装的底层,不涉及到Mongodb的安装,启动,可视化查询等东西,后面会附上一些参考的地址供大家一起学习。
目前mongodb提供的驱动主要有两种:
1.官网驱动 下载地址:http://github.com/mongodb/mongo-csharp-driver/downloads
2.第三方的samus驱动 下载地址:https://github.com/samus/mongodb-csharp
两个驱动的运用自己都有使用过,个人感觉官方的驱动提供的方法比较多,用起来也比较顺手,而且更新度比samus的高,所以自己使用的是官方的驱动。
官网驱动的简单使用
主要使用下面的两个dll
MongoDB.dll 驱动的主要程序
MongoDB.GridFS.dll 用于存储大文件。
基本的增删改查代码如下:
//数据库连接字符串 const string strconn = "mongodb://127.0.0.1:27017"; //数据库名称 const string dbName = "cnblogs"; //定义数据库 MongoDatabase db; ////// 打开数据库链接 /// public void GetConnection() { //定义Mongo服务 Mongo mongo = new Mongo(strconn); //打开连接 mongo.Connect(); //获得数据库cnblogs,若不存在则自动创建 db = mongo.GetDatabase(dbName) as MongoDatabase; } ////// 添加数据 /// public void Insert() { var col = db.GetCollection(); //或者 //var col = db.GetCollection("Users"); Users users = new Users(); users.Name = "xumingxiang"; users.Sex = "man"; col.Insert(users); } /// /// 更新数据 /// public void Update() { var col = db.GetCollection(); //查出Name值为xumingxiang的第一条记录 Users users = col.FindOne(x => x.Name == "xumingxiang"); //或者 //Users users = col.FindOne(new Document { { "Name", "xumingxiang" } }); users.Sex = "women"; col.Update(users, x => x.Sex == "man"); } /// /// 删除数据 /// public void Delete() { var col = db.GetCollection(); col.Remove(x => x.Sex == "man"); ////或者 ////查出Name值为xumingxiang的第一条记录 //Users users = col.FindOne(x => x.Sex == "man"); //col.Remove(users); } /// /// 查询数据 /// public void Query() { var col = db.GetCollection(); var query = new Document { { "Name", "xumingxiang" } }; //查询指定查询条件的全部数据 var result1 = col.Find(query); //查询指定查询条件的第一条数据 var result2 = col.FindOne(query); //查询全部集合里的数据 var result3 = col.FindAll(); }
封装扩展使用
1.数据库配置文件
考虑到一个项目里面可能使用到不同的数据库(比如:普通数据和文件数据等分别存到不同数据库中),也有可能会跨服务器查询,所以这里首先创建一个配置文件帮助类,主要是可以进行多个数据库配置,满足跨服务器,跨数据的需求。
配置格式如下:
Xml序列化对象类
public class ServiceConfig { [XmlArray, XmlArrayItem("Item")] public Listmongodbs { get; set; } } [XmlRoot] public class mongodbConfig { [XmlAttribute("dbName")] public string dbName { get; set; } [XmlAttribute("hostName")] public string hostName { get; set; } }
读取配置文件管理类
public class ManagerConfig { public static string ConfigPath; //加载配置文件 static ManagerConfig() { ConfigPath = "./config.xml"; } //xml序列化后的对象 private static ServiceConfig _settings; public static ServiceConfig ServiceSettings { get { return _settings ?? (_settings = Load()); } } //加载xml序列化为ServiceConfig对象 static ServiceConfig Load() { if (File.Exists(ConfigPath)) { using (FileStream fs = new FileStream(ConfigPath, FileMode.Open)) { XmlSerializer xs = new XmlSerializer(typeof(ServiceConfig)); //序列化为一个对象 _settings = (ServiceConfig)xs.Deserialize(fs); } } else { throw new Exception("数据库配置文件不存在,请检查"); //_settings = new ServiceConfig(); } return _settings; } }
2.实体通用接口
mongodb中本身没有自增ID的属性,自带有一个ObjectID,为了统一每个实体对象都有这个ID ,这里建立一个通用接口和一个底层实体基类来进行规范化处理
实体接口
public interface IMongoEntity { string Id { get; } }
底层实体基类
public class BaseModel : IMongoEntity { [BsonIgnore] public string Id { get { if (_id == ObjectId.Empty) _id = ObjectId.GenerateNewId(DateTime.Now); return _id.ToString(); } } [BsonId] private ObjectId _id; }
实体类的例子(继承于BaseModel类)
public class UserEntity : BaseModel { public string UserName { get; set; } public int Num { get; set; } //MongoDB中存储的时间是标准时间UTC +0:00 (相差了8个小时) [BsonDateTimeOptions(Kind = DateTimeKind.Local)] public DateTime PostTime { get; set; } }
3.Mongodb通用帮助基类 (主要类)
public class MongodbBasewhere T : class,IMongoEntity { protected MongoServer server = null; protected MongoDatabase db = null; protected MongoCollection collection; protected void Init(string DbName) { var Item = ManagerConfig.ServiceSettings.mongodbs.Where(p => p.dbName == DbName).FirstOrDefault(); if (Item == null) { throw new Exception("不存在数据库为: " + DbName + " 的配置对象,请检查"); } else { server = MongoDB.Driver.MongoServer.Create(Item.hostName); db = server.GetDatabase(Item.dbName); collection = db.GetCollection (typeof(T).Name.Replace("Entity", "")); } } #region 查询 /// /// 根据ID获取对象 /// /// ///public T GetModelById(string id) { return collection.FindOneById(id); } /// /// 获取一条记录(自定义条件) /// ///public T FirstOrDefault(Expression > expression) { MongoDB.Driver.IMongoQuery query = Query .Where(expression); return collection.FindOne(query); } /// /// 获取一条记录 /// ///public T FirstOrDefault() { return collection.FindAll().FirstOrDefault(); } /// /// 获取全部 /// ///public List FindAll() { return collection.FindAll().ToList(); } /// /// 获取全部(自定义条件) /// ///public List FindAll(Expression > expression) { MongoDB.Driver.IMongoQuery query = Query .Where(expression); return collection.Find(query).ToList(); } /// /// 根据条件获取数量 /// /// ///public long GetCount(Expression > expression = null) { if (expression == null) { return collection.Count(); } else { return collection.Count(Query .Where(expression)); } } /// /// 根据ID判断是否存在 /// /// ///public bool Exists(string id) { return collection.FindOneById(id) != null; } /// /// 分页 /// /// 总页码 /// 页容量 /// 总记录数 /// 条件 /// 是否是正序 /// 排序的字段 ///public List Page(int PageIndex, int PageSize, out long RowCounts, Expression > expression = null, bool IsAsc = true, params string[] OrderFiled) { MongoCursor mongoCursor; //条件选择 if (expression != null) { RowCounts = collection.Find(Query .Where(expression)).Count(); mongoCursor = collection.Find(Query .Where(expression)); } else { RowCounts = collection.FindAll().Count(); mongoCursor = collection.FindAll(); } //排序 if (OrderFiled != null && OrderFiled.Length > 0) { //处理主键字段 for (int i = 0; i < OrderFiled.Length; i++) { if (OrderFiled[i].Equals("id", StringComparison.CurrentCultureIgnoreCase)) { OrderFiled[i] = "_id"; } } if (IsAsc) { mongoCursor = mongoCursor.SetSortOrder(SortBy.Ascending(OrderFiled)); } else { mongoCursor = mongoCursor.SetSortOrder(SortBy.Descending(OrderFiled)); } } return mongoCursor.SetSkip((PageIndex - 1) * PageSize).SetLimit(PageSize).ToList(); } #region 效率低,暂时不用 ///// ///// 分页 ///// ///////public List Page(int PageIndex, int PageSize, out long RowCounts, Expression > expression = null) //{ // List ret = new List (); // IQueryable queryable; // //条件选择 // if (expression != null) // { // queryable = collection.Find(Query .Where(expression)).AsQueryable(); // } // else // { // queryable = collection.FindAll().AsQueryable(); // } // RowCounts = queryable.Count(); // ret = queryable.Skip((PageIndex - 1) * PageSize).Take(PageSize).ToList(); // return ret; //} ///// ///// 分页 ///// ////////// ///// ///// ///// ///// ///// ///// //public List Page (int PageIndex, int PageSize, out long RowCounts, Expression > expression = null, Expression > orderBy = null, bool IsOrder = true) //{ // List ret = new List (); // IQueryable queryable; // //条件选择 // if (expression != null) // { // queryable = collection.Find(Query .Where(expression)).AsQueryable(); // } // else // { // queryable = collection.FindAll().AsQueryable(); // } // //排序 // if (orderBy != null) // { // if (IsOrder) // { // queryable = queryable.OrderBy(orderBy); // } // else // { // queryable = queryable.OrderByDescending(orderBy); // } // } // RowCounts = queryable.Count(); // ret = queryable.Skip((PageIndex - 1) * PageSize).Take(PageSize).ToList(); // return ret; //} #endregion #endregion #region 删除 /// /// 带条件的删除 /// /// ///public void Delete(Expression > expression) { MongoDB.Driver.IMongoQuery query = Query .Where(expression); var result = collection.Remove(query); } /// /// 根据模型删除 /// /// public void Delete(T model) { MongoDB.Driver.IMongoQuery query = Query.Where(p => p.Id == model.Id); collection.Remove(query); } /// /// 根据ID删除 /// /// public void Delete(string Id) { MongoDB.Driver.IMongoQuery query = Query.Where(p => p.Id == Id); collection.Remove(query); } /// /// 全部删除 /// ///public void DeleteAll() { var result = collection.RemoveAll(); } #endregion #region 添加 /// /// 单模型添加 /// /// ///public void Insert(T model) { var result = collection.Insert (model); } /// /// 批量添加 /// /// ///public void InsertBatch(List model) { collection.InsertBatch (model); } #endregion #region 修改 /// /// 修改 /// /// ///public void Update(T model) { var result = collection.Save (model); } /// /// 批量修改 /// /// public void UpdateAll(Listmodel) { model.ForEach(e => collection.Save (e)); } #endregion }
4.业务类
当新建一个表(mongodb里面叫做集合),需要对其进行操作,包括一些业务处理时。首先继承MongodbBase类,然后使用Init方法初始化对象,如下面的UserServcices类
public class UserServices : MongodbBase{ public UserServices() { this.Init("myDb"); } }
5.使用
[TestClass] public class UnitTest1 { Random rd = new Random(); UserServices ubll = new UserServices(); #region 添加 [TestMethod] public void 添加() { UserEntity model = new UserEntity(); model.UserName = "Name" + rd.Next(100, 10000); model.Num = rd.Next(100, 10000); model.PostTime = DateTime.Now; ubll.Insert(model); } [TestMethod] public void 添加复杂模型() { ComplexEntity model = new ComplexEntity(); ComplexServices cbll = new ComplexServices(); model.Name = "Complex"; model.Schools = new List(); model.Schools.Add(new School() { Master = new Grade() { Name = "Master" }, Name = "School", Students = new List () }); model.Schools[0].Students.Add(new Student() { Age = 22, Name = "张三" }); cbll.Insert(model); } [TestMethod] public void 批量添加() { List Data = new List (); for (int i = 0; i < 1000000; i++) { UserEntity model = new UserEntity(); model.UserName = "Name" + rd.Next(100, 10000); model.Num = rd.Next(100, 10000); model.PostTime = DateTime.Now; Data.Add(model); } ubll.InsertBatch(Data); } #endregion #region 修改 [TestMethod] public void 获取单个对象_修改() { var model = ubll.FirstOrDefault(p => p.Id != ""); model.UserName = "new1"; ubll.Update(model); } [TestMethod] public void 批量修改() { var model = ubll.FindAll(); for (int i = 0; i < model.Count; i++) { model[i].UserName = "Text"; } ubll.UpdateAll(model); } #endregion #region 查询 [TestMethod] public void 获取全部对象() { var model = ubll.FindAll(); var count = model.Count; } [TestMethod] public void 获取单个对象() { var model = ubll.FirstOrDefault(); var count = model.PostTime; } [TestMethod] public void 根据ID获取对象() { var model = ubll.GetModelById("eeef22d6-7ac6-40cd-9312-59ab15fd904a"); } [TestMethod] public void 获取全部对条件象_带条件() { var model = ubll.FindAll(p => p.UserName.Contains("Name")); var count = model.Count; } [TestMethod] public void 分页() { long Rows; List pageDate = new List (); pageDate = ubll.Page(300, 20, out Rows, p => p.Num > 1500); pageDate = ubll.Page(1, 20, out Rows, null, true, "Id"); pageDate = ubll.Page(1, 20, out Rows, null, true, "Num"); pageDate = ubll.Page(1, 20, out Rows, p => p.Num > 1500, false, "Id"); } [TestMethod] public void 获取数量() { //不带条件 var count = ubll.GetCount(); //带条件 var count1 = ubll.GetCount(p => p.Num > 5000); } #endregion #region 删除 [TestMethod] public void 删除_自定义条件() { ubll.Delete(p => p.Num >= 2000); } [TestMethod] public void 删除_删除模型() { var model = ubll.FirstOrDefault(); if (model != null) { ubll.Delete(model); } } [TestMethod] public void 删除_根据ID删除() { ubll.Delete("ec45ea8b-a551-46eb-ad58-1b4f5f2aab25"); } [TestMethod] public void 删除_删除全部() { ubll.DeleteAll(); } #endregion #region 其他 [TestMethod] public void 同时创建两个对象_同一数据库内() { LogServices Logbll = new LogServices(); UserEntity model = new UserEntity(); model.UserName = "Name" + rd.Next(100, 10000); model.Num = rd.Next(100, 10000); model.PostTime = DateTime.Now; ubll.Insert(model); LogEntity log = new LogEntity(); log.UserName1 = "Name" + rd.Next(100, 10000); log.Num1 = rd.Next(100, 10000); log.PostTime1 = DateTime.Now; Logbll.Insert(log); model.UserName = "Name" + rd.Next(100, 10000); model.Num = rd.Next(100, 10000); model.PostTime = DateTime.Now; ubll.Insert(model); } [TestMethod] public void 同时创建两个对象_不同一数据库内() { Log1Services Logbll = new Log1Services(); UserEntity model = new UserEntity(); model.UserName = "Name" + rd.Next(100, 10000); model.Num = rd.Next(100, 10000); model.PostTime = DateTime.Now; ubll.Insert(model); LogEntity log = new LogEntity(); log.UserName1 = "Name" + rd.Next(100, 10000); log.Num1 = rd.Next(100, 10000); log.PostTime1 = DateTime.Now; Logbll.Insert(log); model.UserName = "Name" + rd.Next(100, 10000); model.Num = rd.Next(100, 10000); model.PostTime = DateTime.Now; ubll.Insert(model); } [TestMethod] public void 当指定名称不存在时候() { ErrorServices error = new ErrorServices(); } #endregion }
以上就是自己封装的整体逻辑和代码,不过这里面还有一些不明白和不足的地方,这里提出来,希望大神们帮我解答下:
1.返回值问题
在添,删,改的使用,根据官网提供的驱动,都有一个WriteConcernResult对象返回,可是在测试中发现,这个返回的对象永远都是null
2.增加ID问题
mongodb中本身没有自增ID的属性,自带有一个ObjectID,如果我需要一个自增ID,是否是自己建一个ID属性,然后在增加的时候自己控制+1?不过这样是否性能上比较低,还要考虑多线程并发的情况下加锁的问题。所以不知道这块大家是怎么去实现的?
3.分页效率的问题
一开始分页我是先将结果转为Queryable,然后在进行操作,这个代码里面有这段,暂时注释掉了,后面再博客园上看到了一个前辈的mongodb分析后,改了下分页的方式,测试过很快,但在带条件获取记录行总数的时候,发现测试300W数据下,获取总数需要600ms的时间,不知道是我方法用错了还是有其他更好的?
MongoDB 分页查询的方法及性能
这篇文章着重的讲讲MongoDB的分页查询,为啥?分页可是常见的头号杀手,弄不好了,客户骂,经理骂。
传统的SQL分页
传统的sql分页,所有的方案几乎是绕不开row_number的,对于需要各种排序,复杂查询的场景,row_number就是杀手锏。另外,针对现在的web很流行的poll/push加载分页的方式,一般会利用时间戳来实现分页。 这两种分页可以说前者是通用的,连Linq生成的分页都是row_number,可想而知它多通用。后者是无论是性能和复杂程度都是最好的,因为只要简单的一个时间戳即可。
MongoDB分页
进入到Mongo的思路,分页其实并不难,那难得是什么?其实倒也没啥,看明白了也就那样,和SQL分页的思路是一致的。
先说明下这篇文章使用的用例,我在数据库里导入了如下的实体数据,其中cus_id、amount我生成为有序的数字,倒入的记录数是200w:
public class Test { ////// 主键 ObjectId 是MongoDB自带的主键类型 /// public ObjectId Id { get; set; } ////// 客户编号 /// [BsonElement("cust_id")] public string CustomerId { get; set; } ////// 总数 /// [BsonElement("amount")] public int Amount { get; set; } ////// 状态 /// [BsonElement("status")] public string Status { get; set; } }
首先来看看分页需要的参数以及结果,一般的分页需要的参数是:
PageIndex 当前页
PageSize 每页记录数
QueryParam[] 其他的查询字段
所以按照row_number的分页思想,也就是说取第(pageIndex*pageSize)到第(pageIndex*pageSize + pageSize),我们用Linq表达就是:
query.Where(xxx...xxx).Skip(pageIndex*pageSize).Take(pageSize)
查找了资料,还真有skip函数,而且还有Limit函数 见参考资料1、2,于是轻易地实现了这样的分页查询:
db.test.find({xxx...xxx}).sort({"amount":1}).skip(10).limit(10)//这里忽略掉查询语句
相当的高效,几乎是几毫秒就出来了结果,果然是NoSql效率一流。但是慢,我这里使用的数据只是10条而已,并没有很多数据。我把数据加到100000,效率大概是20ms。如果这么简单就研究结束了的话,那真的是太辜负了程序猿要钻研的精神了。sql分页的方案,方案可是能有一大把,效率也是不一的,那Mongo难道就这一种,答案显然不是这样的。另外是否效率上,性能上会有问题呢?Redis篇里,就吃过这样的亏,乱用Keys。
在查看了一些资料之后,发现所有的资料都是这样说的:
不要轻易使用Skip来做查询,否则数据量大了就会导致性能急剧下降,这是因为Skip是一条一条的数过来的,多了自然就慢了。
这么说Skip就要避免使用了,那么如何避免呢?首先来回顾SQL分页的后一种时间戳分页方案,这种利用字段的有序性质,利用查询来取数据的方式,可以直接避免掉了大量的数数。也就是说,如果能附带上这样的条件那查询效率就会提高,事实上是这样的么?我们来验证一下:
这里我们假设查询第100001条数据,这条数据的Amount值是:2399927,我们来写两条语句分别如下:
db.test.sort({"amount":1}).skip(100000).limit(10) //183ms
db.test.find({amount:{$gt:2399927}}).sort({"amount":1}).limit(10) //53ms
结果已经附带到注释了,很明显后者的性能是前者的三分之一,差距是非常大的。也印证了Skip效率差的理论。
C#实现
上面已经谈过了MongoDB分页的语句和效率,那么我们来实现C#驱动版本。
本篇文章里使用的是官方的BSON驱动,详见参考资料4。Mongo驱动附带了另种方式一种是类似ADO.NET的原生query,一种是Linq,这里我们两种都实现
方案一:条件查询 原生Query实现
var query = Query
var result = collection.Find(query).SetLimit(100)
.SetSortOrder(SortBy.Ascending("amount")).ToList();
Console.WriteLine(result.First().ToJson());//BSON自带的ToJson
方案二:Skip原生Query实现
var result = collection.FindAll().SetSkip(100000).SetLimit(100)
.SetSortOrder(SortBy.Ascending("amount"));
Console.WriteLine(result.ToList().First().ToJson());
方案三:Linq 条件查询
var result = collection.AsQueryable
.Where(item => item.Amount > 2399927).Take(100);
Console.WriteLine(result.First().ToJson());
方案四:Linq Skip版本
var result = collection.AsQueryable
Console.WriteLine(result.First().ToJson());
性能比较参考
这里的测试代码稍后我上传一下,具体的实现是利用了老赵(我的偶像啊~)的CodeTimer来计算性能。另外我跑代码都是用TestDriven插件来跑的。
方案一:
pagination GT-Limit { "_id" : ObjectId("5472e383fc46de17c45d4682"), "cust_id" : "A12399997", "amount" : 2399928, "status" : "B" } Time Elapsed: 1,322ms CPU Cycles: 4,442,427,252 Gen 0: 0 Gen 1: 0 Gen 2: 0
方案二:
pagination Skip-limit { "_id" : ObjectId("5472e383fc46de17c45d4682"), "cust_id" : "A12399997", "amount" : 2399928, "status" : "B" } Time Elapsed: 95ms CPU Cycles: 18,280,728 Gen 0: 0 Gen 1: 0 Gen 2: 0
方案三:
paginatiLinq on Linq where { "_id" : ObjectId("5472e383fc46de17c45d4682"), "cust_id" : "A12399997", "amount" : 2399928, "status" : "B" } Time Elapsed: 76ms CPU Cycles: 268,734,988 Gen 0: 0 Gen 1: 0 Gen 2: 0
方案四:
pagination Linq Skip { "_id" : ObjectId("5472e383fc46de17c45d4682"), "cust_id" : "A12399997", "amount" : 2399928, "status" : "B" } Time Elapsed: 97ms CPU Cycles: 30,834,648 Gen 0: 0 Gen 1: 0 Gen 2: 0
上面结果是不是大跌眼镜,这和理论实在相差太大,第一次为什么和后面的差距如此大?刚开始我以为是C# Mongo的驱动问题,尝试了换驱动也差不多。这几天我在看《MongoDB in Action》的时候,发现文章里提到:
MongoDB会根据查询,来加载文档的索引和元数据到内存里,并且建议文档元数据的大小始终要保持小于机器内存,否则性能会下降。
注意到了上面的理论之后,我替换了我的测试方案,第一次执行排除下,然后再比较,发现确实结果正常了。
方案一的修正结果:
pagination GT-Limit { "_id" : ObjectId("5472e383fc46de17c45d4682"), "cust_id" : "A12399997", "amount " : 2399928, "status" : "B" } Time Elapsed: 18ms CPU Cycles: 54,753,796 Gen 0: 0 Gen 1: 0 Gen 2: 0
总结
这篇文章,基于Skip分页和有序字段查询分页两种方案进行的对比。后者说白了只是利用查询结果不用依次数数来提高了性能。Skip虽然效率低一些但是通用一些,有序字段的查询,需要在设计分页的时候对这个字段做一些处理,起码要点了页码能获取到这个字段。这里我附加一个方式,就是两者的结合,我们可以拿每次展示的那页数据上的最后一个,结合Skip来处理分页,这样的话,相对来说更好一些。这里就不具体实现了。其他方式的性能比较和实现,欢迎大牛们来分享,十分感谢。另外本篇中如有纰漏和不足请留言指教。