用好資料映射,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,
建立工程:
% 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).
專案準備完成,
看一下目錄結構:
% 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
![]() |
微信公眾號:老王Plus 掃描二維碼,關注個人公眾號,可以第一時間得到最新的個人文章和內容推送 本文著作權歸作者所有,轉載請保留此宣告和原文鏈接 |
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/6430.html
標籤:.NET Core
上一篇:C# 人臉識別庫 0.1
下一篇:微服務中如何設計一個權限授權服務

