用好数据映射,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 方法,以及连接数据库:
Program { ????private?const?string?MongoDBConnection?=?"mongodb://localhost:27031/admin";
????static?IMongoClient?_client?=?new?MongoClient(MongoDBConnection); ????static?IMongoDatabase?_database?=?_client.GetDatabase("Test"); ????static?IMongoCollection<CollectionModel>?_collection?=?_database.GetCollection<CollectionModel>("TestCollection");
????static?async?Task?Main(string[]?args) ????{ ????????await?Demo(); ????????Console.ReadKey(); ????}
????() ????{ ????} }
四、字段映射
从上面的代码中,我们看到,在生成Collection 对象时,用到了CollectionModel :
IMongoDatabase?_database?=?_client.GetDatabase("Test"); IMongoCollection<CollectionModel>?_collection?=?_database.GetCollection<CollectionModel>("TestCollection");
这两行,其实就完成了一个映射的工作:把MongoDB 中,Test 数据库下,TestCollection 数据集(就是sql中的数据表),映射到CollectionModel 这个数据类中。换句话说,就是用CollectionModel 这个类,来完成对数据集TestCollection 的所有操作。
?
保持CollectionModel 为空,我们往数据库写入一行数据:
() { ????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 字段。
例如:
CollectionModel { ????public?ObjectId?_id?{?get;?set;?} ????public?string?title?{?get;?string?content?{?get;?set;?} }
和
set;?} }
在使用上是完全一样的。唯一的区别是,如果映射类中不写_id ,则MongoDB自动添加_id 时,会用ObjectId 作为这个字段的数据类型。
ObjectId 是一个全局唯一的数据。
当然,MongoDB允许使用其它类型的数据作为ID,例如:string ,int ,long ,GUID 等,但这就需要你自己去保证这些数据不超限并且唯一。
例如,我们可以写成:
public?long?_id?{?get;?set;?} }
?
我们也可以在类中修改_id 名称为别的内容,但需要加一个描述属性BsonId :
CollectionModel { ????[BsonId] ????public?ObjectId?topic_id?{?get;?set;?} }
这儿特别要注意:BsonId 属性会告诉映射,topic_id 就是这个文档数据的ID。MongoDB在保存时,会将这个topic_id 转成_id 保存到数据集中。
在MongoDB数据集中,ID字段的名称固定叫_id 。为了代码的阅读方便,可以在类中改为别的名称,但这不会影响MongoDB中存放的ID名称。
?
修改Demo代码:
new?CollectionModel() ????{ ????????title?=?"Demo", ????????content?=?"Demo?content", ????}; ????await?_collection.InsertOneAsync(new_item); }
跑一下Demo,看看保存的结果:
"5ef1e1b1bc1e18086afe3183"),? ????"title"?:?"content"?:?"Demo?content" }
2. 简单字段
就是常规的数据字段,直接写就成。
int?favor?{?get;?set;?} }
保存后的数据:
"5ef1e9caa9d16208de2962bb"),1); word-wrap: inherit !important; word-break: inherit !important">"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 :
Contact { ????string?mobile?{?get;?set;?} }
Author { ????string?name?{?get;?public?List<Contact>?contacts?{?get;?set;?} }
然后,把Author 加到CollectionModel 中:
public?Author?author?{?get;?set;?} }
嗯,开始变得有点复杂了。
完善Demo代码:
100, ????????author?=?new?Author ????????{ ????????????name?=?"WangPlus", ????????????contacts?=?new?List<Contact>(), ????????} ????};
????Contact?contact_item1?=?new?Contact() ????{ ????????mobile?=?"13800000000", ????}; ????Contact?contact_item2?=?"13822222111", ????}; ????new_item.author.contacts.Add(contact_item1); ????new_item.author.contacts.Add(contact_item2);
????await?_collection.InsertOneAsync(new_item); }
保存的数据是这样的:
"5ef1e635ce129908a22dfb5e"),1); word-wrap: inherit !important; word-break: inherit !important">100), ????"author"?:?{ ????????"name"?:?"contacts"?:?[ ????????????{ ????????????????"mobile"?:?"13800000000" ????????????},? ????????????{ ????????????????"13822222111" ????????????} ????????] ????} }
这样的数据结构,用着不要太爽!
5. 枚举字段
枚举字段在使用时,跟类字段相似。
创建一个枚举TagEnumeration :
enum?TagEnumeration { ????CSharp?=?1, ????Python?=?2, }
加到public?TagEnumeration?tag?{?get;?set;?} }
修改Demo代码:
/*?后边代码略过?*/ }
运行后看数据:
"5ef1eb87cbb6b109031fcc31"),1); word-wrap: inherit !important; word-break: inherit !important">"13822222111" ????????????} ????????] ????},1); word-wrap: inherit !important; word-break: inherit !important">"tag"?:?NumberInt(1) }
在这里,tag 保存了枚举的值。
我们也可以保存枚举的字符串。只要在CollectionModel 中,tag 声明上加个属性:
set;?} ????[BsonRepresentation(BsonType.String)] ????set;?} }
数据会变成:
"5ef1ec448f1d540919d15904"),1); word-wrap: inherit !important; word-break: inherit !important">"tag"?:?"CSharp" }
6. 日期字段
日期字段会稍微有点坑。
这个坑其实并不源于MongoDB,而是源于C#的DateTime 类。我们知道,时间根据时区不同,时间也不同。而DateTime 并不准确描述时区的时间。
我们先在CollectionModel 中增加一个时间字段:
public?DateTime?post_time?{?get;?set;?} }
修改Demo:
new?CollectionModel() ????{ ???????? ????????post_time?=?DateTime.Now,? ????}; ???? }
运行看数据:
"5ef1f1b9a75023095e995d9f"),1); word-wrap: inherit !important; word-break: inherit !important">"CSharp",1); word-wrap: inherit !important; word-break: inherit !important">"post_time"?:?ISODate("2020-06-23T12:12:40.463+0000") }
对比代码时间和数据时间,会发现这两个时间差了8小时 - 正好的中国的时区时间。
?
MongoDB规定,在数据集中存储时间时,只会保存UTC时间。
如果只是保存(像上边这样),或者查询时使用时间作为条件(例如查询post_time < DateTime.Now 的数据)时,是可以使用的,不会出现问题。
但是,如果是查询结果中有时间字段,那这个字段,会被DateTime 默认设置为DateTimeKind.Unspecified 类型。而这个类型,是无时区信息的,输出显示时,会造成混乱。
为了避免这种情况,在进行时间字段的映射时,需要加上属性:
[BsonDateTimeOptions(Kind?=?DateTimeKind.Local)] set;?}
这样做,会强制DateTime 类型的字段为DateTimeKind.Local 类型。这时候,从显示到使用就正确了。
?
但是,别高兴的太早,这儿还有一个但是。
这个但是是这样的:数据集中存放的是UTC时间,跟我们正常的时间有8小时时差,如果我们需要按日统计,比方每天的销售额/点击量,怎么搞?上面的方式,解决不了。
当然,基于MongoDB自由的字段处理,可以把需要统计的字段,按年月日时分秒拆开存放,像下面这样的:
Post_Time { ????int?year?{?get;?int?month?{?get;?int?day?{?get;?int?hour?{?get;?int?minute?{?get;?int?second?{?get;?set;?} }
能解决,但是Low哭了有没有?
?
下面,终极方案来了。它就是:改写MongoDB中对于DateTime 字段的序列化类。当当当~~~
先创建一个类MyDateTimeSerializer :
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))] set;?}
或者全局使用:
BsonSerializer.RegisterSerializer(typeof(DateTime),?new?MongoDBDateTimeSerializer());
BsonSerializer 是MongoDB.Driver的全局对象。所以这个代码,可以放到使用数据库前的任何地方。例如在Demo中,我放在Main 里了:
string[]?args) { ????BsonSerializer.RegisterSerializer(typeof(DateTime),1); word-wrap: inherit !important; word-break: inherit !important">new?MyDateTimeSerializer());
????(); ????Console.ReadKey(); }
这回看数据,数据集中的post_time 跟当前时间显示完全一样了,你统计,你分组,可以随便霍霍了。
7. Dictionary字段
这个需求很奇怪。我们希望在一个Key-Value的文档中,保存一个Key-Value的数据。但这个需求又是真实存在的,比方保存一个用户的标签和标签对应的命中次数。
数据声明很简单:
public?Dictionary<string,1); word-wrap: inherit !important; word-break: inherit !important">int>?extra_info?{?get;?set;?}
MongoDB定义了三种保存属性:Document 、ArrayOfDocuments 、ArrayOfArrays ,默认是Document 。
属性写法是这样的:
[BsonDictionaryOptions(DictionaryRepresentation.ArrayOfDocuments)] set;?}
这三种属性下,保存在数据集中的数据结构有区别。
DictionaryRepresentation.Document :
"extra_info"?:?{ ????????"type"?:?NumberInt(1),1); word-wrap: inherit !important; word-break: inherit !important">"mode"?:?NumberInt(2) ????} }
DictionaryRepresentation.ArrayOfDocuments :
"extra_info"?:?[ ????????{ ????????????"k"?:?"type",? ????????????"v"?:?NumberInt(1) ????????},? ????????{ ????????????"mode",1); word-wrap: inherit !important; word-break: inherit !important">2) ????????} ????] }
DictionaryRepresentation.ArrayOfArrays :
"extra_info"?:?[ ????????[ ????????????1) ????????],? ????????[ ????????????2) ????????] ????] }
这三种方式,从数据保存上并没有什么区别,但从查询来讲,如果这个字段需要进行查询,那三种方式区别很大。
如果采用BsonDocument方式查询,DictionaryRepresentation.Document 无疑是写着最方便的。
如果用Builder方式查询,DictionaryRepresentation.ArrayOfDocuments 是最容易写的。
DictionaryRepresentation.ArrayOfArrays 就算了。数组套数组,查询条件写死人。
我自己在使用时,多数情况用DictionaryRepresentation.ArrayOfDocuments 。
五、其它映射属性
上一章介绍了数据映射的完整内容。除了这些内容,MongoDB还给出了一些映射属性,供大家看心情使用。
1. BsonElement属性
这个属性是用来改数据集中的字段名称用的。
看代码:
[BsonElement("pt")] set;?}
在不加BsonElement 的情况下,通过数据映射写到数据集中的文档,字段名就是变量名,上面这个例子,字段名就是post_time 。
加上BsonElement 后,数据集中的字段名会变为pt 。
2. BsonDefaultValue属性
看名称就知道,这是用来设置字段的默认值的。
看代码:
[BsonDefaultValue("This?is?a?default?title")] set;?}
当写入的时候,如果映射中不传入值,则数据库会把这个默认值存到数据集中。
3. BsonRepresentation属性
这个属性是用来在映射类中的数据类型和数据集中的数据类型做转换的。
看代码:
[BsonRepresentation(BsonType.String)] set;?}
这段代表表示,在映射类中,favor 字段是int 类型的,而存到数据集中,会保存为string 类型。
前边Decimal 转换和枚举转换,就是用的这个属性。
4. BsonIgnore属性
这个属性用来忽略某些字段。忽略的意思是:映射类中某些字段,不希望被保存到数据集中。
看代码:
[BsonIgnore] string?ignore_string?{?get;?set;?}
这样,在保存数据时,字段ignore_string 就不会被保存到数据集中。
六、总结
数据映射本身没什么新鲜的内容,但在MongoDB中,如果用好了映射,开发过程从效率到爽的程度,都不是sql可以相比的。正所谓:
一入Mongo深似海,从此sql是路人。
?
谢谢大家!
(全文完)
?
本文的配套代码在https://github.com/humornif/Demo-Code/tree/master/0015/demo
?
?
|