用过 mongodb 吧, 这三个大坑踩过吗?
一:背景
1. 講故事
前段時間有位朋友在微信群問,在向 mongodb 中插入的時間為啥取出來的時候少了 8 個小時,8 在時間處理上是一個非常敏感的數(shù)字,又吉利又是一個普適的話題,后來我想想初次使用 mongodb 的朋友一定還會遇到各種新坑,比如說:插入的數(shù)據(jù)取不出來,看不爽的 ObjectID,時區(qū)不對等等,這篇就和大家一起聊一聊。
二:1號坑 插進(jìn)去的數(shù)據(jù)取不出來
1. 案例展示
這個問題是使用強(qiáng)類型操作 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; }}我去,這么簡單的一個操作還報錯,要初學(xué)到放棄嗎?挺急的,在線等!
2. 堆棧中深挖源碼
作為一個碼農(nóng)還得有鉆研代碼的能力,從錯誤信息中看說有一個?_id?不匹配 student 中的任何一個字段,然后把全部堆棧找出來。
System.FormatExceptionHResult=0x80131537Message=Element '_id' does not match any field or property of class Newtonsoft.Test.Student.Source=MongoDB.DriverStackTrace: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?表達(dá)式樹,我們知道表達(dá)式樹用于將一個領(lǐng)域的查詢結(jié)構(gòu)轉(zhuǎn)換為另一個領(lǐng)域的查詢結(jié)構(gòu),但要尋找如何構(gòu)建這個方法體就比較耗時間了,接下來還是用 dnspy 去調(diào)試看看有沒有更深層次的堆棧。
這個堆棧信息就非常清楚了,原來是在?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,找了一圈源碼,發(fā)現(xiàn)這里是關(guān)鍵:
也就是:?foreach (IBsonClassMapAttribute bsonClassMapAttribute in classMap.ClassType.GetTypeInfo().GetCustomAttributes(false).OfType<IBsonClassMapAttribute>())這句話,這里的 classMap 就是 student,只有讓 foreach 得以執(zhí)行才能有望 classMap.IgnoreExtraElements 賦值為 true ,接下來找找看在類上有沒有類似?IgnoreExtraElements?的 Attribute,嘿嘿,還真有一個類似的:?BsonIgnoreExtraElements?,如下代碼:
[BsonIgnoreExtraElements]public class Student{public string StudentName { get; set; }public DateTime Created { get; set; }}接下來執(zhí)行一下代碼,可以看到問題搞定:
如果你想驗證的話,可以繼續(xù)用 dnspy 去驗證一下源碼哈,如下代碼所示:
接下來還有一種辦法就是增加 _id 字段,如果你不知道用什么類型接,那就用object就好啦,后續(xù)再改成真正的類型。
三:2號坑 DateTime 時區(qū)不對
如果你細(xì)心的話,你會發(fā)現(xiàn)剛才案例中的 Created 時間是?2020/8/16 4:24:57, 大家請放心,我不會傻到凌晨4點(diǎn)還在寫代碼,好了哈,看看到底問題在哪吧, 可以先看看 mongodb 中的記錄數(shù)據(jù),如下:
{"_id" : ObjectId("5f38b83e0351908eedac60c9"),"StudentName" : "hxc","Created" : ISODate("2020-08-16T04:38:22.587Z") }從 ISODate 可以看出,這是格林威治時間,按照0時區(qū)存儲,所以這個問題轉(zhuǎn)成了如何在獲取數(shù)據(jù)的時候,自動將 ISO 時間轉(zhuǎn)成 Local 時間就可以了,如果你看過底層源碼,你會發(fā)現(xiàn)在 mongodb 中每個實(shí)體的每個類型都有一個專門的?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; }可以看出,如果當(dāng)前的?this._kind= DateTimeKind.Local?的話,就將 UTC 時間轉(zhuǎn)成 Local 時間,如果你有上一個坑的經(jīng)驗,你大概就知道應(yīng)該也是用特性注入的,
[BsonDateTimeOptions(Kind = DateTimeKind.Local)]public DateTime Created { get; set; }不信的話,我調(diào)試給你看看哈。
接下來再看看?this._kind?是怎么被賦的。
四:3號坑 自定義ObjectID
在第一個坑中,不知道大家看沒看到類似這樣的語句:?ObjectId("5f38b83e0351908eedac60c9")?,乍一看像是一個 GUID,當(dāng)然肯定不是,這是mongodb自己組建了一個 number 組合的十六進(jìn)制表示,姑且不說性能如何,反正看著不是很舒服,畢竟大家都習(xí)慣使用 int/long 類型展示的主鍵ID。
那接下來的問題是:如何改成我自定義的 number ID 呢?當(dāng)然可以,只要實(shí)現(xiàn)?IIdGenerator?接口即可,那主鍵ID的生成,我準(zhǔn)備用?雪花算法,完整代碼如下:
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:
四:總結(jié)
好了,這三個坑,我想很多剛接觸 mongodb 的朋友是一定會遇到的困惑,總結(jié)一下方便后人乘涼,結(jié)果不重要,重要的還是探索問題的思路和不擇手段????????????。
總結(jié)
以上是生活随笔為你收集整理的用过 mongodb 吧, 这三个大坑踩过吗?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [PBI催化剂]国际水准,中国首款重量级
- 下一篇: [C#.NET 拾遗补漏]07:迭代器和