MongoDB via Dotnet Core数据映射详解

536次阅读  |  发布于4年以前

一、前言

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上安装即可。

三、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,例如:stringintlongGUID等,但这就需要你自己去保证这些数据不超限并且唯一。

例如,我们可以写成:

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"
}
  1. 简单字段

就是常规的数据字段,直接写就成。

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)
}
  1. 一个的特殊的类型 - Decimal

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

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

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

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

  1. 类字段

把类作为一个数据集的一个字段。这是MongoDB作为文档NoSQL数据库的特色。这样可以很方便的把相关的数据组织到一条记录中,方便展示时的查询。

我们在项目中添加两个类ContactAuthor

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"
            }
        ]
    }
}

这样的数据结构,用着不要太爽!

  1. 枚举字段

枚举字段在使用时,跟类字段相似。

创建一个枚举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"
}
  1. 日期字段

日期字段会稍微有点坑。

这个坑其实并不源于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

创建完了,怎么用?

如果你只想对某个特定映射的特定字段使用,比方只对CollectionModelpost_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跟当前时间显示完全一样了,你统计,你分组,可以随便霍霍了。

  1. Dictionary字段

这个需求很奇怪。我们希望在一个Key-Value的文档中,保存一个Key-Value的数据。但这个需求又是真实存在的,比方保存一个用户的标签和标签对应的命中次数。

数据声明很简单:

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

MongoDB定义了三种保存属性:DocumentArrayOfDocumentsArrayOfArrays,默认是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

  1. BsonDefaultValue属性

看名称就知道,这是用来设置字段的默认值的。

看代码:

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

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

  1. BsonRepresentation属性

这个属性是用来在映射类中的数据类型和数据集中的数据类型做转换的。

看代码:

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

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

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

  1. BsonIgnore属性

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

看代码:

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

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

六、总结

数据映射本身没什么新鲜的内容,但在MongoDB中,如果用好了映射,开发过程从效率到爽的程度,都不是SQL可以相比的。正所谓:

一入Mongo深似海,从此SQL是路人。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8