用过 mongodb 吧, 这三个大坑踩过吗?
一:背景
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 吧, 这三个大坑踩过吗?的更多相关文章
- MongoDB学习笔记三—增删改文档上
插入insert 单条插入 > db.foo.insert({"bar":"baz"}) WriteResult({ }) 批量插入 > db.fo ...
- MongoDB【第三篇】MongoDB基本操作
MongoDB的基本操作包括文档的创建.删除.和更新 文档插入 1.插入 #查看当前都有哪些数据库 > show dbs; local 0.000GB tim 0.000GB #使用 tim数据 ...
- Mongodb学习笔记三(Mongodb索引操作及性能测试)
第三章 索引操作及性能测试 索引在大数据下的重要性就不多说了 下面测试中用到了mongodb的一个客户端工具Robomongo,大家可以在网上选择下载.官网下载地址:http://www.robomo ...
- MongoDB学习笔记三:查询
MongoDB中使用find来进行查询.查询就是返回一个集合中文档的子集,子集合的范围从0个文档到整个集合.find的第一个参数决定了要返回哪些文档,其形式也是一个文档,说明要执行的查询细节.空的查询 ...
- MongoDB 复制集 (三) 内部数据同步
一 数据同步 一个健康的secondary在运行时,会选择一个离自己最近的,数据比自己新的节点进行数据同步.选定节点后,它会从这个节点拉取oplog同步日志,具体流程是这样的: ...
- MongoDB学习笔记(三) 在MVC模式下通过Jqgrid表格操作MongoDB数据
看到下图,是通过Jqgrid实现表格数据的基本增删查改的操作.表格数据增删改是一般企业应用系统开发的常见功能,不过不同的是这个表格数据来源是非关系型的数据库MongoDB.nosql虽然概念新颖,但是 ...
- MongoDB【第三篇】RockMongo 的安装
第一步:准备 1. 安装 Nginx 参照 Nginx[第一篇]安装 2. 安装 php 参照 PHP[第一篇]安装 3. RockMongo 安装包 rockmongo-v1.0.5.r53.zip ...
- MongoDB系列:三、springboot整合mongoDB的简单demo
在上篇 MongoDB常用操作练习 中,我们在命令提示符窗口使用简单的mongdb的方法操作数据库,实现增删改查及其他的功能.在本篇中,我们将mongodb与spring boot进行整合,也就是在j ...
- MongoDB 教程(三):MongoDB 的下载、安装和配置
一.下载 下载地址:https://www.mongodb.com/download-center#community(这里是Windows 版,其他版本也可以在该网页进行下载) 版本选择: Mong ...
随机推荐
- canvas学习01
canvas 必须指定宽高,确定可绘图区域的大小 canvas标签里写的是浏览器不支持canvas时展示的内容 <canvas id="drawing" width=&quo ...
- DP学习记录Ⅰ
DP学习记录Ⅱ 前言 状态定义,转移方程,边界处理,这三部分想好了,就问题不大了.重点在状态定义,转移方程是基于状态定义的,边界处理是方便转移方程的开始的.因此最好先在纸上写出自己状态的意义,越详细越 ...
- 放弃dagger?Anrdoi依赖注入框架koin
Koin 是什么 Koin 是为 Kotlin 开发者提供的一个实用型轻量级依赖注入框架,采用纯 Kotlin 语言编写而成,仅使用功能解析,无代理.无代码生成.无反射. 官网地址 优势 依赖注入好处 ...
- C#递归的简单实例
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threa ...
- java基础(十)--空指针异常
空指针异常即:java.lang.NUllPointException异常,主要用于在对象为null的情况下,调用对象的方法或对象的属性时会抛出. 举例说明: public class TestBas ...
- NGINX 上的限流
NGINX 上的限流(译) zlup YP小站 今天 前言 本文是对Rate Limiting with NGINX and NGINX Plus的主要内容(去掉了关于NGINX Plus相关内容) ...
- Spring JPA实现增删改查
1. 创建一个Spring工程 2.配置application文件 spring.datasource.driver-class-name= com.mysql.cj.jdbc.Driver spri ...
- 使用 eval(input()) 的便利
输入列表或者字典时使用eval可以自动转换为其类型 2020-06-18
- 读取 csv , xlsx 表格并添加总分列
import pandas as pd import numpy as np data = pd.read_excel('学生成绩表.csv',columns = ['学号','姓名','高数','英 ...
- Python os.stat_float_times() 方法
概述 os.stat_float_times() 方法用于决定stat_result是否以float对象显示时间戳.高佣联盟 www.cgewang.com 语法 stat_float_times() ...