一:背景

1. 讲故事

前段时间有位朋友在微信群问,在向 mongodb 中插入的时间为啥取出来的时候少了 8 个小时,8 在时间处理上是一个非常敏感的数字,又吉利又是一个普适的话题,后来我想想初次使用 mongodb 的朋友一定还会遇到各种新坑,比如说: 插入的数据取不出来,看不爽的 ObjectID,时区不对等等,这篇就和大家一起聊一聊。

二: 1号坑 插进去的数据取不出来

1. 案例展示

这个问题是使用强类型操作 mongodb 你一定会遇到的问题,案例代码如下:


class Program
{
static void Main(string[] args)
{
var client = new MongoClient("mongodb://192.168.1.128:27017");
var database = client.GetDatabase("school");
var table = database.GetCollection<Student>("student"); table.InsertOne(new Student() { StudentName = "hxc", Created = DateTime.Now }); var query = table.AsQueryable().ToList(); }
} public class Student
{
public string StudentName { get; set; } public DateTime Created { get; set; }
}

我去,这么简单的一个操作还报错,要初学到放弃吗? 挺急的,在线等!

2. 堆栈中深挖源码

作为一个码农还得有钻研代码的能力,从错误信息中看说有一个 _id 不匹配 student 中的任何一个字段,然后把全部堆栈找出来。


System.FormatException
HResult=0x80131537
Message=Element '_id' does not match any field or property of class Newtonsoft.Test.Student.
Source=MongoDB.Driver
StackTrace:
at MongoDB.Driver.Linq.MongoQueryProviderImpl`1.Execute(Expression expression)
at MongoDB.Driver.Linq.MongoQueryableImpl`2.GetEnumerator()
at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
at Newtonsoft.Test.Program.Main(String[] args) in E:\crm\JsonNet\Newtonsoft.Test\Program.cs:line 32

接下来就用 dnspy 去定位一下 MongoQueryProviderImpl.Execute 到底干的啥,截图如下:

我去,这代码硬核哈,用了 LambdaExpression 表达式树,我们知道表达式树用于将一个领域的查询结构转换为另一个领域的查询结构,但要寻找如何构建这个方法体就比较耗时间了,接下来还是用 dnspy 去调试看看有没有更深层次的堆栈。

这个堆栈信息就非常清楚了,原来是在 MongoDB.Bson.Serialization.BsonClassMapSerializer.DeserializeClass 方法中出了问题,接下来找到问题代码,简化如下:


public TClass DeserializeClass(BsonDeserializationContext context)
{
while (reader.ReadBsonType() != BsonType.EndOfDocument)
{
TrieNameDecoder<int> trieNameDecoder = new TrieNameDecoder<int>(elementTrie);
string text = reader.ReadName(trieNameDecoder);
if (trieNameDecoder.Found)
{
int value = trieNameDecoder.Value;
BsonMemberMap bsonMemberMap = allMemberMaps[value];
}
else
{
if (!this._classMap.IgnoreExtraElements)
{
throw new FormatException(string.Format("Element '{0}' does not match any field or property of class {1}.", text, this._classMap.ClassType.FullName));
}
reader.SkipValue();
}
}
}

上面的代码逻辑非常清楚,要么 student 中存在 _id 字段,也就是 trieNameDecoder.Found, 要么使用 忽略未知的元素,也就是 this._classMap.IgnoreExtraElements,添加字段容易,接下来看看怎么让 IgnoreExtraElements = true,找了一圈源码,发现这里是关键:

也就是: foreach (IBsonClassMapAttribute bsonClassMapAttribute in classMap.ClassType.GetTypeInfo().GetCustomAttributes(false).OfType<IBsonClassMapAttribute>())这句话,这里的 classMap 就是 student,只有让 foreach 得以执行才能有望 classMap.IgnoreExtraElements 赋值为 true ,接下来找找看在类上有没有类似 IgnoreExtraElements 的 Attribute,嘿嘿,还真有一个类似的: BsonIgnoreExtraElements ,如下代码:


[BsonIgnoreExtraElements]
public class Student
{
public string StudentName { get; set; } public DateTime Created { get; set; }
}

接下来执行一下代码,可以看到问题搞定:

如果你想验证的话,可以继续用 dnspy 去验证一下源码哈,如下代码所示:

接下来还有一种办法就是增加 _id 字段,如果你不知道用什么类型接,那就用object就好啦,后续再改成真正的类型。

三: 2号坑 DateTime 时区不对

如果你细心的话,你会发现刚才案例中的 Created 时间是 2020/8/16 4:24:57, 大家请放心,我不会傻到凌晨4点还在写代码,好了哈,看看到底问题在哪吧, 可以先看看 mongodb 中的记录数据,如下:


{
"_id" : ObjectId("5f38b83e0351908eedac60c9"),
"StudentName" : "hxc",
"Created" : ISODate("2020-08-16T04:38:22.587Z")
}

从 ISODate 可以看出,这是格林威治时间,按照0时区存储,所以这个问题转成了如何在获取数据的时候,自动将 ISO 时间转成 Local 时间就可以了,如果你看过底层源码,你会发现在 mongodb 中每个实体的每个类型都有一个专门的 XXXSerializer,如下图:

接下来就好好研读一下里面的 Deserialize 方法即可,代码精简后如下:


public override DateTime Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
IBsonReader bsonReader = context.Reader;
BsonType currentBsonType = bsonReader.GetCurrentBsonType();
DateTime value; switch (this._kind)
{
case DateTimeKind.Unspecified:
case DateTimeKind.Local:
value = DateTime.SpecifyKind(BsonUtils.ToLocalTime(value), this._kind);
break;
case DateTimeKind.Utc:
value = BsonUtils.ToUniversalTime(value);
break;
}
return value;
}

可以看出,如果当前的 this._kind= DateTimeKind.Local 的话,就将 UTC 时间转成 Local 时间,如果你有上一个坑的经验,你大概就知道应该也是用特性注入的,


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

不信的话,我调试给你看看哈。

接下来再看看 this._kind 是怎么被赋的。

四: 3号坑 自定义ObjectID

在第一个坑中,不知道大家看没看到类似这样的语句: ObjectId("5f38b83e0351908eedac60c9") ,乍一看像是一个 GUID,当然肯定不是,这是mongodb自己组建了一个 number 组合的十六进制表示,姑且不说性能如何,反正看着不是很舒服,毕竟大家都习惯使用 int/long 类型展示的主键ID。

那接下来的问题是:如何改成我自定义的 number ID 呢? 当然可以,只要实现 IIdGenerator 接口即可,那主键ID的生成,我准备用 雪花算法,完整代码如下:


class Program
{
static void Main(string[] args)
{
var client = new MongoClient("mongodb://192.168.1.128:27017");
var database = client.GetDatabase("school");
var table = database.GetCollection<Student>("student"); table.InsertOne(new Student() { Created = DateTime.Now });
table.InsertOne(new Student() { Created = DateTime.Now });
}
} class Student
{
[BsonId(IdGenerator = typeof(MyGenerator))]
public long ID { get; set; } [BsonDateTimeOptions(Kind = DateTimeKind.Local)]
public DateTime Created { get; set; }
} public class MyGenerator : IIdGenerator
{
private static readonly IdWorker worker = new IdWorker(1, 1); public object GenerateId(object container, object document)
{
return worker.NextId();
} public bool IsEmpty(object id)
{
return id == null || Convert.ToInt64(id) == 0;
}
}

然后去看一下 mongodb 生成的 json:

四: 总结

好了,这三个坑,我想很多刚接触 mongodb 的朋友是一定会遇到的困惑,总结一下方便后人乘凉,结果不重要,重要的还是探索问题的思路和不择手段。

用过 mongodb 吧, 这三个大坑踩过吗?的更多相关文章

  1. MongoDB学习笔记三—增删改文档上

    插入insert 单条插入 > db.foo.insert({"bar":"baz"}) WriteResult({ }) 批量插入 > db.fo ...

  2. MongoDB【第三篇】MongoDB基本操作

    MongoDB的基本操作包括文档的创建.删除.和更新 文档插入 1.插入 #查看当前都有哪些数据库 > show dbs; local 0.000GB tim 0.000GB #使用 tim数据 ...

  3. Mongodb学习笔记三(Mongodb索引操作及性能测试)

    第三章 索引操作及性能测试 索引在大数据下的重要性就不多说了 下面测试中用到了mongodb的一个客户端工具Robomongo,大家可以在网上选择下载.官网下载地址:http://www.robomo ...

  4. MongoDB学习笔记三:查询

    MongoDB中使用find来进行查询.查询就是返回一个集合中文档的子集,子集合的范围从0个文档到整个集合.find的第一个参数决定了要返回哪些文档,其形式也是一个文档,说明要执行的查询细节.空的查询 ...

  5. MongoDB 复制集 (三) 内部数据同步

    一 数据同步        一个健康的secondary在运行时,会选择一个离自己最近的,数据比自己新的节点进行数据同步.选定节点后,它会从这个节点拉取oplog同步日志,具体流程是这样的:      ...

  6. MongoDB学习笔记(三) 在MVC模式下通过Jqgrid表格操作MongoDB数据

    看到下图,是通过Jqgrid实现表格数据的基本增删查改的操作.表格数据增删改是一般企业应用系统开发的常见功能,不过不同的是这个表格数据来源是非关系型的数据库MongoDB.nosql虽然概念新颖,但是 ...

  7. MongoDB【第三篇】RockMongo 的安装

    第一步:准备 1. 安装 Nginx 参照 Nginx[第一篇]安装 2. 安装 php 参照 PHP[第一篇]安装 3. RockMongo 安装包 rockmongo-v1.0.5.r53.zip ...

  8. MongoDB系列:三、springboot整合mongoDB的简单demo

    在上篇 MongoDB常用操作练习 中,我们在命令提示符窗口使用简单的mongdb的方法操作数据库,实现增删改查及其他的功能.在本篇中,我们将mongodb与spring boot进行整合,也就是在j ...

  9. MongoDB 教程(三):MongoDB 的下载、安装和配置

    一.下载 下载地址:https://www.mongodb.com/download-center#community(这里是Windows 版,其他版本也可以在该网页进行下载) 版本选择: Mong ...

随机推荐

  1. 高效C++:设计与声明

    C++软件开发可以理解为设计一系列的类,让这些类相互使用,最终实现我们所需要的功能.类与类之间的相互关系可以很复杂,也可以很简单,如何简单高效的描述类与类之间的关系是设计的难点之一.遵循本文所提供的方 ...

  2. C++语法小记---一个有趣的现象

    下面的代码会飞吗? #include <iostream> #include <string> using namespace std; class Test { public ...

  3. 【JVM之内存与垃圾回收篇】StringTable

    StringTable String的基本特性 String:字符串,使用一对 "" 引起来表示 String s1 = "Nemo"; // 字面量的定义方式 ...

  4. 字符串学习总结(Hash & Manacher & KMP)

    前言 终于开始学习新的东西了,总结一下字符串的一些知识. NO.1 字符串哈希(Hash) 定义 即将一个字符串转化成一个整数,并保证字符串不同,得到的哈希值不同,这样就可以用来判断一个该字串是否重复 ...

  5. 题解 洛谷 P4547 【[THUWC2017]随机二分图】

    根据题意,题目中所求的即为所有\(n!\)种完美匹配的各自的出现概率之和再乘上\(2^n\)的值. 发现\(n\)很小,考虑状压\(DP\).设\(f_{S,T}\)为左部图匹配情况为\(S\),右部 ...

  6. JMS微服务开发示例(一)Hello world

    网关部署 1.在网关服务器上,安装.net core 3.1运行环境: 2.到 https://www.cnblogs.com/IWings/p/13354541.html 下载Gateway.zip ...

  7. 最大连续区间(HDU-1540)

    HDU1540 线段树最大连续区间. 给定长度为n的数组,m次操作. 操作D,删除给定节点. 操作R,恢复最后一个删除的节点. 操作Q,询问给定节点的最大连续区间 维护三个值,区间的最大左连续区间,最 ...

  8. Springboot(一)springboot简介与入门程序

    一.springboot简介: 对spring框架盛行了多年的java方向开发人员来说,每个人java开发已经把spring框架当做开发中不可或缺的一部分. 之前传统的模式都是以application ...

  9. pandas之groupby分组与pivot_table透视

    一.groupby 类似excel的数据透视表,一般是按照行进行分组,使用方法如下. df.groupby(by=None, axis=0, level=None, as_index=True, so ...

  10. python学习笔记1 -- 函数式编程之高阶函数 使用函数作为返回值

    使用函数作为返回值,看起来就很高端有木有,前面了解过函数名本身就是一个变量,就比如abs()函数,abs只是变量名,而abs()才是函数调用,那么我们如果把ads这个变量作为返回值返回会怎么样呢,这就 ...