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操作。
下面,對資料對映的各個部分,我會逐個說明。
為了防止不提供原網址的轉載,特在這裡加上原文連結:https://www.cnblogs.com/tiger-wang/p/13185605.html
二、開發環境&基礎工程
這個Demo的開發環境是:Mac + VS Code + Dotnet Core 3.1.2。
建立工程:
%dotnetnewsln-odemo
Thetemplate"SolutionFile"wascreatedsuccessfully.
%cddemo
%dotnetnewconsole-odemo
Thetemplate"ConsoleApplication"wascreatedsuccessfully.
Processingpost-creationactions...
Running'dotnetrestore'ondemo/demo.csproj...
Determiningprojectstorestore...
Restoreddemo/demo/demo.csproj(in162ms).
Restoresucceeded.
%dotnetslnadddemo/demo.csproj
Project`demo/demo.csproj`addedtothesolution.
建立工程完成。
下面,增加包mongodb.driver
到工程:
%cddemo
%dotnetaddpackagemongodb.driver
Determiningprojectstorestore...
info:AddingPackageReferenceforpackage'mongodb.driver'intoproject'demo/demo/demo.csproj'.
info:Committingrestore...
info:Writingassetsfiletodisk.Path:demo/demo/obj/project.assets.json
log:Restored/demo/demo/demo.csproj(in6.01sec).
專案準備完成。
看一下目錄結構:
%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
,現在是個空類,後面所有的資料對映相關內容會在這個類進行說明:
publicclassCollectionModel
{
}
並修改Program.cs
,準備Demo
方法,以及連線資料庫:
classProgram
{
privateconststringMongoDBConnection="mongodb://localhost:27031/admin";
privatestaticIMongoClient_client=newMongoClient(MongoDBConnection);
privatestaticIMongoDatabase_database=_client.GetDatabase("Test");
privatestaticIMongoCollection<CollectionModel>_collection=_database.GetCollection<CollectionModel>("TestCollection");
staticasyncTaskMain(string[]args)
{
awaitDemo();
Console.ReadKey();
}
privatestaticasyncTaskDemo()
{
}
}
四、欄位對映
從上面的程式碼中,我們看到,在生成Collection
物件時,用到了CollectionModel
:
IMongoDatabase_database=_client.GetDatabase("Test");
IMongoCollection<CollectionModel>_collection=_database.GetCollection<CollectionModel>("TestCollection");
這兩行,其實就完成了一個對映的工作:把MongoDB
中,Test
資料庫下,TestCollection
資料集(就是SQL中的資料表),對映到CollectionModel
這個資料類中。換句話說,就是用CollectionModel
這個類,來完成對資料集TestCollection
的所有操作。
保持CollectionModel
為空,我們往資料庫寫入一行資料:
privatestaticasyncTaskDemo()
{
CollectionModelnew_item=newCollectionModel();
await_collection.InsertOneAsync(new_item);
}
執行,看一下寫入的資料:
{
"_id":ObjectId("5ef1d8325327fd4340425ac9")
}
OK,我們已經寫進去一條資料了。因為對映類是空的,所以寫入的資料,也只有_id
一行內容。
但是,為什麼會有一個_id
呢?
1. ID欄位
MongoDB資料集中存放的資料,稱之為檔案(Document
)。每個檔案在存放時,都需要有一個ID,而這個ID的名稱,固定叫_id
。
當我們建立對映時,如果給出_id
欄位,則MongoDB會採用這個ID做為這個檔案的ID,如果不給出,MongoDB會自動新增一個_id
欄位。
例如:
publicclassCollectionModel
{
publicObjectId_id{get;set;}
publicstringtitle{get;set;}
publicstringcontent{get;set;}
}
和
publicclassCollectionModel
{
publicstringtitle{get;set;}
publicstringcontent{get;set;}
}
在使用上是完全一樣的。唯一的區別是,如果對映類中不寫_id
,則MongoDB自動新增_id
時,會用ObjectId
作為這個欄位的資料型別。
ObjectId
是一個全域性唯一的資料。
當然,MongoDB允許使用其它型別的資料作為ID,例如:string
,int
,long
,GUID
等,但這就需要你自己去保證這些資料不超限並且唯一。
例如,我們可以寫成:
publicclassCollectionModel
{
publiclong_id{get;set;}
publicstringtitle{get;set;}
publicstringcontent{get;set;}
}
我們也可以在類中修改_id
名稱為別的內容,但需要加一個描述屬性BsonId
:
publicclassCollectionModel
{
[BsonId]
publicObjectIdtopic_id{get;set;}
publicstringtitle{get;set;}
publicstringcontent{get;set;}
}
這兒特別要注意:BsonId
屬性會告訴對映,topic_id
就是這個檔案資料的ID。MongoDB在儲存時,會將這個topic_id
轉成_id
儲存到資料集中。
在MongoDB資料集中,ID欄位的名稱固定叫_id
。為了程式碼的閱讀方便,可以在類中改為別的名稱,但這不會影響MongoDB中存放的ID名稱。
修改Demo程式碼:
privatestaticasyncTaskDemo()
{
CollectionModelnew_item=newCollectionModel()
{
title="Demo",
content="Democontent",
};
await_collection.InsertOneAsync(new_item);
}
跑一下Demo,看看儲存的結果:
{
"_id":ObjectId("5ef1e1b1bc1e18086afe3183"),
"title":"Demo",
"content":"Democontent"
}
2. 簡單欄位
就是常規的資料欄位,直接寫就成。
publicclassCollectionModel
{
[BsonId]
publicObjectIdtopic_id{get;set;}
publicstringtitle{get;set;}
publicstringcontent{get;set;}
publicintfavor{get;set;}
}
儲存後的資料:
{
"_id":ObjectId("5ef1e9caa9d16208de2962bb"),
"title":"Demo",
"content":"Democontent",
"favor":NumberInt(100)
}
3. 一個的特殊的型別 - Decimal
說Decimal特殊,是因為MongoDB在早期,是不支援Decimal的。直到MongoDB v3.4開始,資料庫才正式支援Decimal。
所以,如果使用的是v3.4以後的版本,可以直接使用,而如果是以前的版本,需要用以下的方式:
[BsonRepresentation(BsonType.Double,AllowTruncation=true)]
publicdecimalprice{get;set;}
其實就是把Decimal通過對映,轉為Double儲存。
4. 類欄位
把類作為一個資料集的一個欄位。這是MongoDB作為檔案NoSQL資料庫的特色。這樣可以很方便的把相關的資料組織到一條記錄中,方便展示時的查詢。
我們在專案中新增兩個類Contact
和Author
:
publicclassContact
{
publicstringmobile{get;set;}
}
publicclassAuthor
{
publicstringname{get;set;}
publicList<Contact>contacts{get;set;}
}
然後,把Author
加到CollectionModel
中:
publicclassCollectionModel
{
[BsonId]
publicObjectIdtopic_id{get;set;}
publicstringtitle{get;set;}
publicstringcontent{get;set;}
publicintfavor{get;set;}
publicAuthorauthor{get;set;}
}
嗯,開始變得有點複雜了。
完善Demo程式碼:
privatestaticasyncTaskDemo()
{
CollectionModelnew_item=newCollectionModel()
{
title="Demo",
content="Democontent",
favor=100,
author=newAuthor
{
name="WangPlus",
contacts=newList<Contact>(),
}
};
Contactcontact_item1=newContact()
{
mobile="13800000000",
};
Contactcontact_item2=newContact()
{
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":"Democontent",
"favor":NumberInt(100),
"author":{
"name":"WangPlus",
"contacts":[
{
"mobile":"13800000000"
},
{
"mobile":"13811111111"
}
]
}
}
這樣的資料結構,用著不要太爽!
5. 列舉欄位
列舉欄位在使用時,跟類欄位相似。
建立一個列舉TagEnumeration
:
publicenumTagEnumeration
{
CSharp=1,
Python=2,
}
加到CollectionModel
中:
publicclassCollectionModel
{
[BsonId]
publicObjectIdtopic_id{get;set;}
publicstringtitle{get;set;}
publicstringcontent{get;set;}
publicintfavor{get;set;}
publicAuthorauthor{get;set;}
publicTagEnumerationtag{get;set;}
}
修改Demo程式碼:
privatestaticasyncTaskDemo()
{
CollectionModelnew_item=newCollectionModel()
{
title="Demo",
content="Democontent",
favor=100,
author=newAuthor
{
name="WangPlus",
contacts=newList<Contact>(),
},
tag=TagEnumeration.CSharp,
};
/*後邊程式碼略過*/
}
執行後看資料:
{
"_id":ObjectId("5ef1eb87cbb6b109031fcc31"),
"title":"Demo",
"content":"Democontent",
"favor":NumberInt(100),
"author":{
"name":"WangPlus",
"contacts":[
{
"mobile":"13800000000"
},
{
"mobile":"13811111111"
}
]
},
"tag":NumberInt(1)
}
在這裡,tag
儲存了列舉的值。
我們也可以儲存列舉的字串。只要在CollectionModel
中,tag
宣告上加個屬性:
publicclassCollectionModel
{
[BsonId]
publicObjectIdtopic_id{get;set;}
publicstringtitle{get;set;}
publicstringcontent{get;set;}
publicintfavor{get;set;}
publicAuthorauthor{get;set;}
[BsonRepresentation(BsonType.String)]
publicTagEnumerationtag{get;set;}
}
資料會變成:
{
"_id":ObjectId("5ef1ec448f1d540919d15904"),
"title":"Demo",
"content":"Democontent",
"favor":NumberInt(100),
"author":{
"name":"WangPlus",
"contacts":[
{
"mobile":"13800000000"
},
{
"mobile":"13811111111"
}
]
},
"tag":"CSharp"
}
6. 日期欄位
日期欄位會稍微有點坑。
這個坑其實並不源於MongoDB,而是源於C#的DateTime
類。我們知道,時間根據時區不同,時間也不同。而DateTime
並不準確描述時區的時間。
我們先在CollectionModel
中增加一個時間欄位:
publicclassCollectionModel
{
[BsonId]
publicObjectIdtopic_id{get;set;}
publicstringtitle{get;set;}
publicstringcontent{get;set;}
publicintfavor{get;set;}
publicAuthorauthor{get;set;}
[BsonRepresentation(BsonType.String)]
publicTagEnumerationtag{get;set;}
publicDateTimepost_time{get;set;}
}
修改Demo:
privatestaticasyncTaskDemo()
{
CollectionModelnew_item=newCollectionModel()
{
/*前邊程式碼略過*/
post_time=DateTime.Now,/*2020-06-23T20:12:40.463+0000*/
};
/*後邊程式碼略過*/
}
執行看資料:
{
"_id":ObjectId("5ef1f1b9a75023095e995d9f"),
"title":"Demo",
"content":"Democontent",
"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)]
publicDateTimepost_time{get;set;}
這樣做,會強制DateTime
型別的欄位為DateTimeKind.Local
型別。這時候,從顯示到使用就正確了。
但是,別高興的太早,這兒還有一個但是。
這個但是是這樣的:資料集中存放的是UTC時間,跟我們正常的時間有8小時時差,如果我們需要按日統計,比方每天的銷售額/點選量,怎麼搞?上面的方式,解決不了。
當然,基於MongoDB自由的欄位處理,可以把需要統計的欄位,按年月日時分秒拆開存放,像下面這樣的:
classPost_Time
{
publicintyear{get;set;}
publicintmonth{get;set;}
publicintday{get;set;}
publicinthour{get;set;}
publicintminute{get;set;}
publicintsecond{get;set;}
}
能解決,但是Low哭了有沒有?
下面,終極方案來了。它就是:改寫MongoDB中對於DateTime
欄位的序列化類。噹噹噹~~~
先建立一個類MyDateTimeSerializer
:
publicclassMyDateTimeSerializer:DateTimeSerializer
{
publicoverrideDateTimeDeserialize(BsonDeserializationContextcontext,BsonDeserializationArgsargs)
{
varobj=base.Deserialize(context,args);
returnnewDateTime(obj.Ticks,DateTimeKind.Unspecified);
}
publicoverridevoidSerialize(BsonSerializationContextcontext,BsonSerializationArgsargs,DateTimevalue)
{
varutcValue=newDateTime(value.Ticks,DateTimeKind.Utc);
base.Serialize(context,args,utcValue);
}
}
程式碼簡單,一看就懂。
注意,使用這個方法,上邊那個對於時間加的屬性[BsonDateTimeOptions(Kind = DateTimeKind.Local)]
一定不要新增,要不然就等著哭吧:P
建立完了,怎麼用?
如果你只想對某個特定對映的特定欄位使用,比方只對CollectionModel
的post_time
欄位來使用,可以這麼寫:
[BsonSerializer(typeof(MyDateTimeSerializer))]
publicDateTimepost_time{get;set;}
或者全域性使用:
BsonSerializer.RegisterSerializer(typeof(DateTime),newMongoDBDateTimeSerializer());
BsonSerializer
是MongoDB.Driver的全域性物件。所以這個程式碼,可以放到使用資料庫前的任何地方。例如在Demo中,我放在Main
裡了:
staticasyncTaskMain(string[]args)
{
BsonSerializer.RegisterSerializer(typeof(DateTime),newMyDateTimeSerializer());
awaitDemo();
Console.ReadKey();
}
這回看資料,資料集中的post_time
跟當前時間顯示完全一樣了,你統計,你分組,可以隨便霍霍了。
7. Dictionary欄位
這個需求很奇怪。我們希望在一個Key-Value的檔案中,儲存一個Key-Value的資料。但這個需求又是真實存在的,比方儲存一個使用者的標籤和標籤對應的命中次數。
資料宣告很簡單:
publicDictionary<string,int>extra_info{get;set;}
MongoDB定義了三種儲存屬性:Document
、ArrayOfDocuments
、ArrayOfArrays
,預設是Document
。
屬性寫法是這樣的:
[BsonDictionaryOptions(DictionaryRepresentation.ArrayOfDocuments)]
publicDictionary<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")]
publicDateTimepost_time{get;set;}
在不加BsonElement
的情況下,通過資料對映寫到資料集中的檔案,欄位名就是變數名,上面這個例子,欄位名就是post_time
。
加上BsonElement
後,資料集中的欄位名會變為pt
。
2. BsonDefaultValue屬性
看名稱就知道,這是用來設定欄位的預設值的。
看程式碼:
[BsonDefaultValue("Thisisadefaulttitle")]
publicstringtitle{get;set;}
當寫入的時候,如果對映中不傳入值,則資料庫會把這個預設值存到資料集中。
3. BsonRepresentation屬性
這個屬性是用來在對映類中的資料型別和資料集中的資料型別做轉換的。
看程式碼:
[BsonRepresentation(BsonType.String)]
publicintfavor{get;set;}
這段代表表示,在對映類中,favor
欄位是int
型別的,而存到資料集中,會儲存為string
型別。
前邊Decimal
轉換和列舉轉換,就是用的這個屬性。
4. BsonIgnore屬性
這個屬性用來忽略某些欄位。忽略的意思是:對映類中某些欄位,不希望被儲存到資料集中。
看程式碼:
[BsonIgnore]
publicstringignore_string{get;set;}
這樣,在儲存資料時,欄位ignore_string
就不會被儲存到資料集中。
六、總結
資料對映本身沒什麼新鮮的內容,但在MongoDB中,如果用好了對映,開發過程從效率到爽的程度,都不是SQL可以相比的。正所謂:
一入Mongo深似海,從此SQL是路人。
謝謝大家!
(全文完)
本文的配套程式碼在https://github.com/humornif/Demo-Code/tree/master/0015/demo
微信公眾號:老王Plus 掃描二維碼,關注個人公眾號,可以第一時間得到最新的個人文章和內容推送 本文版權歸作者所有,轉載請保留此宣告和原文連結 |