MongoDB via Dotnet Core数据映射详解

用好数据映射,MongoDB via Dotnet Core开发变会成一件超级快乐的事。html

1、前言

MongoDB这几年已经成为NoSQL的头部数据库。git

因为MongoDB free schema的特性,使得它在互联网应用方面优于常规数据库,成为了至关一部分大厂的主数据选择;而它的快速布署和开发简单的特色,也吸引着大量小开发团队的支持。github

关于MongoDB快速布署,我在15分钟从零开始搭建支持10w+用户的生产环境(二)里有写,须要了能够去看看。web

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

方式1、BsonDocument方式数据库

BsonDocument方式,适合能熟练使用MongoDB Shell的开发者。MongoDB Driver提供了彻底覆盖Shell命令的各类方式,来处理用户的CRUD操做。json

这种方法自由度很高,能够在不须要知道完整数据集结构的状况下,完成数据库的CRUD操做。c#

方式2、数据映射方式数组

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

下面,对数据映射的各个部分,我会逐个说明。

    为了防止不提供原网址的转载,特在这里加上原文连接:http://www.javashuo.com/article/p-mhcfbzyw-kn.html

2、开发环境&基础工程

这个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上安装便可。

3、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()
    
{
    }
}

4、字段映射

从上面的代码中,咱们看到,在生成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"
}

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数据库的特点。这样能够很方便的把相关的数据组织到一条记录中,方便展现时的查询。

咱们在项目中添加两个类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"
            }
        ]
    }
}

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

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

代码简单,一看就懂。

注意,使用这个方法,上边那个对于时间加的属性[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跟当前时间显示彻底同样了,你统计,你分组,能够随便霍霍了。

7. Dictionary字段

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

数据声明很简单:

public Dictionary<stringint> extra_info { get; set; }

MongoDB定义了三种保存属性:DocumentArrayOfDocumentsArrayOfArrays,默认是Document

属性写法是这样的:

[BsonDictionaryOptions(DictionaryRepresentation.ArrayOfDocuments)]
public Dictionary<stringint> 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

5、其它映射属性

上一章介绍了数据映射的完整内容。除了这些内容,MongoDB还给出了一些映射属性,供你们看心情使用。

1. BsonElement属性

这个属性是用来改数据集中的字段名称用的。

看代码:

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

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

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

2. BsonDefaultValue属性

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

看代码:

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

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

3. BsonRepresentation属性

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

看代码:

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

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

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

4. BsonIgnore属性

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

看代码:

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

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

6、总结

数据映射自己没什么新鲜的内容,但在MongoDB中,若是用好了映射,开发过程从效率到爽的程度,都不是SQL能够相比的。正所谓:

一入Mongo深似海,今后SQL是路人。

谢谢你们!

(全文完)

本文的配套代码在https://github.com/humornif/Demo-Code/tree/master/0015/demo

 


 

微信公众号:老王Plus

扫描二维码,关注我的公众号,能够第一时间获得最新的我的文章和内容推送

本文版权归做者全部,转载请保留此声明和原文连接

相关文章
相关标签/搜索