MongoDB via Dotnet Core数据映射详解
用好數據映射,MongoDB via Dotnet Core開發變會成一件超級快樂的事。
?
一、前言
MongoDB這幾年已經成為NoSQL的頭部數據庫。
由于MongoDB?free schema的特性,使得它在互聯網應用方面優于常規數據庫,成為了相當一部分大廠的主數據選擇;而它的快速布署和開發簡單的特點,也吸引著大量小開發團隊的支持。
關于MongoDB快速布署,我在15分鐘從零開始搭建支持10w+用戶的生產環境(二)里有寫,需要了可以去看看。
?
作為一個數據庫,基本的操作就是CRUD。MongoDB的CRUD,不使用SQL來寫,而是提供了更簡單的方式。
方式一、BsonDocument方式
BsonDocument方式,適合能熟練使用MongoDB Shell的開發者。MongoDB Driver提供了完全覆蓋Shell命令的各種方式,來處理用戶的CRUD操作。
這種方法自由度很高,可以在不需要知道完整數據集結構的情況下,完成數據庫的CRUD操作。
方式二、數據映射方式
數據映射是最常用的一種方式。準備好需要處理的數據類,直接把數據類映射到MongoDB,并對數據集進行CRUD操作。
?
下面,對數據映射的各個部分,我會逐個說明。
二、開發環境&基礎工程
這個Demo的開發環境是:Mac + VS Code + Dotnet Core 3.1.2。
建立工程:
%?dotnet?new?sln?-o?demo The?template?"Solution?File"?was?created?successfully. %?cd?demo? %?dotnet?new?console?-o?demo The?template?"Console?Application"?was?created?successfully.Processing?post-creation?actions... Running?'dotnet?restore'?on?demo/demo.csproj...Determining?projects?to?restore...Restored?demo/demo/demo.csproj?(in?162?ms).Restore?succeeded. %?dotnet?sln?add?demo/demo.csproj? Project?`demo/demo.csproj`?added?to?the?solution.建立工程完成。
下面,增加包mongodb.driver到工程:
%?cd?demo %?dotnet?add?package?mongodb.driverDetermining?projects?to?restore... info?:?Adding?PackageReference?for?package?'mongodb.driver'?into?project?'demo/demo/demo.csproj'. info?:?Committing?restore... info?:?Writing?assets?file?to?disk.?Path:?demo/demo/obj/project.assets.json log??:?Restored?/demo/demo/demo.csproj?(in?6.01?sec).項目準備完成。
看一下目錄結構:
%?tree?. . ├──?demo │???├──?Program.cs │???├──?demo.csproj │???└──?obj │???????├──?demo.csproj.nuget.dgspec.json │???????├──?demo.csproj.nuget.g.props │???????├──?demo.csproj.nuget.g.targets │???????├──?project.assets.json │???????└──?project.nuget.cache └──?demo.sln?
mongodb.driver是MongoDB官方的數據庫SDK,從Nuget上安裝即可。
三、Demo準備工作
創建數據映射的模型類CollectionModel.cs,現在是個空類,后面所有的數據映射相關內容會在這個類進行說明:
public?class?CollectionModel { }并修改Program.cs,準備Demo方法,以及連接數據庫:
class?Program {private?const?string?MongoDBConnection?=?"mongodb://localhost:27031/admin";private?static?IMongoClient?_client?=?new?MongoClient(MongoDBConnection);private?static?IMongoDatabase?_database?=?_client.GetDatabase("Test");private?static?IMongoCollection<CollectionModel>?_collection?=?_database.GetCollection<CollectionModel>("TestCollection");static?async?Task?Main(string[]?args){await?Demo();Console.ReadKey();}private?static?async?Task?Demo(){} }四、字段映射
從上面的代碼中,我們看到,在生成Collection對象時,用到了CollectionModel:
IMongoDatabase?_database?=?_client.GetDatabase("Test"); IMongoCollection<CollectionModel>?_collection?=?_database.GetCollection<CollectionModel>("TestCollection");這兩行,其實就完成了一個映射的工作:把MongoDB中,Test數據庫下,TestCollection數據集(就是SQL中的數據表),映射到CollectionModel這個數據類中。換句話說,就是用CollectionModel這個類,來完成對數據集TestCollection的所有操作。
?
保持CollectionModel為空,我們往數據庫寫入一行數據:
private?static?async?Task?Demo() {CollectionModel?new_item?=?new?CollectionModel();await?_collection.InsertOneAsync(new_item); }執行,看一下寫入的數據:
{?"_id"?:?ObjectId("5ef1d8325327fd4340425ac9") }OK,我們已經寫進去一條數據了。因為映射類是空的,所以寫入的數據,也只有_id一行內容。
?
但是,為什么會有一個_id呢?
1. ID字段
MongoDB數據集中存放的數據,稱之為文檔(Document)。每個文檔在存放時,都需要有一個ID,而這個ID的名稱,固定叫_id。
當我們建立映射時,如果給出_id字段,則MongoDB會采用這個ID做為這個文檔的ID,如果不給出,MongoDB會自動添加一個_id字段。
例如:
public?class?CollectionModel {public?ObjectId?_id?{?get;?set;?}public?string?title?{?get;?set;?}public?string?content?{?get;?set;?} }和
public?class?CollectionModel {public?string?title?{?get;?set;?}public?string?content?{?get;?set;?} }在使用上是完全一樣的。唯一的區別是,如果映射類中不寫_id,則MongoDB自動添加_id時,會用ObjectId作為這個字段的數據類型。
ObjectId是一個全局唯一的數據。
當然,MongoDB允許使用其它類型的數據作為ID,例如:string,int,long,GUID等,但這就需要你自己去保證這些數據不超限并且唯一。
例如,我們可以寫成:
public?class?CollectionModel {public?long?_id?{?get;?set;?}public?string?title?{?get;?set;?}public?string?content?{?get;?set;?} }?
我們也可以在類中修改_id名稱為別的內容,但需要加一個描述屬性BsonId:
public?class?CollectionModel {[BsonId]public?ObjectId?topic_id?{?get;?set;?}public?string?title?{?get;?set;?}public?string?content?{?get;?set;?} }這兒特別要注意:BsonId屬性會告訴映射,topic_id就是這個文檔數據的ID。MongoDB在保存時,會將這個topic_id轉成_id保存到數據集中。
在MongoDB數據集中,ID字段的名稱固定叫_id。為了代碼的閱讀方便,可以在類中改為別的名稱,但這不會影響MongoDB中存放的ID名稱。
?
修改Demo代碼:
private?static?async?Task?Demo() {CollectionModel?new_item?=?new?CollectionModel(){title?=?"Demo",content?=?"Demo?content",};await?_collection.InsertOneAsync(new_item); }跑一下Demo,看看保存的結果:
{?"_id"?:?ObjectId("5ef1e1b1bc1e18086afe3183"),?"title"?:?"Demo",?"content"?:?"Demo?content" }2. 簡單字段
就是常規的數據字段,直接寫就成。
public?class?CollectionModel {[BsonId]public?ObjectId?topic_id?{?get;?set;?}public?string?title?{?get;?set;?}public?string?content?{?get;?set;?}public?int?favor?{?get;?set;?} }保存后的數據:
{?"_id"?:?ObjectId("5ef1e9caa9d16208de2962bb"),?"title"?:?"Demo",?"content"?:?"Demo?content",?"favor"?:?NumberInt(100) }3. 一個的特殊的類型 - Decimal
說Decimal特殊,是因為MongoDB在早期,是不支持Decimal的。直到MongoDB v3.4開始,數據庫才正式支持Decimal。
所以,如果使用的是v3.4以后的版本,可以直接使用,而如果是以前的版本,需要用以下的方式:
[BsonRepresentation(BsonType.Double,?AllowTruncation?=?true)] public?decimal?price?{?get;?set;?}其實就是把Decimal通過映射,轉為Double存儲。
4. 類字段
把類作為一個數據集的一個字段。這是MongoDB作為文檔NoSQL數據庫的特色。這樣可以很方便的把相關的數據組織到一條記錄中,方便展示時的查詢。
我們在項目中添加兩個類Contact和Author:
public?class?Contact {public?string?mobile?{?get;?set;?} } public?class?Author {public?string?name?{?get;?set;?}public?List<Contact>?contacts?{?get;?set;?} }然后,把Author加到CollectionModel中:
public?class?CollectionModel {[BsonId]public?ObjectId?topic_id?{?get;?set;?}public?string?title?{?get;?set;?}public?string?content?{?get;?set;?}public?int?favor?{?get;?set;?}public?Author?author?{?get;?set;?} }嗯,開始變得有點復雜了。
完善Demo代碼:
private?static?async?Task?Demo() {CollectionModel?new_item?=?new?CollectionModel(){title?=?"Demo",content?=?"Demo?content",favor?=?100,author?=?new?Author{name?=?"WangPlus",contacts?=?new?List<Contact>(),}};Contact?contact_item1?=?new?Contact(){mobile?=?"13800000000",};Contact?contact_item2?=?new?Contact(){mobile?=?"13811111111",};new_item.author.contacts.Add(contact_item1);new_item.author.contacts.Add(contact_item2);await?_collection.InsertOneAsync(new_item); }保存的數據是這樣的:
{?"_id"?:?ObjectId("5ef1e635ce129908a22dfb5e"),?"title"?:?"Demo",?"content"?:?"Demo?content",?"favor"?:?NumberInt(100),"author"?:?{"name"?:?"WangPlus",?"contacts"?:?[{"mobile"?:?"13800000000"},?{"mobile"?:?"13811111111"}]} }這樣的數據結構,用著不要太爽!
5. 枚舉字段
枚舉字段在使用時,跟類字段相似。
創建一個枚舉TagEnumeration:
public?enum?TagEnumeration {CSharp?=?1,Python?=?2, }加到CollectionModel中:
public?class?CollectionModel {[BsonId]public?ObjectId?topic_id?{?get;?set;?}public?string?title?{?get;?set;?}public?string?content?{?get;?set;?}public?int?favor?{?get;?set;?}public?Author?author?{?get;?set;?}public?TagEnumeration?tag?{?get;?set;?} }修改Demo代碼:
private?static?async?Task?Demo() {CollectionModel?new_item?=?new?CollectionModel(){title?=?"Demo",content?=?"Demo?content",favor?=?100,author?=?new?Author{name?=?"WangPlus",contacts?=?new?List<Contact>(),},tag?=?TagEnumeration.CSharp,};/*?后邊代碼略過?*/ }運行后看數據:
{?"_id"?:?ObjectId("5ef1eb87cbb6b109031fcc31"),?"title"?:?"Demo",?"content"?:?"Demo?content",?"favor"?:?NumberInt(100),?"author"?:?{"name"?:?"WangPlus",?"contacts"?:?[{"mobile"?:?"13800000000"},?{"mobile"?:?"13811111111"}]},?"tag"?:?NumberInt(1) }在這里,tag保存了枚舉的值。
我們也可以保存枚舉的字符串。只要在CollectionModel中,tag聲明上加個屬性:
public?class?CollectionModel {[BsonId]public?ObjectId?topic_id?{?get;?set;?}public?string?title?{?get;?set;?}public?string?content?{?get;?set;?}public?int?favor?{?get;?set;?}public?Author?author?{?get;?set;?}[BsonRepresentation(BsonType.String)]public?TagEnumeration?tag?{?get;?set;?} }數據會變成:
{?"_id"?:?ObjectId("5ef1ec448f1d540919d15904"),?"title"?:?"Demo",?"content"?:?"Demo?content",?"favor"?:?NumberInt(100),?"author"?:?{"name"?:?"WangPlus",?"contacts"?:?[{"mobile"?:?"13800000000"},?{"mobile"?:?"13811111111"}]},?"tag"?:?"CSharp" }6. 日期字段
日期字段會稍微有點坑。
這個坑其實并不源于MongoDB,而是源于C#的DateTime類。我們知道,時間根據時區不同,時間也不同。而DateTime并不準確描述時區的時間。
我們先在CollectionModel中增加一個時間字段:
public?class?CollectionModel {[BsonId]public?ObjectId?topic_id?{?get;?set;?}public?string?title?{?get;?set;?}public?string?content?{?get;?set;?}public?int?favor?{?get;?set;?}public?Author?author?{?get;?set;?}[BsonRepresentation(BsonType.String)]public?TagEnumeration?tag?{?get;?set;?}public?DateTime?post_time?{?get;?set;?} }修改Demo:
private?static?async?Task?Demo() {CollectionModel?new_item?=?new?CollectionModel(){/*?前邊代碼略過?*/post_time?=?DateTime.Now,?/*?2020-06-23T20:12:40.463+0000?*/};/*?后邊代碼略過?*/ }運行看數據:
{?"_id"?:?ObjectId("5ef1f1b9a75023095e995d9f"),?"title"?:?"Demo",?"content"?:?"Demo?content",?"favor"?:?NumberInt(100),?"author"?:?{"name"?:?"WangPlus",?"contacts"?:?[{"mobile"?:?"13800000000"},?{"mobile"?:?"13811111111"}]},?"tag"?:?"CSharp",?"post_time"?:?ISODate("2020-06-23T12:12:40.463+0000") }對比代碼時間和數據時間,會發現這兩個時間差了8小時 - 正好的中國的時區時間。
?
MongoDB規定,在數據集中存儲時間時,只會保存UTC時間。
如果只是保存(像上邊這樣),或者查詢時使用時間作為條件(例如查詢post_time < DateTime.Now的數據)時,是可以使用的,不會出現問題。
但是,如果是查詢結果中有時間字段,那這個字段,會被DateTime默認設置為DateTimeKind.Unspecified類型。而這個類型,是無時區信息的,輸出顯示時,會造成混亂。
為了避免這種情況,在進行時間字段的映射時,需要加上屬性:
[BsonDateTimeOptions(Kind?=?DateTimeKind.Local)] public?DateTime?post_time?{?get;?set;?}這樣做,會強制DateTime類型的字段為DateTimeKind.Local類型。這時候,從顯示到使用就正確了。
?
但是,別高興的太早,這兒還有一個但是。
這個但是是這樣的:數據集中存放的是UTC時間,跟我們正常的時間有8小時時差,如果我們需要按日統計,比方每天的銷售額/點擊量,怎么搞?上面的方式,解決不了。
當然,基于MongoDB自由的字段處理,可以把需要統計的字段,按年月日時分秒拆開存放,像下面這樣的:
class?Post_Time {public?int?year?{?get;?set;?}public?int?month?{?get;?set;?}public?int?day?{?get;?set;?}public?int?hour?{?get;?set;?}public?int?minute?{?get;?set;?}public?int?second?{?get;?set;?} }能解決,但是Low哭了有沒有?
?
下面,終極方案來了。它就是:改寫MongoDB中對于DateTime字段的序列化類。當當當~~~
先創建一個類MyDateTimeSerializer:
public?class?MyDateTimeSerializer?:?DateTimeSerializer {public?override?DateTime?Deserialize(BsonDeserializationContext?context,?BsonDeserializationArgs?args){var?obj?=?base.Deserialize(context,?args);return?new?DateTime(obj.Ticks,?DateTimeKind.Unspecified);}public?override?void?Serialize(BsonSerializationContext?context,?BsonSerializationArgs?args,?DateTime?value){var?utcValue?=?new?DateTime(value.Ticks,?DateTimeKind.Utc);base.Serialize(context,?args,?utcValue);} }代碼簡單,一看就懂。
注意,使用這個方法,上邊那個對于時間加的屬性[BsonDateTimeOptions(Kind = DateTimeKind.Local)]一定不要添加,要不然就等著哭吧:P
創建完了,怎么用?
如果你只想對某個特定映射的特定字段使用,比方只對CollectionModel的post_time字段來使用,可以這么寫:
[BsonSerializer(typeof(MyDateTimeSerializer))] public?DateTime?post_time?{?get;?set;?}或者全局使用:
BsonSerializer.RegisterSerializer(typeof(DateTime),?new?MongoDBDateTimeSerializer());BsonSerializer是MongoDB.Driver的全局對象。所以這個代碼,可以放到使用數據庫前的任何地方。例如在Demo中,我放在Main里了:
static?async?Task?Main(string[]?args) {BsonSerializer.RegisterSerializer(typeof(DateTime),?new?MyDateTimeSerializer());await?Demo();Console.ReadKey(); }這回看數據,數據集中的post_time跟當前時間顯示完全一樣了,你統計,你分組,可以隨便霍霍了。
7. Dictionary字段
這個需求很奇怪。我們希望在一個Key-Value的文檔中,保存一個Key-Value的數據。但這個需求又是真實存在的,比方保存一個用戶的標簽和標簽對應的命中次數。
數據聲明很簡單:
public?Dictionary<string,?int>?extra_info?{?get;?set;?}MongoDB定義了三種保存屬性:Document、ArrayOfDocuments、ArrayOfArrays,默認是Document。
屬性寫法是這樣的:
[BsonDictionaryOptions(DictionaryRepresentation.ArrayOfDocuments)] public?Dictionary<string,?int>?extra_info?{?get;?set;?}這三種屬性下,保存在數據集中的數據結構有區別。
DictionaryRepresentation.Document:
{?"extra_info"?:?{"type"?:?NumberInt(1),?"mode"?:?NumberInt(2)} }DictionaryRepresentation.ArrayOfDocuments:
{?"extra_info"?:?[{"k"?:?"type",?"v"?:?NumberInt(1)},?{"k"?:?"mode",?"v"?:?NumberInt(2)}] }DictionaryRepresentation.ArrayOfArrays:
{?"extra_info"?:?[["type",?NumberInt(1)],?["mode",?NumberInt(2)]] }這三種方式,從數據保存上并沒有什么區別,但從查詢來講,如果這個字段需要進行查詢,那三種方式區別很大。
如果采用BsonDocument方式查詢,DictionaryRepresentation.Document無疑是寫著最方便的。
如果用Builder方式查詢,DictionaryRepresentation.ArrayOfDocuments是最容易寫的。
DictionaryRepresentation.ArrayOfArrays就算了。數組套數組,查詢條件寫死人。
我自己在使用時,多數情況用DictionaryRepresentation.ArrayOfDocuments。
五、其它映射屬性
上一章介紹了數據映射的完整內容。除了這些內容,MongoDB還給出了一些映射屬性,供大家看心情使用。
1. BsonElement屬性
這個屬性是用來改數據集中的字段名稱用的。
看代碼:
[BsonElement("pt")] public?DateTime?post_time?{?get;?set;?}在不加BsonElement的情況下,通過數據映射寫到數據集中的文檔,字段名就是變量名,上面這個例子,字段名就是post_time。
加上BsonElement后,數據集中的字段名會變為pt。
2. BsonDefaultValue屬性
看名稱就知道,這是用來設置字段的默認值的。
看代碼:
[BsonDefaultValue("This?is?a?default?title")] public?string?title?{?get;?set;?}當寫入的時候,如果映射中不傳入值,則數據庫會把這個默認值存到數據集中。
3. BsonRepresentation屬性
這個屬性是用來在映射類中的數據類型和數據集中的數據類型做轉換的。
看代碼:
[BsonRepresentation(BsonType.String)] public?int?favor?{?get;?set;?}這段代表表示,在映射類中,favor字段是int類型的,而存到數據集中,會保存為string類型。
前邊Decimal轉換和枚舉轉換,就是用的這個屬性。
4. BsonIgnore屬性
這個屬性用來忽略某些字段。忽略的意思是:映射類中某些字段,不希望被保存到數據集中。
看代碼:
[BsonIgnore] public?string?ignore_string?{?get;?set;?}這樣,在保存數據時,字段ignore_string就不會被保存到數據集中。
六、總結
數據映射本身沒什么新鮮的內容,但在MongoDB中,如果用好了映射,開發過程從效率到爽的程度,都不是SQL可以相比的。正所謂:
一入Mongo深似海,從此SQL是路人。
?
謝謝大家!
(全文完)
?
本文的配套代碼在https://github.com/humornif/Demo-Code/tree/master/0015/demo
點「在看」,讓更多人因你而受益
↘ ?↘ ?↘
總結
以上是生活随笔為你收集整理的MongoDB via Dotnet Core数据映射详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .NET 5 尝鲜 - 开源项目Term
- 下一篇: FreeSql.Generator命令行