用好數(shù)據(jù)映射,MongoDB via Dotnet Core開發(fā)變會成一件超級快樂的事。
一、前言
MongoDB這幾年已經(jīng)成為NoSQL的頭部數(shù)據(jù)庫。
由于MongoDB free schema的特性,使得它在互聯(lián)網(wǎng)應(yīng)用方面優(yōu)于常規(guī)數(shù)據(jù)庫,成為了相當(dāng)一部分大廠的主數(shù)據(jù)選擇;而它的快速布署和開發(fā)簡單的特點,也吸引著大量小開發(fā)團(tuán)隊的支持。
關(guān)于MongoDB快速布署,我在15分鐘從零開始搭建支持10w+用戶的生產(chǎn)環(huán)境(二)里有寫,需要了可以去看看。
作為一個數(shù)據(jù)庫,基本的操作就是CRUD。MongoDB的CRUD,不使用SQL來寫,而是提供了更簡單的方式。
方式一、BsonDocument方式
BsonDocument方式,適合能熟練使用MongoDB Shell的開發(fā)者。MongoDB Driver提供了完全覆蓋Shell命令的各種方式,來處理用戶的CRUD操作。
這種方法自由度很高,可以在不需要知道完整數(shù)據(jù)集結(jié)構(gòu)的情況下,完成數(shù)據(jù)庫的CRUD操作。
方式二、數(shù)據(jù)映射方式
數(shù)據(jù)映射是最常用的一種方式。準(zhǔn)備好需要處理的數(shù)據(jù)類,直接把數(shù)據(jù)類映射到MongoDB,并對數(shù)據(jù)集進(jìn)行CRUD操作。
下面,對數(shù)據(jù)映射的各個部分,我會逐個說明。
為了防止不提供原網(wǎng)址的轉(zhuǎn)載,特在這里加上原文鏈接:https://www.cnblogs.com/tiger-wang/p/13185605.html
二、開發(fā)環(huán)境&基礎(chǔ)工程
這個Demo的開發(fā)環(huán)境是: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.driver
Determining 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).
項目準(zhǔn)備完成。
看一下目錄結(jié)構(gòu):
% 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官方的數(shù)據(jù)庫SDK,從Nuget上安裝即可。
三、Demo準(zhǔn)備工作
創(chuàng)建數(shù)據(jù)映射的模型類CollectionModel.cs,現(xiàn)在是個空類,后面所有的數(shù)據(jù)映射相關(guān)內(nèi)容會在這個類進(jìn)行說明:
public class CollectionModel
{
}
并修改Program.cs,準(zhǔn)備Demo方法,以及連接數(shù)據(jù)庫:
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數(shù)據(jù)庫下,TestCollection數(shù)據(jù)集(就是SQL中的數(shù)據(jù)表),映射到CollectionModel這個數(shù)據(jù)類中。換句話說,就是用CollectionModel這個類,來完成對數(shù)據(jù)集TestCollection的所有操作。
保持CollectionModel為空,我們往數(shù)據(jù)庫寫入一行數(shù)據(jù):
private static async Task Demo()
{
CollectionModel new_item = new CollectionModel();
await _collection.InsertOneAsync(new_item);
}
執(zhí)行,看一下寫入的數(shù)據(jù):
{
"_id" : ObjectId("5ef1d8325327fd4340425ac9")
}
OK,我們已經(jīng)寫進(jìn)去一條數(shù)據(jù)了。因為映射類是空的,所以寫入的數(shù)據(jù),也只有_id一行內(nèi)容。
但是,為什么會有一個_id呢?
1. ID字段
MongoDB數(shù)據(jù)集中存放的數(shù)據(jù),稱之為文檔(Document)。每個文檔在存放時,都需要有一個ID,而這個ID的名稱,固定叫_id。
當(dāng)我們建立映射時,如果給出_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; }
}
在使用上是完全一樣的。唯一的區(qū)別是,如果映射類中不寫_id,則MongoDB自動添加_id時,會用ObjectId作為這個字段的數(shù)據(jù)類型。
ObjectId是一個全局唯一的數(shù)據(jù)。
當(dāng)然,MongoDB允許使用其它類型的數(shù)據(jù)作為ID,例如:string,int,long,GUID等,但這就需要你自己去保證這些數(shù)據(jù)不超限并且唯一。
例如,我們可以寫成:
public class CollectionModel
{
public long _id { get; set; }
public string title { get; set; }
public string content { get; set; }
}
我們也可以在類中修改_id名稱為別的內(nèi)容,但需要加一個描述屬性BsonId:
public class CollectionModel
{
[BsonId]
public ObjectId topic_id { get; set; }
public string title { get; set; }
public string content { get; set; }
}
這兒特別要注意:BsonId屬性會告訴映射,topic_id就是這個文檔數(shù)據(jù)的ID。MongoDB在保存時,會將這個topic_id轉(zhuǎn)成_id保存到數(shù)據(jù)集中。
在MongoDB數(shù)據(jù)集中,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,看看保存的結(jié)果:
{
"_id" : ObjectId("5ef1e1b1bc1e18086afe3183"),
"title" : "Demo",
"content" : "Demo content"
}
2. 簡單字段
就是常規(guī)的數(shù)據(jù)字段,直接寫就成。
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; }
}
保存后的數(shù)據(jù):
{
"_id" : ObjectId("5ef1e9caa9d16208de2962bb"),
"title" : "Demo",
"content" : "Demo content",
"favor" : NumberInt(100)
}
3. 一個的特殊的類型 - Decimal
說Decimal特殊,是因為MongoDB在早期,是不支持Decimal的。直到MongoDB v3.4開始,數(shù)據(jù)庫才正式支持Decimal。
所以,如果使用的是v3.4以后的版本,可以直接使用,而如果是以前的版本,需要用以下的方式:
[BsonRepresentation(BsonType.Double, AllowTruncation = true)]
public decimal price { get; set; }
其實就是把Decimal通過映射,轉(zhuǎn)為Double存儲。
4. 類字段
把類作為一個數(shù)據(jù)集的一個字段。這是MongoDB作為文檔NoSQL數(shù)據(jù)庫的特色。這樣可以很方便的把相關(guān)的數(shù)據(jù)組織到一條記錄中,方便展示時的查詢。
我們在項目中添加兩個類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; }
}
嗯,開始變得有點復(fù)雜了。
完善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);
}
保存的數(shù)據(jù)是這樣的:
{
"_id" : ObjectId("5ef1e635ce129908a22dfb5e"),
"title" : "Demo",
"content" : "Demo content",
"favor" : NumberInt(100),
"author" : {
"name" : "WangPlus",
"contacts" : [
{
"mobile" : "13800000000"
},
{
"mobile" : "13811111111"
}
]
}
}
這樣的數(shù)據(jù)結(jié)構(gòu),用著不要太爽!
5. 枚舉字段
枚舉字段在使用時,跟類字段相似。
創(chuàng)建一個枚舉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,
};
/* 后邊代碼略過 */
}
運行后看數(shù)據(jù):
{
"_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; }
}
數(shù)據(jù)會變成:
{
"_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類。我們知道,時間根據(jù)時區(qū)不同,時間也不同。而DateTime并不準(zhǔn)確描述時區(qū)的時間。
我們先在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 */
};
/* 后邊代碼略過 */
}
運行看數(shù)據(jù):
{
"_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")
}
對比代碼時間和數(shù)據(jù)時間,會發(fā)現(xiàn)這兩個時間差了8小時 - 正好的中國的時區(qū)時間。
MongoDB規(guī)定,在數(shù)據(jù)集中存儲時間時,只會保存UTC時間。
如果只是保存(像上邊這樣),或者查詢時使用時間作為條件(例如查詢post_time < DateTime.Now的數(shù)據(jù))時,是可以使用的,不會出現(xiàn)問題。
但是,如果是查詢結(jié)果中有時間字段,那這個字段,會被DateTime默認(rèn)設(shè)置為DateTimeKind.Unspecified類型。而這個類型,是無時區(qū)信息的,輸出顯示時,會造成混亂。
為了避免這種情況,在進(jìn)行時間字段的映射時,需要加上屬性:
[BsonDateTimeOptions(Kind = DateTimeKind.Local)]
public DateTime post_time { get; set; }
這樣做,會強(qiáng)制DateTime類型的字段為DateTimeKind.Local類型。這時候,從顯示到使用就正確了。
但是,別高興的太早,這兒還有一個但是。
這個但是是這樣的:數(shù)據(jù)集中存放的是UTC時間,跟我們正常的時間有8小時時差,如果我們需要按日統(tǒng)計,比方每天的銷售額/點擊量,怎么搞?上面的方式,解決不了。
當(dāng)然,基于MongoDB自由的字段處理,可以把需要統(tǒng)計的字段,按年月日時分秒拆開存放,像下面這樣的:
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字段的序列化類。當(dāng)當(dāng)當(dāng)~~~
先創(chuàng)建一個類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
創(chuàng)建完了,怎么用?
如果你只想對某個特定映射的特定字段使用,比方只對CollectionModel的post_time字段來使用,可以這么寫:
[BsonSerializer(typeof(MyDateTimeSerializer))]
public DateTime post_time { get; set; }
或者全局使用:
BsonSerializer.RegisterSerializer(typeof(DateTime), new MongoDBDateTimeSerializer());
BsonSerializer是MongoDB.Driver的全局對象。所以這個代碼,可以放到使用數(shù)據(jù)庫前的任何地方。例如在Demo中,我放在Main里了:
static async Task Main(string[] args)
{
BsonSerializer.RegisterSerializer(typeof(DateTime), new MyDateTimeSerializer());
await Demo();
Console.ReadKey();
}
這回看數(shù)據(jù),數(shù)據(jù)集中的post_time跟當(dāng)前時間顯示完全一樣了,你統(tǒng)計,你分組,可以隨便霍霍了。
7. Dictionary字段
這個需求很奇怪。我們希望在一個Key-Value的文檔中,保存一個Key-Value的數(shù)據(jù)。但這個需求又是真實存在的,比方保存一個用戶的標(biāo)簽和標(biāo)簽對應(yīng)的命中次數(shù)。
數(shù)據(jù)聲明很簡單:
public Dictionary<string, int> extra_info { get; set; }
MongoDB定義了三種保存屬性:Document、ArrayOfDocuments、ArrayOfArrays,默認(rèn)是Document。
屬性寫法是這樣的:
[BsonDictionaryOptions(DictionaryRepresentation.ArrayOfDocuments)]
public Dictionary<string, int> extra_info { get; set; }
這三種屬性下,保存在數(shù)據(jù)集中的數(shù)據(jù)結(jié)構(gòu)有區(qū)別。
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)
]
]
}
這三種方式,從數(shù)據(jù)保存上并沒有什么區(qū)別,但從查詢來講,如果這個字段需要進(jìn)行查詢,那三種方式區(qū)別很大。
如果采用BsonDocument方式查詢,DictionaryRepresentation.Document無疑是寫著最方便的。
如果用Builder方式查詢,DictionaryRepresentation.ArrayOfDocuments是最容易寫的。
DictionaryRepresentation.ArrayOfArrays就算了。數(shù)組套數(shù)組,查詢條件寫死人。
我自己在使用時,多數(shù)情況用DictionaryRepresentation.ArrayOfDocuments。
五、其它映射屬性
上一章介紹了數(shù)據(jù)映射的完整內(nèi)容。除了這些內(nèi)容,MongoDB還給出了一些映射屬性,供大家看心情使用。
1. BsonElement屬性
這個屬性是用來改數(shù)據(jù)集中的字段名稱用的。
看代碼:
[BsonElement("pt")]
public DateTime post_time { get; set; }
在不加BsonElement的情況下,通過數(shù)據(jù)映射寫到數(shù)據(jù)集中的文檔,字段名就是變量名,上面這個例子,字段名就是post_time。
加上BsonElement后,數(shù)據(jù)集中的字段名會變?yōu)?code style="font-size: inherit; line-height: inherit; word-wrap: break-word; padding: 2px 4px; border-radius: 4px; margin: 0 2px; color: rgba(233, 105, 0, 1); background-color: rgba(248, 248, 248, 1)">pt。
2. BsonDefaultValue屬性
看名稱就知道,這是用來設(shè)置字段的默認(rèn)值的。
看代碼:
[BsonDefaultValue("This is a default title")]
public string title { get; set; }
當(dāng)寫入的時候,如果映射中不傳入值,則數(shù)據(jù)庫會把這個默認(rèn)值存到數(shù)據(jù)集中。
3. BsonRepresentation屬性
這個屬性是用來在映射類中的數(shù)據(jù)類型和數(shù)據(jù)集中的數(shù)據(jù)類型做轉(zhuǎn)換的。
看代碼:
[BsonRepresentation(BsonType.String)]
public int favor { get; set; }
這段代表表示,在映射類中,favor字段是int類型的,而存到數(shù)據(jù)集中,會保存為string類型。
前邊Decimal轉(zhuǎn)換和枚舉轉(zhuǎn)換,就是用的這個屬性。
4. BsonIgnore屬性
這個屬性用來忽略某些字段。忽略的意思是:映射類中某些字段,不希望被保存到數(shù)據(jù)集中。
看代碼:
[BsonIgnore]
public string ignore_string { get; set; }
這樣,在保存數(shù)據(jù)時,字段ignore_string就不會被保存到數(shù)據(jù)集中。
六、總結(jié)
數(shù)據(jù)映射本身沒什么新鮮的內(nèi)容,但在MongoDB中,如果用好了映射,開發(fā)過程從效率到爽的程度,都不是SQL可以相比的。正所謂:
一入Mongo深似海,從此SQL是路人。
謝謝大家!
(全文完)
本文的配套代碼在https://github.com/humornif/Demo-Code/tree/master/0015/demo
|
本文版權(quán)歸作者所有,轉(zhuǎn)載請保留此聲明和原文鏈接 |