MongoDB这几年已经成为NoSQL的头部数据库。
由于MongoDB free schema的特性,使得它在互联网应用方面优于常规数据库,成为了相当一部分大厂的主数据选择;而它的快速布署和开发简单的特点,也吸引着大量小开发团队的支持。
关于MongoDB快速布署,我在15分钟从零开始搭建支持10w+用户的生产环境(二)里有写,需要了可以去看看。
作为一个数据库,基本的操作就是CRUD。MongoDB的CRUD,不使用SQL来写,而是提供了更简单的方式。
方式一、BsonDocument方式
BsonDocument方式,适合能熟练使用MongoDB Shell的开发者。MongoDB Driver提供了完全覆盖Shell命令的各种方式,来处理用户的CRUD操作。
这种方法自由度很高,可以在不需要知道完整数据集结构的情况下,完成数据库的CRUD操作。
方式二、数据映射方式
数据映射是最常用的一种方式。准备好需要处理的数据类,直接把数据类映射到MongoDB,并对数据集进行CRUD操作。
下面,对数据映射的各个部分,我会逐个说明。
这个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上安装即可。
创建数据映射的模型类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
呢?
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"
}
就是常规的数据字段,直接写就成。
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)
}
说Decimal特殊,是因为MongoDB在早期,是不支持Decimal的。直到MongoDB v3.4开始,数据库才正式支持Decimal。
所以,如果使用的是v3.4以后的版本,可以直接使用,而如果是以前的版本,需要用以下的方式:
[BsonRepresentation(BsonType.Double, AllowTruncation = true)]
public decimal price { get; set; }
其实就是把Decimal通过映射,转为Double存储。
把类作为一个数据集的一个字段。这是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"
}
]
}
}
这样的数据结构,用着不要太爽!
枚举字段在使用时,跟类字段相似。
创建一个枚举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"
}
日期字段会稍微有点坑。
这个坑其实并不源于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
跟当前时间显示完全一样了,你统计,你分组,可以随便霍霍了。
这个需求很奇怪。我们希望在一个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还给出了一些映射属性,供大家看心情使用。
这个属性是用来改数据集中的字段名称用的。
看代码:
[BsonElement("pt")]
public DateTime post_time { get; set; }
在不加BsonElement
的情况下,通过数据映射写到数据集中的文档,字段名就是变量名,上面这个例子,字段名就是post_time
。
加上BsonElement
后,数据集中的字段名会变为pt
。
看名称就知道,这是用来设置字段的默认值的。
看代码:
[BsonDefaultValue("This is a default title")]
public string title { get; set; }
当写入的时候,如果映射中不传入值,则数据库会把这个默认值存到数据集中。
这个属性是用来在映射类中的数据类型和数据集中的数据类型做转换的。
看代码:
[BsonRepresentation(BsonType.String)]
public int favor { get; set; }
这段代表表示,在映射类中,favor
字段是int
类型的,而存到数据集中,会保存为string
类型。
前边Decimal
转换和枚举转换,就是用的这个属性。
这个属性用来忽略某些字段。忽略的意思是:映射类中某些字段,不希望被保存到数据集中。
看代码:
[BsonIgnore]
public string ignore_string { get; set; }
这样,在保存数据时,字段ignore_string
就不会被保存到数据集中。
数据映射本身没什么新鲜的内容,但在MongoDB中,如果用好了映射,开发过程从效率到爽的程度,都不是SQL可以相比的。正所谓:
一入Mongo深似海,从此SQL是路人。
本文由哈喽比特于4年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/ffNZeaCIgnmK_JQDaNwpog
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。