MongoDB Dotnet Core数据映射

简介

作为一个数据库,基本的操作就是 CRUD。MongoDB 的 CRUD,不使用 SQL 来写,而是提供了更简单的方式。

BsonDocument方式

BsonDocument 方式,适合能熟练使用 MongoDB Shell 的开发者。MongoDB Driver 提供了完全覆盖 Shell 命令的各种方式,来处理用户的 CRUD 操作。

这种方法自由度很高,可以在不需要知道完整数据集结构的情况下,完成数据库的CRUD操作。

数据映射方式

数据映射是最常用的一种方式。准备好需要处理的数据类,直接把数据类映射到 MongoDB,并对数据集进行 CRUD 操作。

字段映射

ID字段

MongoDB 数据集中存放的数据,称之为文档(Document)。每个文档在存放时,都需要有一个ID,而这个 ID 的名称,固定叫 _id,类型是 MongoDB.Bson.ObjectId。

当建立映射时,如果给出 _id 字段,则 MongoDB 会采用这个 ID 做为这个文档的 ID ,如果不给出,MongoDB 会自动添加一个 _id 字段。在使用上是完全一样的。唯一的区别是,如果映射类中不写 _id,则 MongoDB 自动添加 _id 时,会用 ObjectId 作为这个字段的数据类型。

ObjectId 是一个全局唯一的数据。

MongoDB 允许使用其它类型的数据作为 ID,例如:string,int,long,GUID 等,但这就需要你自己去保证这些数据不超限并且唯一。

可以在类中修改 _id 名称为别的名称,但需要加一个描述属性 BsonId,BsonId 属性会告诉映射,topic_id 就是这个文档数据的ID 。MongoDB在保存时,会将这个 topic_id 转成 _id 保存到数据集中。

1
2
3
4
5
6
7
public class CollectionModel
{
[BsonId]
public ObjectId topic_id { get; set; }
public string title { get; set; }
public string content { get; set; }
}

注:在 MongoDB 数据集中,ID 字段的名称固定叫 _id。为了代码的阅读方便,可以在类中改为别的名称,但这不会影响 MongoDB 中存放的 ID 名称。

2. 特殊类型 - Decimal

MongoDB 在早期,是不支持 Decimal 的。直到 MongoDB v3.4 开始,数据库才正式支持 Decimal。

所以,如果使用的是 v3.4 以后的版本,可以直接使用,而如果是以前的版本,需要用以下的方式:

1
2
[BsonRepresentation(BsonType.Double, AllowTruncation = true)]
public decimal price { get; set; }

其实就是把 Decimal 通过映射,转为 Double 存储。

类字段

添加两个类 Contact 和 Author:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class Contact
{
public string mobile { get; set; }
}
public class Author
{
public string name { get; set; }
public List<Contact> contacts { get; set; }
}

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; }
}

/*
调用 代码:
*/
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);
}

文档结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{ 
"_id" : ObjectId("5ef1e635ce129908a22dfb5e"),
"title" : "Demo",
"content" : "Demo content",
"favor" : NumberInt(100),
"author" : {
"name" : "WangPlus",
"contacts" : [
{
"mobile" : "13800000000"
},
{
"mobile" : "13811111111"
}
]
}
}

枚举字段

创建一个枚举 TagEnumeration:

1
2
3
4
5
public enum TagEnumeration
{
CSharp = 1,
Python = 2,
}

加到 CollectionModel 中:

1
2
3
4
5
6
7
8
9
10
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代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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,
};

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);
}

保存后的文档:注意,tag 保存了枚举的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{ 
"_id" : ObjectId("5ef1eb87cbb6b109031fcc31"),
"title" : "Demo",
"content" : "Demo content",
"favor" : NumberInt(100),
"author" : {
"name" : "WangPlus",
"contacts" : [
{
"mobile" : "13800000000"
},
{
"mobile" : "13811111111"
}
]
},
"tag" : NumberInt(1)
}

可以保存枚举的字符串。只要在 CollectionModel 中,tag 声明上加个属性:

1
2
[BsonRepresentation(BsonType.String)]
public TagEnumeration tag { get; set; }

数据会变成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{ 
"_id" : ObjectId("5ef1ec448f1d540919d15904"),
"title" : "Demo",
"content" : "Demo content",
"favor" : NumberInt(100),
"author" : {
"name" : "WangPlus",
"contacts" : [
{
"mobile" : "13800000000"
},
{
"mobile" : "13811111111"
}
]
},
"tag" : "CSharp"
}

日期字段

CollectionModel 中增加一个时间字段:

1
public DateTime post_time { get; set; }

MongoDB 的 datetime 存储的是 unixtimestamp ,所以默认只能是 utc0 时区的,它问题出在 C# 的 DateTime 对时区的处理上遗留的问题,可以换成 DateTimeOffset。

如果只是保存(像上边这样),或者查询时使用时间作为条件(例如查询 post_time < DateTime.Now 的数据)时,是可以使用的,不会出现问题。

但是,如果是查询结果中有时间字段,那这个字段,会被 DateTime 默认设置为 DateTimeKind.Unspecified 类型。而这个类型,是无时区信息的,输出显示时,会造成混乱。

为了避免这种情况,在进行时间字段的映射时,需要加上属性:

1
2
[BsonDateTimeOptions(Kind = DateTimeKind.Local)]
public DateTime post_time { get; set; }

这样做,会强制 DateTime 类型的字段为 DateTimeKind.Local 类型。这时候,从显示到使用就正确了。

数据集中存放的是 UTC 时间,跟我们正常的时间有8小时时差,如果我们需要按日统计,比方每天的销售额/点击量。

按年月日时分秒拆开存放

1
2
3
4
5
6
7
8
9
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; }
}

MyDateTimeSerializer

1
2
3
4
5
6
7
8
9
10
11
12
13
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)]

对某个特定映射的特定字段使用,比方只对 CollectionModel 的 post_time 字段来使用,可以这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[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();
}

Dictionary字段

数据声明很简单:

public Dictionary<string, int> extra_info { get; set; }

MongoDB 定义了三种保存属性:Document、ArrayOfDocuments、ArrayOfArrays,默认是 Document。

属性写法是这样的:

1
2
[BsonDictionaryOptions(DictionaryRepresentation.ArrayOfDocuments)]
public Dictionary<string, int> extra_info { get; set; }

这三种属性下,保存在数据集中的数据结构有区别。多数情况用 DictionaryRepresentation.ArrayOfDocuments。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 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 就算了。数组套数组,查询条件写死人。

BsonElement属性

用来改数据集中的字段名称。

1
2
[BsonElement("pt")]
public DateTime post_time { get; set; }

在不加 BsonElement 的情况下,通过数据映射写到数据集中的文档,字段名就是变量名,上面这个例子,字段名就是 post_time 。

加上 BsonElement 后,数据集中的字段名会变为 pt。

BsonDefaultValue属性

设置字段的默认值。
当写入的时候,如果映射中不传入值,则数据库会把这个默认值存到数据集中。

1
2
[BsonDefaultValue("This is a default title")]
public string title { get; set; }

BsonRepresentation属性

用来在映射类中的数据类型和数据集中的数据类型做转换。

1
2
[BsonRepresentation(BsonType.String)]
public int favor { get; set; }

表示,在映射类中,favor 字段是 int 类型的,而存到数据集中,会保存为 string 类型。

前边 Decimal 转换和枚举转换,就是用的这个属性。

BsonIgnore属性

用来忽略某些字段。
忽略的意思是:映射类中某些字段,不希望被保存到数据集中。

1
2
[BsonIgnore]
public string ignore_string { get; set; }

这样,在保存数据时,字段 ignore_string 就不会被保存到数据集中。

参考:

MongoDB via Dotnet Core数据映射详解