前言
上一篇文章《MongoDB系列(一):簡介及安裝》已經介紹了MongoDB以及其在window環境下的安裝,這篇文章主要講講如何用C#來與MongoDB進行通訊。再次強調一下,我使用的MongoDB版本是2.6,因為2.6是我最熟悉的版本,而且我使用的GUI工具Robomongo目前還不支援3.0版本。
添加官方驅動
官方驅動可以從Nuget上擷取,但是這裡我們不使用最新的驅動,而是使用1.9.2這個版本,個人認為該版本對MongoDB2.6的支援最好,而且目前的下載下傳量也是最多。驅動位址:https://www.nuget.org/packages/mongocsharpdriver/1.9.2。是以,需要在程式包管理器中擷取Nuget。
打開“程式包管理器中”

輸入指令Install-Package mongocsharpdriver -Version 1.9.2,下載下傳添加驅動
連接配接字元串
mongodb://[username:password@]host1[:port1][,host2[:port2],…[,hostN[:portN]]][/[database][?options]]
内容 | 描述 |
mongodb:// | 是連接配接字串必須的字首字串 |
username:password@ | 可選項,連接配接到資料庫後會嘗試驗證登陸 |
host1 | 必須的指定至少一個host |
:portX | 可選項,預設連接配接到27017 |
/database | 如果指定username:password@,連接配接并驗證登陸指定資料庫。若不指定,預設打開admin資料庫。 |
?options | 是連接配接選項。如果不使用/database,則前面需要加上/。所有連接配接選項都是鍵值對name=value,鍵值對之間通過&或;(分号)隔開 |
C#驅動提供的常用API
方法 | |
InsertBatch | 批量插入 |
Insert | 單條插入 |
FindOneById | 按Id查詢 |
Save | 儲存,如果庫中有記錄則更新,否則做插入,按Id比對 |
Remove | 删除指定文檔 |
AsQueryable | 傳回IQueryable<T>對象 |
Update | 更新一個或多個文檔 |
RemoveAll | 删除所有記錄 |
… | 其它 |
代碼說明
抽象實體類Entity
public abstract class EntityWithTypedId<TId>
{
public TId Id { get; set; }
}
public abstract class Entity : EntityWithTypedId<ObjectId>
{
}
MongoDB要求每個集合都需要有一個Id,即使你定義的類中沒有Id字段,存資料的時候也會生成一個Id,而且Id的類型預設是使用ObjectId,當然也可以使用其他簡單類型作為Id,如int。
核心代碼封裝DbContext
public class DbContext
{
private readonly MongoDatabase _db;
public DbContext()
{
var client = new MongoClient("mongodb://localhost:27017");
var server = client.GetServer();
_db = server.GetDatabase("Temp");
}
public MongoCollection<T> Collection<T>() where T : Entity
{
var collectionName = InferCollectionNameFrom<T>();
return _db.GetCollection<T>(collectionName);
}
private static string InferCollectionNameFrom<T>()
{
var type = typeof(T);
return type.Name;
}
}
1. 通過連接配接字元串與資料庫建立連接配接。
2. 擷取需要操作的Database,這裡是Temp。
3. 類名與Collection名一緻,作為映射的限制。如果庫中沒有這個Collection,則建立該Collection,如果有,則操作該Collection。
定義一個股票類Stock,包含股票代碼,股票名稱,股票價格等簡單類型字段以及股票粉絲複雜字段:
public class Stock : Entity
{
public string Symbol { get; set; }
public string Name { get; set; }
public double Price { get; set; }
public List<Follower> Followers { get; set; }
}
public class Follower
{
public string Name { get; set; }
public int Age { get; set; }
}
代碼調用
static void Main()
{
SetConvention();
var db = new DbContext();
var collection = db.Collection<Stock>();
var stocks = new List<Stock>
{
new Stock
{
Symbol = "000001",
Name = "股票1",
Price = 100,
Followers = new List<Follower>
{
new Follower{ Name = "張三", Age = 20 },
new Follower{ Name = "李四", Age = 22 },
new Follower{ Name = "王五", Age = 23 }
}
},
new Stock
{
Symbol = "000002",
Name = "股票2",
Price = 200,
Followers = new List<Follower>
{
new Follower{ Name = "張三", Age = 20 },
new Follower{ Name = "李四", Age = 22 }
}
},
new Stock
{
Symbol = "000003",
Name = "股票3",
Price = 300,
Followers = new List<Follower>
{
new Follower{ Name = "張三", Age = 20 }
}
},
new Stock
{
Id = ObjectId.GenerateNewId(), //這裡可以自己設定Id,也可以不設,不設的話操作後會自動配置設定Id
Symbol = "000004",
Name = "股票4",
Price = 400
}
};
Console.WriteLine("批量插入");
var results = collection.InsertBatch(stocks);
Console.WriteLine(results.Count()); //這裡傳回的是1,挺奇怪的。
Console.WriteLine();
var stock = new Stock
{
Id = ObjectId.GenerateNewId(), //這裡可以自己設定Id,也可以不設,不設的話操作後會自動配置設定Id
Symbol = "000005",
Name = "股票5",
Price = 500
};
Console.WriteLine("單條插入");
var result = collection.Insert(stock);
Console.WriteLine("插入是否成功:{0}", result.Ok);
Console.WriteLine();
Console.WriteLine("通過Id檢索");
var findedStock = collection.FindOneById(BsonValue.Create(stock.Id));
Console.WriteLine("Symbol:{0}, Name:{1}, Price:{2}", findedStock.Symbol, findedStock.Name, findedStock.Price);
Console.WriteLine();
Console.WriteLine("儲存操作,庫裡有資料");
stock.Symbol = "000006";
result = collection.Save(stock);
Console.WriteLine("儲存是否成功:{0}", result.Ok);
Console.WriteLine();
Console.WriteLine("删除");
result = collection.Remove(Query<Stock>.EQ(n => n.Id, stock.Id));
Console.WriteLine("删除是否成功:{0}", result.Ok);
Console.WriteLine();
Console.WriteLine("儲存操作,庫裡沒資料");
result = collection.Save(stock);
Console.WriteLine("儲存是否成功:{0}", result.Ok);
Console.WriteLine();
Console.WriteLine("簡單查詢");
var list = collection.AsQueryable().Where(n => n.Price >= 300).ToList();
Console.WriteLine("查詢結果條數:{0}", list.Count);
Console.WriteLine();
Console.WriteLine("複雜類型查詢");
list = collection.AsQueryable().Where(n => n.Followers.Any(f => f.Name == "王五")).ToList();
Console.WriteLine("查詢結果條數:{0}", list.Count);
Console.WriteLine();
Console.WriteLine("批量更新");
var query = Query<Stock>.Where(n => n.Price >= 300);
var update = Update<Stock>.Set(n => n.Name, "股票300")
.Set(n => n.Price, 299);
result = collection.Update(query, update, UpdateFlags.Multi);
Console.WriteLine("批量更新是否成功:{0}", result.Ok);
Console.WriteLine();
Console.WriteLine("批量删除");
result = collection.Remove(Query<Stock>.Where(n => n.Price >= 299));
Console.WriteLine("批量删除更新是否成功:{0}", result.Ok);
Console.WriteLine();
Console.WriteLine("删除所有記錄");
result = collection.RemoveAll();
Console.WriteLine("删除所有記錄是否成功:{0}", result.Ok);
Console.WriteLine();
Console.ReadKey();
}
全局公約設定
private static void SetConvention()
{
var pack = new ConventionPack {new IgnoreExtraElementsConvention(true), new IgnoreIfNullConvention(true)};
ConventionRegistry.Register("IgnoreExtraElements&IgnoreIfNull", pack, type => true);
}
1. IgnoreExtraElementsConvention:忽略庫中有但是類中沒有定義的字段。這個一般用于敏感字段處理,例如密碼字段,它會存在使用者Collection中,但是這個字段隻是登入校驗的時候會用到(這時可以用js來查詢),其他使用者查詢(linq查詢)基本都不需要用到密碼字段。
2. IgnoreIfNullConvention:如果字段null,則不存這個字段,簡單來說就是省空間,假設一個類中有A,B兩個字段,其中A字段為空,如果指定該設定,存為:{B:'B'},否則,存為{A:null, B:'B'}。
傳回值說明
為什麼MongoDB提供的API基本都有傳回值?那如果API中出現的異常怎麼處理,被MongoDB吃掉了?
這裡我檢視了MongoDB的驅動源碼,它的結果是通過執行getLastError的方式來擷取的,這是c++的方式,C#無法捕捉到這些異常,是以,傳回值辨別着操作是否成功。同時,API中也會抛出C#的異常或者自定義的異常。這就說明了,操作要滿足兩個條件才算成功:一、無異常,二、傳回值辨別成功。
Repository方式
來到這裡,應該有很多人會問,為什麼還要用Repository?特别是接觸過Entity Framework,因為Entity Framework表明已包含了Unit of Work和Repository,或者是看過《部落格園的大牛們,被你們害慘了,Entity Framework從來都不需要去寫Repository設計模式》這類文章的童鞋。
首先,我需要表明立場,對于不使用Repository的觀點,我是百分之八九十贊同的。那為什麼還要用呢?不為什麼,我就是任性(開個玩笑)!我認為做技術的不能太偏執,還是要根據具體的場景和需求,技術和架構沒有絕對好的,隻有相對好的。技術是發展的,但技術不可能面面俱到。
那麼為什麼要用Repository呢?因為我要寫單元測試,我需要通過Mock的方式抛開資料庫通路的依賴,要Mock的話,要通過接口或虛方法(virtual)。現在的EF 6确實包含了Repository的思想,但是直接用dbContext的話,還是無法Mock(如果可Mock,請告之),是以需要用Repository來包裝一下。就好像當你需要測試一個internal的類的時候,你需要定義一個public的類包一下這個内部類。
是以,如果你需要寫單元測試的話,那麼你應該需要Repository,否則,覺得怎麼爽就怎麼用吧!
通用接口IRepository
public interface IRepositoryWithTypedId<T, in TId> where T : EntityWithTypedId<TId>
{
IEnumerable<bool> InsertBatch(IEnumerable<T> entities);
bool Insert(T entity);
T Get(TId id);
bool Save(T entity);
bool Delete(TId id);
IQueryable<T> AsQueryable();
bool RemoveAll();
}
public interface IRepository<T> : IRepositoryWithTypedId<T, ObjectId> where T : Entity
{
}
通用實作MongoRepository
public class MongoRepositoryWithTypedId<T, TId> : IRepositoryWithTypedId<T, TId> where T : EntityWithTypedId<TId>
{
private readonly MongoCollection<T> _collection;
public MongoRepositoryWithTypedId()
{
var client = new MongoClient("mongodb://localhost:27017");
var server = client.GetServer();
var db = server.GetDatabase("Temp");
var collectionName = InferCollectionNameFrom();
_collection = db.GetCollection<T>(collectionName);
}
private string InferCollectionNameFrom()
{
var type = typeof(T);
return type.Name;
}
protected internal MongoCollection<T> Collection
{
get { return _collection; }
}
public IEnumerable<bool> InsertBatch(IEnumerable<T> entities)
{
var result = Collection.InsertBatch(entities);
return result.Select(n => n.Ok);
}
public bool Insert(T entity)
{
var result = Collection.Insert(entity);
return result.Ok;
}
public T Get(TId id)
{
return Collection.FindOneById(BsonValue.Create(id));
}
public bool Save(T entity)
{
var result = Collection.Save(entity);
return result.Ok;
}
public bool Delete(TId id)
{
var result = Collection.Remove(Query<T>.EQ(t => t.Id, id));
return result.Ok;
}
public IQueryable<T> AsQueryable()
{
return Collection.AsQueryable();
}
public bool RemoveAll()
{
var result = Collection.RemoveAll();
return result.Ok;
}
}
public class MongoRepository<T> : MongoRepositoryWithTypedId<T, ObjectId>, IRepository<T> where T : Entity
{
}
股票接口IStockRepository
public interface IStockRepository : IRepository<Stock>
{
bool UpdateBatch(double minPrice, string name, double price);
bool DeleteBatch(double minPrice);
}
注:如果通用方法足夠用的話,可不需要自定義接口,直接使用IRepository<T>。 如IRepository<Stock> repository = new MongoRepository<Stock>();
股票接口實作StockRepository
public class StockRepository : MongoRepository<Stock>, IStockRepository
{
public bool UpdateBatch(double minPrice, string name, double price)
{
var query = Query<Stock>.Where(n => n.Price >= minPrice);
var update = Update<Stock>.Set(n => n.Name, name)
.Set(n => n.Price, price);
var result = Collection.Update(query, update, UpdateFlags.Multi);
return result.Ok;
}
public bool DeleteBatch(double minPrice)
{
var result = Collection.Remove(Query<Stock>.Where(n => n.Price >= minPrice));
return result.Ok;
}
}
static void Main()
{
SetConvention();
var repository = new StockRepository();
var stocks = new List<Stock>
{
new Stock
{
Symbol = "000001",
Name = "股票1",
Price = 100,
Followers = new List<Follower>
{
new Follower{ Name = "張三", Age = 20 },
new Follower{ Name = "李四", Age = 22 },
new Follower{ Name = "王五", Age = 23 }
}
},
new Stock
{
Symbol = "000002",
Name = "股票2",
Price = 200,
Followers = new List<Follower>
{
new Follower{ Name = "張三", Age = 20 },
new Follower{ Name = "李四", Age = 22 }
}
},
new Stock
{
Symbol = "000003",
Name = "股票3",
Price = 300,
Followers = new List<Follower>
{
new Follower{ Name = "張三", Age = 20 }
}
},
new Stock
{
Id = ObjectId.GenerateNewId(), //這裡可以自己設定Id,也可以不設,不設的話操作後會自動配置設定Id
Symbol = "000004",
Name = "股票4",
Price = 400
}
};
Console.WriteLine("批量插入");
var results = repository.InsertBatch(stocks);
Console.WriteLine(results.Count());
Console.WriteLine();
var stock = new Stock
{
Id = ObjectId.GenerateNewId(), //這裡可以自己設定Id,也可以不設,不設的話操作後會自動配置設定Id
Symbol = "000005",
Name = "股票5",
Price = 500
};
Console.WriteLine("單條插入");
var result = repository.Insert(stock);
Console.WriteLine("插入是否成功:{0}", result);
Console.WriteLine();
Console.WriteLine("通過Id檢索");
var findedStock = repository.Get(stock.Id);
Console.WriteLine("Symbol:{0}, Name:{1}, Price:{2}", findedStock.Symbol, findedStock.Name, findedStock.Price);
Console.WriteLine();
Console.WriteLine("儲存操作,庫裡有資料");
stock.Symbol = "000006";
result = repository.Save(stock);
Console.WriteLine("儲存是否成功:{0}", result);
Console.WriteLine();
Console.WriteLine("删除");
result = repository.Delete(stock.Id);
Console.WriteLine("删除是否成功:{0}", result);
Console.WriteLine();
Console.WriteLine("儲存操作,庫裡沒資料");
result = repository.Save(stock);
Console.WriteLine("儲存是否成功:{0}", result);
Console.WriteLine();
Console.WriteLine("簡單查詢");
var list = repository.AsQueryable().Where(n => n.Price >= 300).ToList();
Console.WriteLine("查詢結果條數:{0}", list.Count);
Console.WriteLine();
Console.WriteLine("複雜類型查詢");
list = repository.AsQueryable().Where(n => n.Followers.Any(f => f.Name == "王五")).ToList();
Console.WriteLine("查詢結果條數:{0}", list.Count);
Console.WriteLine();
Console.WriteLine("批量更新");
result = repository.UpdateBatch(300, "股票300", 299);
Console.WriteLine("批量更新是否成功:{0}", result);
Console.WriteLine();
Console.WriteLine("批量删除");
result = repository.DeleteBatch(299);
Console.WriteLine("批量删除更新是否成功:{0}", result);
Console.WriteLine();
Console.WriteLine("删除所有記錄");
result = repository.RemoveAll();
Console.WriteLine("删除所有記錄是否成功:{0}", result);
Console.WriteLine();
Console.ReadKey();
}
注:我這裡沒有提供Unit of Work的實作,因為我認為MongoDB對連接配接的處理比關系型好,當然用Unit of Work的話應該會更好。
源碼下載下傳
下載下傳位址:https://github.com/ErikXu/MongoDBUsage