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。
建立工程:
% 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);
}
}
程式碼簡單,一看就懂。
注意