MongoDB via Dotnet Core数据映射详解

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

例如,我们可以写成:

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

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

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 = "13811111111",
    };
    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"
            }, 
            {
                "13811111111"
            }
        ]
    }
}

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

5. 枚举字段

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

创建一个枚举TagEnumeration

enum TagEnumeration
{
    CSharp = 1,
    Python = 2,
}

加到public TagEnumeration tag { get; set; }
}

修改Demo代码:

/* 后边代码略过 */
}

运行后看数据:

"5ef1eb87cbb6b109031fcc31"),1); word-wrap: inherit !important; word-break: inherit !important">"13811111111"
            }
        ]
    },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, /* 2020-06-23T20:12:40.463+0000 */
    };
    /* 后边代码略过 */
}

运行看数据:

"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

创建完了,怎么用?

如果你只想对某个特定映射的特定字段使用,比方只对CollectionModelpost_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定义了三种保存属性:DocumentArrayOfDocumentsArrayOfArrays,默认是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

 

 

微信公众号:老王Plus

扫描二维码,关注个人公众号,可以第一时间得到最新的个人文章和内容推送

本文版权归作者所有,转载请保留此声明和原文链接

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


在上文中,我介绍了事件驱动型架构的一种简单的实现,并演示了一个完整的事件派发、订阅和处理的流程。这种实现太简单了,百十行代码就展示了一个基本工作原理。然而,要将这样的解决方案运用到实际生产环境,还有很长的路要走。今天,我们就研究一下在事件处理器中,对象生命周期的管理问题。事实上,不仅仅是在事件处理器
上文已经介绍了Identity Service的实现过程。今天我们继续,实现一个简单的Weather API和一个基于Ocelot的API网关。 回顾 《Angular SPA基于Ocelot API网关与IdentityServer4的身份认证与授权(一)》 Weather API Weather
最近我为我自己的应用开发框架Apworks设计了一套案例应用程序,并以Apache 2.0开源,开源地址是:https://github.com/daxnet/apworks-examples,目的是为了让大家更为方便地学习和使用.NET Core、最新的前端开发框架Angular,以及Apwork
HAL(Hypertext Application Language,超文本应用语言)是一种RESTful API的数据格式风格,为RESTful API的设计提供了接口规范,同时也降低了客户端与服务端接口的耦合度。很多当今流行的RESTful API开发框架,包括Spring REST,也都默认支
在前面两篇文章中,我详细介绍了基本事件系统的实现,包括事件派发和订阅、通过事件处理器执行上下文来解决对象生命周期问题,以及一个基于RabbitMQ的事件总线的实现。接下来对于事件驱动型架构的讨论,就需要结合一个实际的架构案例来进行分析。在领域驱动设计的讨论范畴,CQRS架构本身就是事件驱动的,因此,
HAL,全称为Hypertext Application Language,它是一种简单的数据格式,它能以一种简单、统一的形式,在API中引入超链接特性,使得API的可发现性(discoverable)更强,并具有自描述的特点。使用了HAL的API会更容易地被第三方开源库所调用,并且使用起来也很方便
何时使用领域驱动设计?其实当你的应用程序架构设计是面向业务的时候,你已经开始使用领域驱动设计了。领域驱动设计既不是架构风格(Architecture Style),也不是架构模式(Architecture Pattern),它也不是一种软件开发方法论,所以,是否应该使用领域驱动设计,以及什么时候使用
《在ASP.NET Core中使用Apworks快速开发数据服务》一文中,我介绍了如何使用Apworks框架的数据服务来快速构建用于查询和管理数据模型的RESTful API,通过该文的介绍,你会看到,使用Apworks框架开发数据服务是何等简单快捷,提供的功能也非常多,比如对Hypermedia的
在上一讲中,我们已经完成了一个完整的案例,在这个案例中,我们可以通过Angular单页面应用(SPA)进行登录,然后通过后端的Ocelot API网关整合IdentityServer4完成身份认证。在本讲中,我们会讨论在当前这种架构的应用程序中,如何完成用户授权。 回顾 《Angular SPA基于
Keycloak是一个功能强大的开源身份和访问管理系统,提供了一整套解决方案,包括用户认证、单点登录(SSO)、身份联合、用户注册、用户管理、角色映射、多因素认证和访问控制等。它广泛应用于企业和云服务,可以简化和统一不同应用程序和服务的安全管理,支持自托管或云部署,适用于需要安全、灵活且易于扩展的用
3月7日,微软发布了Visual Studio 2017 RTM,与之一起发布的还有.NET Core Runtime 1.1.0以及.NET Core SDK 1.0.0,尽管这些并不是最新版,但也已经从preview版本升级到了正式版。所以,在安装Visual Studio 2017时如果启用了
在上文中,我介绍了如何在Ocelot中使用自定义的中间件来修改下游服务的response body。今天,我们再扩展一下设计,让我们自己设计的中间件变得更为通用,使其能够应用在不同的Route上。比如,我们可以设计一个通用的替换response body的中间件,然后将其应用在多个Route上。 O
不少关注我博客的朋友都知道我在2009年左右开发过一个名为Apworks的企业级应用程序开发框架,旨在为分布式企业系统软件开发提供面向领域驱动(DDD)的框架级别的解决方案,并对多种系统架构风格提供支持。这个框架的开发和维护我坚持了很久,一直到2015年,我都一直在不停地重构这个项目。目前这个项目在
好吧,这个题目我也想了很久,不知道如何用最简单的几个字来概括这篇文章,原本打算取名《Angular单页面应用基于Ocelot API网关与IdentityServer4ʺSP.NET Identity实现身份认证与授权》,然而如你所见,这样的名字实在是太长了。所以,我不得不缩写“单页面应用”几个字
在前面两篇文章中,我介绍了基于IdentityServer4的一个Identity Service的实现,并且实现了一个Weather API和基于Ocelot的API网关,然后实现了通过Ocelot API网关整合Identity Service做身份认证的API请求。今天,我们进入前端开发,设计
Ocelot是ASP.NET Core下的API网关的一种实现,在微服务架构领域发挥了非常重要的作用。本文不会从整个微服务架构的角度来介绍Ocelot,而是介绍一下最近在学习过程中遇到的一个问题,以及如何使用中间件(Middleware)来解决这样的问题。 问题描述 在上文中,我介绍了一种在Angu
在大数据处理和人工智能时代,数据工厂(Data Factory)无疑是一个非常重要的大数据处理平台。市面上也有成熟的相关产品,比如Azure Data Factory,不仅功能强大,而且依托微软的云计算平台Azure,为大数据处理提供了强大的计算能力,让大数据处理变得更为稳定高效。由于工作中我的项目
在上文中,我们讨论了事件处理器中对象生命周期的问题,在进入新的讨论之前,首先让我们总结一下,我们已经实现了哪些内容。下面的类图描述了我们已经实现的组件及其之间的关系,貌似系统已经变得越来越复杂了。其中绿色的部分就是上文中新实现的部分,包括一个简单的Event Store,一个事件处理器执行上下文的接
在之前《在ASP.NET Core中使用Apworks快速开发数据服务》一文的评论部分,.NET大神张善友为我提了个建议,可以使用Compile As a Service的Roslyn为语法解析提供支持。在此非常感激友哥给我的建议,也让我了解了一些Roslyn的知识。使用Roslyn的一个很大的好处
很长一段时间以来,我都在思考如何在ASP.NET Core的框架下,实现一套完整的事件驱动型架构。这个问题看上去有点大,其实主要目标是为了实现一个基于ASP.NET Core的微服务,它能够非常简单地订阅来自于某个渠道的事件消息,并对接收到的消息进行处理,于此同时,它还能够向该渠道发送事件消息,以便